From 19f39779da399b87bc5ca03345749ab040b27024 Mon Sep 17 00:00:00 2001 From: Andy Pols Date: Fri, 17 Apr 2026 12:47:30 +0000 Subject: [PATCH 01/25] chore: update ui --- package-lock.json | 1137 +-------------------------------------------- package.json | 44 +- 2 files changed, 28 insertions(+), 1153 deletions(-) diff --git a/package-lock.json b/package-lock.json index e35608362..984d89683 100644 --- a/package-lock.json +++ b/package-lock.json @@ -89,7 +89,6 @@ "@vitest/coverage-v8": "^3.2.4", "c8": "^11.0.0", "cross-env": "^10.1.0", - "cypress": "^15.9.0", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-cypress": "^5.2.1", @@ -1766,61 +1765,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@cypress/request": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.10.tgz", - "integrity": "sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~4.0.4", - "http-signature": "~1.4.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "performance-now": "^2.1.0", - "qs": "~6.14.1", - "safe-buffer": "^5.1.2", - "tough-cookie": "^5.0.0", - "tunnel-agent": "^0.6.0", - "uuid": "^8.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@cypress/request/node_modules/uuid": { - "version": "8.3.2", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@cypress/xvfb": { - "version": "1.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.1.0", - "lodash.once": "^4.1.1" - } - }, - "node_modules/@cypress/xvfb/node_modules/debug": { - "version": "3.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/@emotion/hash": { "version": "0.8.0", "license": "MIT" @@ -4514,16 +4458,6 @@ "@types/node": "*" } }, - "node_modules/@types/sinonjs__fake-timers": { - "version": "8.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/sizzle": { - "version": "2.3.8", - "dev": true, - "license": "MIT" - }, "node_modules/@types/supertest": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", @@ -4548,11 +4482,6 @@ "form-data": "^4.0.0" } }, - "node_modules/@types/tmp": { - "version": "0.2.6", - "dev": true, - "license": "MIT" - }, "node_modules/@types/validator": { "version": "13.15.10", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", @@ -4587,15 +4516,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.56.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", @@ -5286,39 +5206,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "license": "MIT", @@ -5350,25 +5237,6 @@ "node": ">=8" } }, - "node_modules/arch": { - "version": "2.2.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/archy": { "version": "1.0.0", "dev": true, @@ -5569,14 +5437,6 @@ "dev": true, "license": "MIT" }, - "node_modules/astral-regex": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/async": { "version": "3.2.5", "license": "MIT" @@ -5597,14 +5457,6 @@ "version": "0.4.0", "license": "MIT" }, - "node_modules/at-least-node": { - "version": "1.0.0", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/available-typed-arrays": { "version": "1.0.7", "license": "MIT", @@ -5618,19 +5470,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.13.2", - "dev": true, - "license": "MIT" - }, "node_modules/axios": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", @@ -5674,14 +5513,6 @@ ], "license": "MIT" }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, "node_modules/bcryptjs": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", @@ -5691,16 +5522,6 @@ "bcrypt": "bin/bcrypt" } }, - "node_modules/blob-util": { - "version": "2.0.2", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/bluebird": { - "version": "3.7.2", - "dev": true, - "license": "MIT" - }, "node_modules/bn.js": { "version": "4.12.3", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", @@ -5862,37 +5683,6 @@ "node": ">=14.20.1" } }, - "node_modules/buffer": { - "version": "5.7.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -6049,14 +5839,6 @@ "node": ">=8" } }, - "node_modules/cachedir": { - "version": "2.4.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/caching-transform": { "version": "4.0.0", "dev": true, @@ -6147,11 +5929,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/caseless": { - "version": "0.12.0", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/chalk": { "version": "4.1.2", "license": "MIT", @@ -6215,82 +5992,6 @@ "node": ">=6" } }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-table3": { - "version": "0.6.1", - "dev": true, - "license": "MIT", - "dependencies": { - "string-width": "^4.2.0" - }, - "engines": { - "node": "10.* || >= 12.*" - }, - "optionalDependencies": { - "colors": "1.4.0" - } - }, - "node_modules/cli-table3/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/cli-table3/node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-truncate": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/cli-truncate/node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/cliui": { "version": "8.0.1", "license": "ISC", @@ -6365,15 +6066,6 @@ "dev": true, "license": "MIT" }, - "node_modules/colors": { - "version": "1.4.0", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.1.90" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "license": "MIT", @@ -6428,22 +6120,6 @@ "node": ">=12.17" } }, - "node_modules/commander": { - "version": "6.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/common-tags": { - "version": "1.8.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/commondir": { "version": "1.0.1", "dev": true, @@ -6730,73 +6406,8 @@ "version": "2.6.21", "license": "MIT" }, - "node_modules/cypress": { - "version": "15.9.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-15.9.0.tgz", - "integrity": "sha512-Ks6Bdilz3TtkLZtTQyqYaqtL/WT3X3APKaSLhTV96TmTyudzSjc6EJsJCHmBb7DxO+3R12q3Jkbjgm/iPgmwfg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@cypress/request": "^3.0.10", - "@cypress/xvfb": "^1.2.4", - "@types/sinonjs__fake-timers": "8.1.1", - "@types/sizzle": "^2.3.2", - "@types/tmp": "^0.2.3", - "arch": "^2.2.0", - "blob-util": "^2.0.2", - "bluebird": "^3.7.2", - "buffer": "^5.7.1", - "cachedir": "^2.3.0", - "chalk": "^4.1.0", - "ci-info": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-table3": "0.6.1", - "commander": "^6.2.1", - "common-tags": "^1.8.0", - "dayjs": "^1.10.4", - "debug": "^4.3.4", - "enquirer": "^2.3.6", - "eventemitter2": "6.4.7", - "execa": "4.1.0", - "executable": "^4.1.1", - "extract-zip": "2.0.1", - "figures": "^3.2.0", - "fs-extra": "^9.1.0", - "hasha": "5.2.2", - "is-installed-globally": "~0.4.0", - "listr2": "^3.8.3", - "lodash": "^4.17.21", - "log-symbols": "^4.0.0", - "minimist": "^1.2.8", - "ospath": "^1.2.2", - "pretty-bytes": "^5.6.0", - "process": "^0.11.10", - "proxy-from-env": "1.0.0", - "request-progress": "^3.0.0", - "supports-color": "^8.1.1", - "systeminformation": "^5.27.14", - "tmp": "~0.2.4", - "tree-kill": "1.2.2", - "untildify": "^4.0.0", - "yauzl": "^2.10.0" - }, - "bin": { - "cypress": "bin/cypress" - }, - "engines": { - "node": "^20.1.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/cypress/node_modules/proxy-from-env": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", - "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", - "dev": true, - "license": "MIT" - }, - "node_modules/dargs": { - "version": "8.1.0", + "node_modules/dargs": { + "version": "8.1.0", "dev": true, "license": "MIT", "engines": { @@ -6806,17 +6417,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/dashdash": { - "version": "1.14.1", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/data-view-buffer": { "version": "1.0.2", "dev": true, @@ -6865,11 +6465,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/dayjs": { - "version": "1.11.11", - "dev": true, - "license": "MIT" - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -7114,15 +6709,6 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -7154,26 +6740,6 @@ "node": ">= 0.8" } }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/enquirer": { - "version": "2.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.1", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/entities": { "version": "1.1.2", "license": "BSD-2-Clause" @@ -7731,11 +7297,6 @@ "node": ">=6" } }, - "node_modules/eventemitter2": { - "version": "6.4.7", - "dev": true, - "license": "MIT" - }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", @@ -7750,39 +7311,6 @@ "node": ">=0.8.x" } }, - "node_modules/execa": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/executable": { - "version": "4.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.2.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/expect-type": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", @@ -7942,30 +7470,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/extend": { - "version": "3.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/extsprintf": { "version": "1.4.1", "engines": [ @@ -8066,38 +7570,6 @@ "fxparser": "src/cli/cli.js" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/figures": { - "version": "3.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/figures/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/file-entry-cache": { "version": "8.0.0", "dev": true, @@ -8268,14 +7740,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/forever-agent": { - "version": "0.6.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -8345,20 +7809,6 @@ ], "license": "MIT" }, - "node_modules/fs-extra": { - "version": "9.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "dev": true, @@ -8479,20 +7929,6 @@ "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-symbol-description": { "version": "1.1.0", "dev": true, @@ -8520,14 +7956,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/getpass": { - "version": "0.1.7", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - } - }, "node_modules/git-raw-commits": { "version": "4.0.0", "dev": true, @@ -8613,28 +8041,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/global-dirs": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ini": "2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/global-dirs/node_modules/ini": { - "version": "2.0.0", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/globals": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", @@ -8848,27 +8254,6 @@ "node": ">= 0.8" } }, - "node_modules/http-signature": { - "version": "1.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^2.0.2", - "sshpk": "^1.18.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/human-signals": { - "version": "1.1.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8.12.0" - } - }, "node_modules/husky": { "version": "9.1.7", "dev": true, @@ -9215,21 +8600,6 @@ "version": "1.1.3", "license": "MIT" }, - "node_modules/is-installed-globally": { - "version": "0.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "global-dirs": "^3.0.0", - "is-path-inside": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-map": { "version": "2.0.3", "dev": true, @@ -9283,14 +8653,6 @@ "node": ">=8" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -9410,17 +8772,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-url": { "version": "1.2.4", "dev": true, @@ -9556,11 +8907,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/isstream": { - "version": "0.1.2", - "dev": true, - "license": "MIT" - }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -9800,11 +9146,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbn": { - "version": "0.1.1", - "dev": true, - "license": "MIT" - }, "node_modules/jsesc": { "version": "3.1.0", "dev": true, @@ -9826,11 +9167,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-schema": { - "version": "0.4.0", - "dev": true, - "license": "(AFL-2.1 OR BSD-3-Clause)" - }, "node_modules/json-schema-traverse": { "version": "1.0.0", "dev": true, @@ -9841,11 +9177,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "dev": true, - "license": "ISC" - }, "node_modules/json5": { "version": "2.2.3", "dev": true, @@ -9857,17 +9188,6 @@ "node": ">=6" } }, - "node_modules/jsonfile": { - "version": "6.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/jsonparse": { "version": "1.3.1", "dev": true, @@ -9925,41 +9245,6 @@ "node": ">=10" } }, - "node_modules/jsprim": { - "version": "2.0.2", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - } - }, - "node_modules/jsprim/node_modules/extsprintf": { - "version": "1.3.0", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, - "node_modules/jsprim/node_modules/verror": { - "version": "1.10.0", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, "node_modules/jss": { "version": "10.10.0", "license": "MIT", @@ -10435,88 +9720,14 @@ "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/listr2": { - "version": "3.14.0", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^2.1.0", - "colorette": "^2.0.16", - "log-update": "^4.0.0", - "p-map": "^4.0.0", - "rfdc": "^1.3.0", - "rxjs": "^7.5.1", - "through": "^2.3.8", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "enquirer": ">= 2.3.0 < 3" - }, - "peerDependenciesMeta": { - "enquirer": { - "optional": true - } - } - }, - "node_modules/listr2/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/listr2/node_modules/p-map": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/listr2/node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/listr2/node_modules/wrap-ansi": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/load-plugin": { @@ -10631,85 +9842,6 @@ "dev": true, "license": "MIT" }, - "node_modules/log-symbols": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^4.3.0", - "cli-cursor": "^3.1.0", - "slice-ansi": "^4.0.0", - "wrap-ansi": "^6.2.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/log-update/node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "6.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/loose-envify": { "version": "1.4.0", "license": "MIT", @@ -10845,11 +9977,6 @@ "node": ">=8" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, "node_modules/methods": { "version": "1.1.2", "dev": true, @@ -10900,14 +10027,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -11144,17 +10263,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/nyc": { "version": "17.1.0", "dev": true, @@ -11469,20 +10577,6 @@ "wrappy": "1" } }, - "node_modules/onetime": { - "version": "5.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/openid-client": { "version": "6.8.1", "license": "MIT", @@ -11510,11 +10604,6 @@ "node": ">= 0.8.0" } }, - "node_modules/ospath": { - "version": "1.2.2", - "dev": true, - "license": "MIT" - }, "node_modules/own-keys": { "version": "1.0.1", "dev": true, @@ -11786,20 +10875,10 @@ "node_modules/pause": { "version": "0.0.1" }, - "node_modules/pend": { - "version": "1.2.0", - "dev": true, - "license": "MIT" - }, "node_modules/perfect-scrollbar": { "version": "1.5.6", "license": "MIT" }, - "node_modules/performance-now": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.1.1", "dev": true, @@ -11829,14 +10908,6 @@ "node": ">=0.10" } }, - "node_modules/pify": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/pkg-dir": { "version": "4.2.0", "dev": true, @@ -11974,17 +11045,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/pretty-bytes": { - "version": "5.6.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/proc-log": { "version": "3.0.0", "license": "ISC", @@ -12043,15 +11103,6 @@ "node": ">=10" } }, - "node_modules/pump": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "license": "MIT", @@ -12477,14 +11528,6 @@ "node": ">=4" } }, - "node_modules/request-progress": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "throttleit": "^1.0.0" - } - }, "node_modules/require-directory": { "version": "2.1.1", "license": "MIT", @@ -12547,18 +11590,6 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/rfdc": { "version": "1.4.1", "dev": true, @@ -13050,19 +12081,6 @@ "url": "https://github.com/steveukx/git-js?sponsor=1" } }, - "node_modules/slice-ansi": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/smart-buffer": { "version": "4.2.0", "license": "MIT", @@ -13151,30 +12169,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/sshpk": { - "version": "1.18.0", - "dev": true, - "license": "MIT", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -13420,14 +12414,6 @@ "node": ">=8" } }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "dev": true, @@ -13531,33 +12517,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/systeminformation": { - "version": "5.31.4", - "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.4.tgz", - "integrity": "sha512-lZppDyQx91VdS5zJvAyGkmwe+Mq6xY978BDUG2wRkWE+jkmUF5ti8cvOovFQoN5bvSFKCXVkyKEaU5ec3SJiRg==", - "dev": true, - "license": "MIT", - "os": [ - "darwin", - "linux", - "win32", - "freebsd", - "openbsd", - "netbsd", - "sunos", - "android" - ], - "bin": { - "systeminformation": "lib/cli.js" - }, - "engines": { - "node": ">=8.0.0" - }, - "funding": { - "type": "Buy me a coffee", - "url": "https://www.buymeacoffee.com/systeminfo" - } - }, "node_modules/table-layout": { "version": "4.1.1", "dev": true, @@ -13621,14 +12580,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/throttleit": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/through": { "version": "2.3.8", "dev": true, @@ -13733,30 +12684,6 @@ "node": ">=14.0.0" } }, - "node_modules/tldts": { - "version": "6.1.86", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^6.1.86" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "6.1.86", - "dev": true, - "license": "MIT" - }, - "node_modules/tmp": { - "version": "0.2.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, "node_modules/to-buffer": { "version": "1.2.1", "license": "MIT", @@ -13787,17 +12714,6 @@ "node": ">=0.6" } }, - "node_modules/tough-cookie": { - "version": "5.1.2", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^6.1.32" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/tr46": { "version": "3.0.0", "license": "MIT", @@ -13931,22 +12847,6 @@ "fsevents": "~2.3.3" } }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "dev": true, - "license": "Unlicense" - }, "node_modules/type-check": { "version": "0.4.0", "dev": true, @@ -14183,14 +13083,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/universalify": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/unpipe": { "version": "1.0.0", "license": "MIT", @@ -14198,14 +13090,6 @@ "node": ">= 0.8" } }, - "node_modules/untildify": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/update-browserslist-db": { "version": "1.1.3", "dev": true, @@ -15022,15 +13906,6 @@ "node": ">=8" } }, - "node_modules/yauzl": { - "version": "2.10.0", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, "node_modules/yn": { "version": "3.1.1", "dev": true, diff --git a/package.json b/package.json index 4b5e6232f..dbc54a6be 100644 --- a/package.json +++ b/package.json @@ -97,11 +97,13 @@ }, "dependencies": { "@aws-sdk/credential-providers": "^3.980.0", - "@fontsource/roboto": "^5.2.9", - "@material-ui/core": "^4.12.4", - "@material-ui/icons": "4.11.3", - "@primer/octicons-react": "^19.21.2", + "@headlessui/react": "^2.2.9", + "@heroicons/react": "^2.2.0", + "@primer/octicons-react": "^19.23.1", + "@primer/primitives": "^11.6.0", + "@primer/react": "^38.17.0", "@seald-io/nedb": "^4.1.2", + "@tanstack/react-query": "^5.97.0", "axios": "^1.13.4", "bcryptjs": "^3.0.3", "clsx": "^2.1.1", @@ -115,26 +117,23 @@ "express-http-proxy": "^2.1.2", "express-rate-limit": "^8.2.1", "express-session": "^1.19.0", - "font-awesome": "^4.7.0", - "history": "5.3.0", + "html-react-parser": "^5.2.17", "isomorphic-git": "^1.36.3", "jsonwebtoken": "^9.0.3", "load-plugin": "^6.0.3", "lodash": "^4.17.23", "lusca": "^1.7.0", - "material-design-icons": "^3.0.1", - "moment": "^2.30.1", + "luxon": "^3.7.2", "mongodb": "^5.9.2", "openid-client": "^6.8.1", "parse-diff": "^0.11.1", "passport": "^0.7.0", "passport-activedirectory": "^1.4.0", "passport-local": "^1.0.0", - "perfect-scrollbar": "^1.5.6", - "react": "^16.14.0", - "react-dom": "^16.14.0", - "react-html-parser": "^2.0.2", - "react-router-dom": "6.30.3", + "passport-openidconnect": "^0.1.2", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "react-router": "^7.14.1", "simple-git": "^3.30.0", "uuid": "^13.0.0", "validator": "^13.15.26", @@ -146,31 +145,31 @@ "@commitlint/cli": "^19.8.1", "@commitlint/config-conventional": "^19.8.1", "@eslint/compat": "^2.0.2", - "@eslint/js": "^9.39.2", + "@eslint/js": "^9.39.4", "@eslint/json": "^1.0.1", + "@tailwindcss/vite": "^4.2.2", "@types/activedirectory2": "^1.2.6", "@types/cors": "^2.8.19", - "@types/domutils": "^2.1.0", "@types/express": "^5.0.6", "@types/express-http-proxy": "^1.6.7", "@types/express-session": "^1.18.2", "@types/jsonwebtoken": "^9.0.10", - "@types/lodash": "^4.17.23", + "@types/lodash": "^4.17.24", "@types/lusca": "^1.7.5", + "@types/luxon": "^3.7.1", "@types/node": "^22.19.7", "@types/passport": "^1.0.17", "@types/passport-local": "^1.0.38", - "@types/react-dom": "^17.0.26", - "@types/react-html-parser": "^2.0.7", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", "@types/supertest": "^6.0.3", "@types/validator": "^13.15.10", "@types/yargs": "^17.0.35", - "@vitejs/plugin-react": "^5.1.2", + "@vitejs/plugin-react": "^5.2.0", "@vitest/coverage-v8": "^3.2.4", "c8": "^11.0.0", "cross-env": "^10.1.0", - "cypress": "^15.9.0", - "eslint": "^9.39.2", + "eslint": "^9.39.4", "eslint-config-prettier": "^10.1.8", "eslint-plugin-cypress": "^5.2.1", "eslint-plugin-license-header": "^0.9.0", @@ -183,10 +182,11 @@ "prettier": "^3.8.1", "quicktype": "^23.2.6", "supertest": "^7.2.2", + "tailwindcss": "^4.2.2", "ts-node": "^10.9.2", "tsx": "^4.21.0", "typescript": "^5.9.3", - "typescript-eslint": "^8.54.0", + "typescript-eslint": "^8.57.2", "vite": "^7.3.1", "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.2.4" From 08580b944e1311da129b83ba72cacb44b429db55 Mon Sep 17 00:00:00 2001 From: Andy Pols Date: Fri, 17 Apr 2026 12:55:29 +0000 Subject: [PATCH 02/25] chore: update tooling --- tsconfig.json | 6 +++++- vite.config.ts | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 331c876ef..5cd1a90c7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,11 @@ "outDir": "./dist", "rootDir": "./src", "noEmit": false, - "types": ["node"] + "types": ["node"], + "baseUrl": ".", + "paths": { + "@primer/react/experimental": ["./node_modules/@primer/react/dist/experimental/index"] + } }, "include": ["src"], "exclude": ["node_modules", "dist", "**/*.test.ts"] diff --git a/vite.config.ts b/vite.config.ts index 9ccfe3db1..e2602e6e6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -14,8 +14,9 @@ * limitations under the License. */ -import { defineConfig, loadEnv } from 'vite'; +import tailwindcss from '@tailwindcss/vite'; import react from '@vitejs/plugin-react'; +import { defineConfig, loadEnv } from 'vite'; export default ({ mode }: { mode: string }) => { const env = loadEnv(mode, process.cwd(), ''); @@ -29,7 +30,7 @@ export default ({ mode }: { mode: string }) => { ignored: ['**/.data/**', '**/.remote/**', '**/.tmp/**'], }, }, - plugins: [react()], + plugins: [tailwindcss(), react()], define: { 'process.env': JSON.stringify(env), }, From 440445f365b1a32430edb1bccf507283086b6d0b Mon Sep 17 00:00:00 2001 From: Andy Pols Date: Fri, 17 Apr 2026 13:00:12 +0000 Subject: [PATCH 03/25] chore: update core types, context, utilities, auth --- src/activity/canonicalRemoteUrl.ts | 53 +++++++ src/tailwind.css | 207 +++++++++++++++++++++++++++ src/ui/auth/AuthProvider.tsx | 2 +- src/ui/context.ts | 18 ++- src/ui/types.ts | 70 +++------- src/ui/utils.tsx | 215 ----------------------------- src/ui/utils/parseGitRemoteUrl.ts | 86 ++++++++++++ 7 files changed, 374 insertions(+), 277 deletions(-) create mode 100644 src/activity/canonicalRemoteUrl.ts create mode 100644 src/tailwind.css create mode 100644 src/ui/utils/parseGitRemoteUrl.ts diff --git a/src/activity/canonicalRemoteUrl.ts b/src/activity/canonicalRemoteUrl.ts new file mode 100644 index 000000000..6814bdf9f --- /dev/null +++ b/src/activity/canonicalRemoteUrl.ts @@ -0,0 +1,53 @@ +/** + * Copyright 2026 GitProxy Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** Strip ".git", slashes, used to compare push remote URLs to registered repo.url. */ +function normalizeRepoPathForMatch(path: string): string { + let p = path.replace(/^\/+|\/+$/gu, ''); + if (p.toLowerCase().endsWith('.git')) { + p = p.slice(0, -4); + } + return p; +} + +/** + * Stable key for matching a git remote (HTTPS or git@host:path) to a row in the repo catalog. + * Host is lowercased; path is lowercased for case-insensitive hosts (GitHub, typical GitLab). + */ +export function canonicalRemoteUrl(raw: string): string { + const input = raw.trim(); + if (!input) { + return ''; + } + try { + const u = new URL(input); + if (u.protocol !== 'http:' && u.protocol !== 'https:') { + return input.toLowerCase(); + } + const host = u.hostname.toLowerCase(); + const port = u.port ? `:${u.port}` : ''; + const path = normalizeRepoPathForMatch(u.pathname); + return `${host}${port}/${path}`.toLowerCase(); + } catch { + const m = /^git@([^:]+):(.+)$/iu.exec(input); + if (m) { + const host = m[1].toLowerCase(); + const path = normalizeRepoPathForMatch(m[2]); + return `${host}/${path}`.toLowerCase(); + } + return input.toLowerCase(); + } +} diff --git a/src/tailwind.css b/src/tailwind.css new file mode 100644 index 000000000..6016e6ff9 --- /dev/null +++ b/src/tailwind.css @@ -0,0 +1,207 @@ +/** + * Tailwind without Preflight — keeps legacy dashboard CSS predictable. + * @see https://tailwindcss.com/docs/preflight + */ +@layer theme, base, components, utilities; + +@import 'tailwindcss/theme.css' layer(theme); +@import 'tailwindcss/utilities.css' layer(utilities); + +/* Primer portals mount here with inner z-index:1; dashboard header is z-[100], so menus would hide behind it. */ +#__primerPortalRoot__ { + z-index: 200; +} + +/* + * Primer Dialog header close: invisible IconButton adds hover/active wash; keep the X visually static. + */ +[role='dialog'] button[data-component='IconButton'][aria-label='Close']:is(:hover, :active) { + background-color: transparent !important; + border-color: transparent !important; + box-shadow: none !important; +} + +[role='dialog'] + button[data-component='IconButton'][aria-label='Close']:is(:hover, :active) + [data-component='leadingVisual'] { + color: var(--button-invisible-iconColor-rest, #59636e) !important; +} + +/* + * GitHub-style global header: Primer semantic header tokens + scoped fg/border/muted vars so + * UnderlineNav (which reads --fgColor-default / --borderColor-muted) renders correctly on dark. + */ +.dashboard-app-header { + /* Near-black masthead (GitHub.com-style); keep Primer header fg tokens from the theme */ + /* Slightly shorter Primer underline row (default ~48px) → less empty band under the orange tab line */ + --control-xlarge-size: 2rem; + --header-bgColor: #010409; + --header-borderColor-divider: rgba(240, 246, 252, 0.15); + /* Warm accent on dark chrome (avoids default blue --fgColor-accent on tabs/focus) */ + --header-accent: #f78166; + background-color: var(--header-bgColor); + border-bottom: 1px solid var(--header-borderColor-divider); + color: var(--header-fgColor-default); + --fgColor-default: var(--header-fgColor-default); + --fgColor-muted: rgba(255, 255, 255, 0.55); + --fgColor-accent: var(--header-accent); + --underlineNav-borderColor-active: var(--header-accent); + --borderColor-muted: rgba(255, 255, 255, 0.22); + --bgColor-neutral-muted: rgba(255, 255, 255, 0.12); + /* Space below tabs/actions before border */ + padding-bottom: 0.3125rem; +} + +@media (min-width: 768px) { + .dashboard-app-header { + padding-bottom: 0.375rem; + } +} + +/* Primer UnderlineNav item padding (avoid fragile Tailwind arbitrary variants on UnderlineNav) */ +.dashboard-app-header nav[aria-label='Main navigation'] [class*='UnderlineItem-'] { + padding-top: 0.125rem; + padding-bottom: 0; +} + +/* + * material-dashboard-react.css sets all anchors to GitHub blue (#0969da) on rest/hover — undo that + * inside the dark masthead so UnderlineNav and logo link stay on-header colors. + */ +.dashboard-app-header a { + color: var(--fgColor-default); + font-weight: inherit; + text-decoration: none; +} + +.dashboard-app-header a:hover, +.dashboard-app-header a:focus { + color: var(--header-fgColor-logo); + text-decoration: none; +} + +/* + * Settings: Primer IconButton + NavLink (Navbar). Masthead is custom dark chrome while Primer theme is "day"; invisible-button hover tokens can be too subtle, and Tailwind hover:* is gated behind @media (hover: hover). Force the same wash as the account IconButton with a plain :hover rule. + */ +.dashboard-app-header a[data-header-settings]:hover { + background-color: rgba(255, 255, 255, 0.1) !important; + border-color: transparent !important; +} + +.dashboard-app-header a[data-header-settings][aria-current='page'] { + background-color: rgba(255, 255, 255, 0.15) !important; + color: var(--header-fgColor-logo); +} + +.dashboard-app-header a[data-header-settings][aria-current='page']:hover { + background-color: rgba(255, 255, 255, 0.2) !important; + color: var(--header-fgColor-logo); +} + +/* + * Headless UI Navbar — not wrapped in .dashboard-app-header. + * material-dashboard-react.css `a { color: #0969da }` still applies unless we scope a reset here. + */ +/* github.com HeaderMenu-link: ~14px medium, f0f6fc @ 82% rest → full white hover */ +.gitproxy-navbar a:not([data-header-settings]) { + color: rgb(240 246 252 / 0.82) !important; + font-weight: inherit; + text-decoration: none !important; +} + +.gitproxy-navbar a:not([data-header-settings]):hover, +.gitproxy-navbar a:not([data-header-settings]):focus-visible { + color: rgb(255 255 255) !important; + text-decoration: none !important; +} + +.gitproxy-navbar a.gitproxy-navbar-link--active:not([data-header-settings]) { + color: rgb(255 255 255) !important; +} + +.gitproxy-navbar a[data-header-settings] { + color: rgb(240 246 252 / 0.82) !important; +} + +.gitproxy-navbar a[data-header-settings]:hover, +.gitproxy-navbar a[data-header-settings]:focus-visible { + color: rgb(255 255 255) !important; +} + +/* + * Primer UnderlineNav / UnderlinePanels: github.com-style tab strip (repo details, user profile). + * material-dashboard-react.css sets body { font-weight: 300 }; tab controls use font:inherit → too light. + */ +.gitproxy-primer-underline-tabs { + font-family: var(--fontStack-sansSerif, ui-sans-serif, system-ui, sans-serif); +} + +.gitproxy-primer-underline-tabs [class*='UnderlineItem-'] { + font-family: inherit !important; + font-size: 0.875rem !important; + line-height: 1.4285 !important; + font-weight: var(--base-text-weight-normal, 400) !important; +} + +.gitproxy-primer-underline-tabs + [class*='UnderlineItem-'][aria-current]:not([aria-current='false']) + [data-component='text'], +.gitproxy-primer-underline-tabs + [class*='UnderlineItem-'][aria-selected='true'] + [data-component='text'] { + font-weight: var(--base-text-weight-semibold, 600) !important; +} + +/* + * Unselected: blend toward default for readability (pure muted too faint). + * Selected: full default + semibold on label. + */ +.gitproxy-primer-underline-tabs + [class*='UnderlineItem-']:not([aria-current]):not([aria-selected='true']) { + color: color-mix(in srgb, var(--fgColor-default) 82%, var(--fgColor-muted)) !important; +} + +.gitproxy-primer-underline-tabs [class*='UnderlineItem-'][aria-current]:not([aria-current='false']), +.gitproxy-primer-underline-tabs [class*='UnderlineItem-'][aria-selected='true'] { + color: var(--fgColor-default) !important; +} + +.gitproxy-primer-underline-tabs + [class*='UnderlineItem-']:not([aria-current]):not([aria-selected='true']) + [data-component='icon'] { + color: color-mix(in srgb, var(--fgColor-default) 72%, var(--fgColor-muted)) !important; +} + +.gitproxy-primer-underline-tabs + [class*='UnderlineItem-'][aria-current]:not([aria-current='false']) + [data-component='icon'], +.gitproxy-primer-underline-tabs + [class*='UnderlineItem-'][aria-selected='true'] + [data-component='icon'] { + color: var(--fgColor-default) !important; +} + +/* Footer GitHub (etc.) icon links — material-dashboard `a` uses GitHub blue */ +.gitproxy-footer-icon-link { + color: #0d1117 !important; + text-decoration: none !important; +} + +.gitproxy-footer-icon-link:hover, +.gitproxy-footer-icon-link:focus-visible { + color: #24292f !important; + text-decoration: none !important; +} + +/* Octicons: set fill on svg and path so nothing inherits link blue (#0969da) */ +.gitproxy-footer-icon-link svg, +.gitproxy-footer-icon-link svg path { + fill: #0d1117 !important; +} + +.gitproxy-footer-icon-link:hover svg, +.gitproxy-footer-icon-link:hover svg path, +.gitproxy-footer-icon-link:focus-visible svg, +.gitproxy-footer-icon-link:focus-visible svg path { + fill: #24292f !important; +} diff --git a/src/ui/auth/AuthProvider.tsx b/src/ui/auth/AuthProvider.tsx index 3d280593b..d5299447e 100644 --- a/src/ui/auth/AuthProvider.tsx +++ b/src/ui/auth/AuthProvider.tsx @@ -19,7 +19,7 @@ import { getUserInfo } from '../services/auth'; import { PublicUser } from '../../db/types'; import { AuthContext } from '../context'; -export const AuthProvider: React.FC> = ({ children }) => { +export const AuthProvider = ({ children }: React.PropsWithChildren) => { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); diff --git a/src/ui/context.ts b/src/ui/context.ts index 90a7242fc..19a69fb5d 100644 --- a/src/ui/context.ts +++ b/src/ui/context.ts @@ -14,21 +14,19 @@ * limitations under the License. */ -import { createContext } from 'react'; +import { createContext, type Dispatch, type SetStateAction } from 'react'; import { PublicUser } from '../db/types'; -export const UserContext = createContext({ - user: { - admin: false, - }, -}); - export interface UserContextType { - user: { - admin: boolean; - }; + user: PublicUser | null; + setUser: Dispatch>; } +export const UserContext = createContext({ + user: null, + setUser: () => {}, +}); + export interface AuthContextType { user: PublicUser | null; setUser: React.Dispatch>; diff --git a/src/ui/types.ts b/src/ui/types.ts index 86425a512..8d125221a 100644 --- a/src/ui/types.ts +++ b/src/ui/types.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import { CSSProperties } from '@material-ui/core/styles/withStyles'; - import { Action } from '../proxy/actions'; import { Step } from '../proxy/actions/Step'; import { Repo } from '../db/types'; @@ -33,6 +31,24 @@ type ActionMethods = | 'setAutoRejection' | 'continue'; +export interface CancellationData { + reviewer: { + username: string; + displayName?: string | null; + }; + reason?: string; + timestamp: string | Date; +} + +export interface RejectionData { + reviewer: { + username: string; + displayName?: string | null; + }; + reason: string; + timestamp: string | Date; +} + export interface BackendResponse { message: string; } @@ -61,52 +77,9 @@ export interface Route { name: string; rtlName?: string; component: React.ComponentType; - icon?: string | React.ComponentType; visible?: boolean; } -export interface GitHubRepositoryMetadata { - description?: string; - language?: string; - license?: { - spdx_id: string; - }; - html_url: string; - parent?: { - full_name: string; - html_url: string; - }; - created_at?: string; - updated_at?: string; - pushed_at?: string; - owner?: { - avatar_url: string; - html_url: string; - }; -} - -export interface GitLabRepositoryMetadata { - description?: string; - primary_language?: string; - license?: { - nickname: string; - }; - web_url: string; - forked_from_project?: { - full_name: string; - web_url: string; - }; - last_activity_at?: string; - avatar_url?: string; - namespace?: { - name: string; - path: string; - full_path: string; - avatar_url?: string; - web_url: string; - }; -} - export interface SCMRepositoryMetadata { description?: string; language?: string; @@ -114,13 +87,8 @@ export interface SCMRepositoryMetadata { htmlUrl?: string; parentName?: string; parentUrl?: string; - lastUpdated?: string; - created_at?: string; - updated_at?: string; - pushed_at?: string; - profileUrl?: string; avatarUrl?: string; } -export type CSSProperty = React.CSSProperties | CSSProperties; +export type CSSProperty = React.CSSProperties; diff --git a/src/ui/utils.tsx b/src/ui/utils.tsx index f8746a8f0..1719f1f62 100644 --- a/src/ui/utils.tsx +++ b/src/ui/utils.tsx @@ -14,12 +14,7 @@ * limitations under the License. */ -import axios from 'axios'; import React from 'react'; -import { GitHubRepositoryMetadata, GitLabRepositoryMetadata, SCMRepositoryMetadata } from './types'; -import { CommitData } from '../proxy/processors/types'; -import moment from 'moment'; -import { getErrorMessage } from '../utils/errors'; /** * Retrieve a decoded cookie value from `document.cookie` with given `name`. @@ -38,213 +33,3 @@ export const getCookie = (name: string): string | null => { return decodeURIComponent(cookies[0].split('=')[1]); }; - -/** - * Retrieve a string indicating whether a repository URL is hosted - * by a known SCM provider (github or gitlab). - * @param {string} url The repository URL. - * @return {string} A string representing the SCM provider or 'unknown'. - */ -export const getGitProvider = (url: string) => { - const hostname = new URL(url).hostname.toLowerCase(); - if (hostname === 'github.com') return 'github'; - if (hostname.includes('gitlab')) return 'gitlab'; - return 'unknown'; -}; - -/** - * Renders a block of mailto: links for author user names and email addresses found in an array of commit data. - * - * @param {CommitData[]} commitData The user.name to render in the link. - * @return {JSX.Element} A JSX Element representing the rendered links - */ -export const generateAuthorLinks = (commitData: CommitData[]) => { - const orderedAuthors: JSX.Element[] = []; - const uniqueAuthors: Set = new Set(); - commitData.forEach((row) => { - if (!uniqueAuthors.has(row.authorEmail)) { - uniqueAuthors.add(row.authorEmail); - orderedAuthors.push( - , - ); - } - }); - return
{orderedAuthors}
; -}; - -/** - * Renders a mailto: link for user name and email address, for use in rendering details of pushes. - * - * @param {string} name The user.name to render in the link. - * @param {string} email The email address to render in the link. - * @return {JSX.Element} An tag based on the username and email address. - */ -export const generateEmailLink = (name: string, email: string) => { - return email ? ( - - "{name}" <{email}> - - ) : ( - No data... - ); -}; - -/** - * Predicts a user's profile URL based on their username and the SCM provider's details. - * TODO: update this to attempt to resolve a user email to a profile URL - * - * @param {string} username The username. - * @param {string} provider The name of the SCM provider. - * @param {string} hostname The hostname of the SCM provider. - * @return {string | null} The predicted profile URL or null - */ -export const getUserProfileUrl = (username: string, provider: string, hostname: string) => { - if (provider == 'github') { - return `https://github.com/${username}`; - } else if (provider == 'gitlab') { - return `https://${hostname}/${username}`; - } else { - return null; - } -}; - -/** - * Attempts to construct a link to the user's profile at an SCM provider. - * - * TODO: update this to attempt to resolve a user email to a profile URL - * - * @param {string} username The username. - * @param {string} provider The name of the SCM provider. - * @param {string} hostname The hostname of the SCM provider. - * @return {string} A string containing an HTML A tag pointing to the user's profile, if possible, degrading to just the username or 'N/A' when not (e.g. because the SCM provider is unknown). - */ -export const getUserProfileLink = (username: string, provider: string, hostname: string) => { - if (username) { - let profileData = ''; - const profileUrl = getUserProfileUrl(username, provider, hostname); - if (profileUrl) { - profileData = `${username}`; - } else { - profileData = `${username}`; - } - return profileData; - } else { - return 'N/A'; - } -}; - -/** - * Predicts an organisation's profile URL at an SCM provider. - * @param {string} project The organisation name. - * @param {string} provider The name of the SCM provider. - * @param {string} hostname The hostname of the SCM provider. - * @return {string} The predicted profile URL or null. - */ -export const getOrganisationProfileUrl = (project: string, provider: string, hostname: string) => { - if (provider == 'github') { - return `https://github.com/${project}`; - } else if (provider == 'gitlab') { - return `https://${hostname}/${project}`; - } else { - return null; - } -}; - -/** - * Predicts an organisation's profile image URL at an SCM provider. - * @param {string} project The organisation name. - * @param {string} provider The name of the SCM provider. - * @param {string} hostname The hostname of the SCM provider. - * @return {string} The predicted profile URL or null. - */ -export const getOrganisationProfileImageUrl = ( - project: string, - provider: string, - hostname: string, -) => { - if (provider == 'github') { - return `https://github.com/${project}.png`; - } else if (provider == 'gitlab') { - return `https://${hostname}/${project}.png`; - } else { - return null; - } -}; - -/** - * Retrieves data about repositories hosted at known SCM providers. - * @param {string} project The organisations's name. - * @param {string} name The repository name. - * @param {string} url The URL of the repository (used to detect the SCM provider) - * @return {Promise} Data retrieved from teh SCM provider or null - */ -export const fetchRemoteRepositoryData = async ( - project: string, - name: string, - url: string, -): Promise => { - const provider = getGitProvider(url); - const hostname = new URL(url).hostname; - - if (provider === 'github') { - const response = await axios.get( - `https://api.github.com/repos/${project}/${name}`, - ); - - return { - description: response.data.description, - language: response.data.language, - license: response.data.license?.spdx_id, - lastUpdated: moment - .max([ - moment(response.data.created_at), - moment(response.data.updated_at), - moment(response.data.pushed_at), - ]) - .fromNow(), - htmlUrl: response.data.html_url, - parentName: response.data.parent?.full_name, - parentUrl: response.data.parent?.html_url, - - avatarUrl: response.data.owner?.avatar_url, - profileUrl: response.data.owner?.html_url, - }; - } else if (provider == 'gitlab') { - const projectPath = encodeURIComponent(`${project}/${name}`); - const apiUrl = `https://${hostname}/api/v4/projects/${projectPath}`; - const response = await axios.get(apiUrl); - - // Make follow-up call to get languages - let primaryLanguage; - try { - const languagesResponse = await axios.get( - `https://${hostname}/api/v4/projects/${projectPath}/languages`, - ); - const languages = languagesResponse.data; - // Get the first key (primary language) from the ordered hash - primaryLanguage = Object.keys(languages)[0]; - } catch (error: unknown) { - const msg = getErrorMessage(error); - console.warn('Could not fetch language data:', msg); - } - - return { - description: response.data.description, - language: primaryLanguage, - license: response.data.license?.nickname, - lastUpdated: moment(response.data.last_activity_at).fromNow(), - htmlUrl: response.data.web_url, - parentName: response.data.forked_from_project?.full_name, - parentUrl: response.data.forked_from_project?.web_url, - avatarUrl: response.data.avatar_url, - profileUrl: response.data.namespace?.web_url, - }; - } else { - // For other/unknown providers, don't make API calls - return null; - } -}; diff --git a/src/ui/utils/parseGitRemoteUrl.ts b/src/ui/utils/parseGitRemoteUrl.ts new file mode 100644 index 000000000..a093cfdcd --- /dev/null +++ b/src/ui/utils/parseGitRemoteUrl.ts @@ -0,0 +1,86 @@ +/** + * Copyright 2026 GitProxy Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { canonicalRemoteUrl } from '../../activity/canonicalRemoteUrl'; + +export type ParsedGitRemotePath = { + /** Path segments before the repo (org or org/group/...). */ + project: string; + /** Repository name without trailing .git */ + name: string; +}; + +function segmentsFromPath(path: string): string[] | null { + const trimmed = path.replace(/^\/+|\/+$/g, ''); + if (!trimmed) return null; + const parts = trimmed.split('/').filter(Boolean); + if (parts.length < 2) return null; + const repoSegment = parts[parts.length - 1].replace(/\.git$/i, ''); + if (!repoSegment) return null; + const orgSegments = parts.slice(0, -1); + const project = orgSegments.join('/'); + if (!project) return null; + return [project, repoSegment]; +} + +/** + * Best-effort parse of a git remote URL/SCP string into project (organization path) and repo name. + * Does not require `.git` in the last segment (unlike server validation on submit). + */ +export function parseGitRemoteUrl(raw: string): ParsedGitRemotePath | null { + const input = raw.trim(); + if (!input) return null; + + let pathPart: string | null = null; + + try { + const u = new URL(input); + if (u.protocol !== 'http:' && u.protocol !== 'https:') return null; + pathPart = u.pathname; + } catch { + const sshMatch = /^git@[^:]+:(.+)$/i.exec(input); + if (sshMatch) { + pathPart = '/' + sshMatch[1].replace(/^\/+/, ''); + } else { + return null; + } + } + + const result = segmentsFromPath(pathPart); + if (!result) return null; + const [project, name] = result; + return { project, name }; +} + +function isGitHubHostname(hostname: string): boolean { + const h = hostname.toLowerCase(); + return h === 'github.com' || h === 'www.github.com'; +} + +/** True when the remote URL points at github.com (https or git@github.com). */ +export function isGitHubGitRemoteUrl(raw: string): boolean { + const input = raw.trim(); + if (!input) return false; + try { + const u = new URL(input); + if (u.protocol !== 'http:' && u.protocol !== 'https:') return false; + return isGitHubHostname(u.hostname); + } catch { + const sshMatch = /^git@([^:]+):/i.exec(input); + if (!sshMatch) return false; + return isGitHubHostname(sshMatch[1]); + } +} From 141af766f087acffb70d252083c79476266e1790 Mon Sep 17 00:00:00 2001 From: Andy Pols Date: Fri, 17 Apr 2026 13:06:56 +0000 Subject: [PATCH 04/25] chore: update API service layer --- src/ui/services/errors.ts | 4 +- src/ui/services/git-push.ts | 82 +++++++++++++++--- src/ui/services/repo.ts | 138 ++++++++++++++++++++++++++++-- src/ui/services/runtime-config.ts | 31 ++++--- src/ui/services/user.ts | 95 ++++++++++++++++++-- 5 files changed, 307 insertions(+), 43 deletions(-) diff --git a/src/ui/services/errors.ts b/src/ui/services/errors.ts index d393be980..55f4b851a 100644 --- a/src/ui/services/errors.ts +++ b/src/ui/services/errors.ts @@ -26,7 +26,9 @@ export const getServiceError = ( fallbackMessage: string, ): { status?: number; message: string } => { const status = error?.response?.status; - const responseMessage = error?.response?.data?.message; + const responseMessage = + error?.response?.data?.message ?? + (typeof error?.response?.data?.error === 'string' ? error.response.data.error : undefined); const message = typeof responseMessage === 'string' && responseMessage.trim().length > 0 ? responseMessage diff --git a/src/ui/services/git-push.ts b/src/ui/services/git-push.ts index 40baf7e25..f2522ccf2 100644 --- a/src/ui/services/git-push.ts +++ b/src/ui/services/git-push.ts @@ -38,21 +38,53 @@ const getPush = async (id: string): Promise> => { } }; -const getPushes = async ( - query = { - blocked: true, - canceled: false, - authorised: false, - rejected: false, - }, -): Promise> => { +export type GetPushesQuery = { + blocked?: boolean; + canceled?: boolean; + authorised?: boolean; + rejected?: boolean; + error?: boolean; + url?: string; +}; + +const getPushesDefaultFilters: Record = { + blocked: true, + canceled: false, + authorised: false, + rejected: false, +}; + +const getUserActivity = async (username: string): Promise> => { + const apiV1Base = await getApiV1BaseUrl(); + const path = `${apiV1Base}/user/${encodeURIComponent(username)}/activity`; + try { + const response = await axios(path, getAxiosConfig()); + return successResult(response.data as unknown as PushActionView[]); + } catch (error: unknown) { + return errorResult(error, 'Failed to load user activity'); + } +}; + +const getPushes = async (query?: GetPushesQuery): Promise> => { const apiV1Base = await getApiV1BaseUrl(); const url = new URL(`${apiV1Base}/push`); - const stringifiedQuery = Object.fromEntries( - Object.entries(query).map(([key, value]) => [key, value.toString()]), - ); - url.search = new URLSearchParams(stringifiedQuery).toString(); + const params = new URLSearchParams(); + if (query === undefined) { + for (const [key, value] of Object.entries(getPushesDefaultFilters)) { + params.set(key, String(value)); + } + } else { + for (const [key, value] of Object.entries(query)) { + if (value === undefined) continue; + params.set(key, typeof value === 'boolean' ? value.toString() : value); + } + } + + const qs = params.toString(); + if (qs) { + url.search = qs; + } try { const response = await axios(url.toString(), getAxiosConfig()); @@ -109,4 +141,28 @@ const cancelPush = async (id: string): Promise => { } }; -export { getPush, getPushes, authorisePush, rejectPush, cancelPush }; +const getPushPermissions = async ( + id: string, +): Promise> => { + const apiV1Base = await getApiV1BaseUrl(); + const url = `${apiV1Base}/push/${id}/permissions`; + try { + const response = await axios<{ canCancel: boolean; canApproveReject: boolean }>( + url, + getAxiosConfig(), + ); + return successResult(response.data); + } catch (error: unknown) { + return errorResult(error, 'Failed to load push permissions'); + } +}; + +export { + getPush, + getPushes, + getPushPermissions, + getUserActivity, + authorisePush, + rejectPush, + cancelPush, +}; diff --git a/src/ui/services/repo.ts b/src/ui/services/repo.ts index 909127c98..9fc91a766 100644 --- a/src/ui/services/repo.ts +++ b/src/ui/services/repo.ts @@ -18,8 +18,97 @@ import axios from 'axios'; import { getAxiosConfig } from './auth.js'; import { Repo } from '../../db/types'; import { RepoView } from '../types'; +import type { RepoSortField } from '../views/RepoList/Components/repoSortField'; import { getApiV1BaseUrl } from './apiConfig'; import { ServiceResult, getServiceError, errorResult, successResult } from './errors'; +import { SCMRepositoryMetadata } from '../types'; + +const compareRepoName = (a: RepoView, b: RepoView, direction: 'asc' | 'desc'): number => { + const cmp = a.name.localeCompare(b.name); + return direction === 'asc' ? cmp : -cmp; +}; + +const userIsContributorOrReviewer = (repo: RepoView, username: string): boolean => { + const { canPush, canAuthorise } = repo.users; + return canPush.includes(username) || canAuthorise.includes(username); +}; + +const compareRelevance = ( + a: RepoView, + b: RepoView, + currentUsername: string | null | undefined, +): number => { + const u = currentUsername?.trim(); + const aMine = u ? userIsContributorOrReviewer(a, u) : false; + const bMine = u ? userIsContributorOrReviewer(b, u) : false; + if (aMine !== bMine) { + return aMine ? -1 : 1; + } + return compareRepoName(a, b, 'asc'); +}; + +const totalActivity = (repo: RepoView): number => { + if (!repo.activity) return 0; + const { pending, approved, canceled, rejected, error } = repo.activity; + return pending + approved + canceled + rejected + error; +}; + +const compareLatestPendingReview = (a: RepoView, b: RepoView): number => { + const aMs = a.latestPendingReviewAtMs ?? Number.NEGATIVE_INFINITY; + const bMs = b.latestPendingReviewAtMs ?? Number.NEGATIVE_INFINITY; + if (bMs !== aMs) { + return bMs - aMs; + } + return compareRepoName(a, b, 'asc'); +}; + +const compareLatestPush = (a: RepoView, b: RepoView, direction: 'asc' | 'desc'): number => { + if (direction === 'desc') { + const aMs = a.latestPushAtMs ?? Number.NEGATIVE_INFINITY; + const bMs = b.latestPushAtMs ?? Number.NEGATIVE_INFINITY; + if (bMs !== aMs) { + return bMs - aMs; + } + } else { + const aMs = a.latestPushAtMs ?? Number.POSITIVE_INFINITY; + const bMs = b.latestPushAtMs ?? Number.POSITIVE_INFINITY; + if (aMs !== bMs) { + return aMs - bMs; + } + } + return compareRepoName(a, b, 'asc'); +}; + +export const sortRepoViews = ( + repos: RepoView[], + sort: RepoSortField, + currentUsername?: string | null, +): RepoView[] => { + const next = [...repos]; + switch (sort) { + case 'relevance': + next.sort((a, b) => compareRelevance(a, b, currentUsername)); + break; + case 'activity': + next.sort((a, b) => totalActivity(b) - totalActivity(a)); + break; + case 'latestPendingReview': + next.sort((a, b) => compareLatestPendingReview(a, b)); + break; + case 'lastPushed-desc': + next.sort((a, b) => compareLatestPush(a, b, 'desc')); + break; + case 'lastPushed-asc': + next.sort((a, b) => compareLatestPush(a, b, 'asc')); + break; + case 'name-desc': + next.sort((a, b) => compareRepoName(a, b, 'desc')); + break; + default: + next.sort((a, b) => compareRepoName(a, b, 'asc')); + } + return next; +}; const canAddUser = async (repoId: string, user: string, action: string) => { const apiV1Base = await getApiV1BaseUrl(); @@ -46,24 +135,29 @@ class DupUserValidationError extends Error { } } -const getRepos = async ( - query: Record = {}, -): Promise> => { +const fetchRepoViews = async (): Promise> => { const apiV1Base = await getApiV1BaseUrl(); const url = new URL(`${apiV1Base}/repo`); - url.search = new URLSearchParams(query as any).toString(); try { const response = await axios(url.toString(), getAxiosConfig()); - const sortedRepos = response.data.sort((a: RepoView, b: RepoView) => - a.name.localeCompare(b.name), - ); - return successResult(sortedRepos); + return successResult(response.data); } catch (error: unknown) { return errorResult(error, 'Failed to load repositories'); } }; +const getRepos = async ( + sort: RepoSortField, + currentUsername?: string | null, +): Promise> => { + const result = await fetchRepoViews(); + if (!result.success || !result.data) { + return result; + } + return successResult(sortRepoViews(result.data, sort, currentUsername)); +}; + const getRepo = async (id: string): Promise> => { const apiV1Base = await getApiV1BaseUrl(); const url = new URL(`${apiV1Base}/repo/${id}`); @@ -76,6 +170,23 @@ const getRepo = async (id: string): Promise> => { } }; +const getRepoScmMetadata = async ( + id: string, +): Promise> => { + const apiV1Base = await getApiV1BaseUrl(); + const url = new URL(`${apiV1Base}/repo/${id}/scm-metadata`); + + try { + const response = await axios.get( + url.toString(), + getAxiosConfig(), + ); + return successResult(response.data ?? null); + } catch (error: unknown) { + return errorResult(error, 'Failed to load SCM metadata'); + } +}; + const addRepo = async (repo: RepoView): Promise> => { const apiV1Base = await getApiV1BaseUrl(); const url = new URL(`${apiV1Base}/repo`); @@ -134,4 +245,13 @@ const deleteRepo = async (repoId: string): Promise => { } }; -export { addUser, deleteUser, getRepos, getRepo, addRepo, deleteRepo }; +export { + addUser, + deleteUser, + getRepos, + fetchRepoViews, + getRepo, + getRepoScmMetadata, + addRepo, + deleteRepo, +}; diff --git a/src/ui/services/runtime-config.ts b/src/ui/services/runtime-config.ts index 77e204376..2c5dccc24 100644 --- a/src/ui/services/runtime-config.ts +++ b/src/ui/services/runtime-config.ts @@ -38,9 +38,13 @@ export const getRuntimeConfig = async (): Promise => { try { const response = await fetch('/runtime-config.json'); - if (response.ok) { + const contentType = response.headers.get('content-type') || ''; + if (response.ok && contentType.includes('application/json')) { runtimeConfig = await response.json(); console.log('Loaded runtime config:', runtimeConfig); + } else if (response.ok) { + console.warn('Runtime config is not JSON, using defaults'); + runtimeConfig = {}; } else { console.warn('Runtime config not found, using defaults'); runtimeConfig = {}; @@ -62,33 +66,32 @@ export const getApiBaseUrl = async (): Promise => { // Priority order: // 1. Runtime config apiUrl (set at deployment) - // 2. Build-time environment variable - // 3. Auto-detect from current location with smart defaults + // 2. Vite dev on localhost:3000 → same origin + `/api` proxy (avoids CORS when the API runs with default ALLOWED_ORIGINS) + // 3. Build-time VITE_API_URI (e.g. UI on another host/port talking to a known API) + // 4. Browser: same origin; Node/tests: localhost:8080 if (config.apiUrl) { return config.apiUrl; } + if (typeof location !== 'undefined') { + const currentHost = location.hostname; + if (currentHost === 'localhost' && location.port === '3000') { + // Vite dev server: same-origin; vite.config proxies /api → backend (see VITE_DEV_API_PROXY) + console.log('Development mode detected: using Vite dev origin for API (see server.proxy)'); + return location.origin; + } + } + // @ts-expect-error - import.meta.env is available in Vite but not in CommonJS tsconfig if (import.meta.env?.VITE_API_URI) { // @ts-expect-error - Vite env variable return import.meta.env.VITE_API_URI as string; } - // Check if running in browser environment (not Node.js tests) if (typeof location !== 'undefined') { - // Smart defaults based on current location - const currentHost = location.hostname; - if (currentHost === 'localhost' && location.port === '3000') { - // Development mode: Vite dev server, API on port 8080 - console.log('Development mode detected: using localhost:8080 for API'); - return 'http://localhost:8080'; - } - - // Production mode or other scenarios: API on same origin return location.origin; } - // Fallback for Node.js/test environment return 'http://localhost:8080'; }; diff --git a/src/ui/services/user.ts b/src/ui/services/user.ts index d55ad388c..2db95413f 100644 --- a/src/ui/services/user.ts +++ b/src/ui/services/user.ts @@ -19,13 +19,41 @@ import { getAxiosConfig, processAuthError } from './auth'; import { PublicUser } from '../../db/types'; import { BackendResponse } from '../types'; import { getBaseUrl, getApiV1BaseUrl } from './apiConfig'; -import { getServiceError, formatErrorMessage } from './errors'; +import { errorResult, formatErrorMessage, getServiceError, successResult } from './errors'; +import type { ServiceResult } from './errors'; +import type { UserSortField } from '../views/UserList/Components/userSortField'; +import { DEFAULT_USER_SORT, userSortDirection } from '../views/UserList/Components/userSortField'; type SetStateCallback = (value: T | ((prevValue: T) => T)) => void; +const userNameSortKey = (user: PublicUser): string => + (user.displayName?.trim() || user.username || '').toLowerCase(); + +const totalUserActivity = (user: PublicUser): number => { + if (!user.activity) return 0; + const { pending, approved, canceled, rejected, error } = user.activity; + return pending + approved + canceled + rejected + error; +}; + +const sortUsers = (users: PublicUser[], sort: UserSortField): PublicUser[] => { + const next = [...users]; + if (sort === 'activity') { + next.sort((a, b) => totalUserActivity(b) - totalUserActivity(a)); + return next; + } + const direction = userSortDirection(sort); + next.sort((a, b) => { + const cmp = userNameSortKey(a).localeCompare(userNameSortKey(b), undefined, { + sensitivity: 'base', + }); + return direction === 'asc' ? cmp : -cmp; + }); + return next; +}; + const getUser = async ( setIsLoading?: SetStateCallback, - setUser?: (user: PublicUser) => void, + setUser?: (user: PublicUser | null) => void, setAuth?: SetStateCallback, setErrorMessage?: SetStateCallback, id: string | null = null, @@ -47,6 +75,7 @@ const getUser = async ( } catch (error: unknown) { const { status, message } = getServiceError(error, 'Unknown error'); if (status === 401) { + setUser?.(null); setAuth?.(false); setErrorMessage?.(processAuthError(error as AxiosError)); } else { @@ -61,6 +90,7 @@ const getUsers = async ( setUsers: SetStateCallback, setAuth: SetStateCallback, setErrorMessage: SetStateCallback, + sort: UserSortField = DEFAULT_USER_SORT, ): Promise => { setIsLoading(true); @@ -70,7 +100,7 @@ const getUsers = async ( `${apiV1BaseUrl}/user`, getAxiosConfig(), ); - setUsers(response.data); + setUsers(sortUsers(response.data, sort)); } catch (error) { const { status, message } = getServiceError(error, 'Unknown error'); if (status === 401) { @@ -84,10 +114,55 @@ const getUsers = async ( } }; +const fetchUsersForAutocomplete = async (): Promise => { + const apiV1BaseUrl = await getApiV1BaseUrl(); + const response: AxiosResponse = await axios( + `${apiV1BaseUrl}/user`, + getAxiosConfig(), + ); + return sortUsers(response.data, DEFAULT_USER_SORT); +}; + +const resolveUsernameByEmail = async (email: string): Promise> => { + try { + const apiV1BaseUrl = await getApiV1BaseUrl(); + const url = new URL(`${apiV1BaseUrl}/user/lookup/by-email`); + url.searchParams.set('email', email.trim().toLowerCase()); + const response: AxiosResponse<{ username: string | null }> = await axios( + url.toString(), + getAxiosConfig(), + ); + return successResult(response.data.username); + } catch (error) { + return errorResult(error, 'Error resolving user by email'); + } +}; + +/** + * Returns the user's display name from the directory, or null if unavailable. + */ +const resolveDisplayNameByUsername = async (username: string): Promise => { + const key = username.trim().toLowerCase(); + if (!key) { + return null; + } + + try { + const apiV1BaseUrl = await getApiV1BaseUrl(); + const response: AxiosResponse = await axios( + `${apiV1BaseUrl}/user/${encodeURIComponent(key)}`, + getAxiosConfig(), + ); + const dn = response.data.displayName?.trim(); + return dn || null; + } catch { + return null; + } +}; + const updateUser = async ( user: PublicUser, setErrorMessage: SetStateCallback, - setIsLoading: SetStateCallback, ): Promise => { try { const baseUrl = await getBaseUrl(); @@ -95,8 +170,16 @@ const updateUser = async ( } catch (error: unknown) { const { status, message } = getServiceError(error, 'Unknown error'); setErrorMessage(formatErrorMessage('Error updating user', status, message)); - setIsLoading(false); + throw error; } }; -export { getUser, getUsers, updateUser }; +export { + getUser, + getUsers, + fetchUsersForAutocomplete, + sortUsers, + resolveUsernameByEmail, + resolveDisplayNameByUsername, + updateUser, +}; From be88117d20cfe18e63e95e78b8153e07c5208ec5 Mon Sep 17 00:00:00 2001 From: Andy Pols Date: Fri, 17 Apr 2026 13:08:51 +0000 Subject: [PATCH 05/25] chore: add TanStack React-Query data layer --- src/ui/query/pushQueryKeys.ts | 22 +++++++++++ src/ui/query/queryClient.ts | 30 +++++++++++++++ src/ui/query/repoQueryKeys.ts | 26 +++++++++++++ src/ui/query/useDisplayNameQuery.ts | 31 +++++++++++++++ src/ui/query/usePushPermissionsQuery.ts | 38 ++++++++++++++++++ src/ui/query/usePushQuery.ts | 40 +++++++++++++++++++ src/ui/query/usePushesQuery.ts | 41 ++++++++++++++++++++ src/ui/query/useRepoQuery.ts | 40 +++++++++++++++++++ src/ui/query/useRepoScmMetadataQuery.ts | 42 ++++++++++++++++++++ src/ui/query/useRepoViewsListQuery.ts | 42 ++++++++++++++++++++ src/ui/query/useUserActivityQuery.ts | 34 +++++++++++++++++ src/ui/query/useUserQuery.ts | 51 +++++++++++++++++++++++++ src/ui/query/useUsernameByEmailQuery.ts | 34 +++++++++++++++++ src/ui/query/useUsersListQuery.ts | 31 +++++++++++++++ src/ui/query/userQueryKeys.ts | 24 ++++++++++++ 15 files changed, 526 insertions(+) create mode 100644 src/ui/query/pushQueryKeys.ts create mode 100644 src/ui/query/queryClient.ts create mode 100644 src/ui/query/repoQueryKeys.ts create mode 100644 src/ui/query/useDisplayNameQuery.ts create mode 100644 src/ui/query/usePushPermissionsQuery.ts create mode 100644 src/ui/query/usePushQuery.ts create mode 100644 src/ui/query/usePushesQuery.ts create mode 100644 src/ui/query/useRepoQuery.ts create mode 100644 src/ui/query/useRepoScmMetadataQuery.ts create mode 100644 src/ui/query/useRepoViewsListQuery.ts create mode 100644 src/ui/query/useUserActivityQuery.ts create mode 100644 src/ui/query/useUserQuery.ts create mode 100644 src/ui/query/useUsernameByEmailQuery.ts create mode 100644 src/ui/query/useUsersListQuery.ts create mode 100644 src/ui/query/userQueryKeys.ts diff --git a/src/ui/query/pushQueryKeys.ts b/src/ui/query/pushQueryKeys.ts new file mode 100644 index 000000000..04bf86cc9 --- /dev/null +++ b/src/ui/query/pushQueryKeys.ts @@ -0,0 +1,22 @@ +/** + * Copyright 2026 GitProxy Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const pushQueryKeys = { + all: ['pushes'] as const, + list: (filters: object) => [...pushQueryKeys.all, 'list', filters] as const, + detail: (id: string) => [...pushQueryKeys.all, id] as const, + permissions: (id: string) => [...pushQueryKeys.all, id, 'permissions'] as const, +}; diff --git a/src/ui/query/queryClient.ts b/src/ui/query/queryClient.ts new file mode 100644 index 000000000..6ce9ccb6c --- /dev/null +++ b/src/ui/query/queryClient.ts @@ -0,0 +1,30 @@ +/** + * Copyright 2026 GitProxy Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { QueryClient } from '@tanstack/react-query'; + +export function createAppQueryClient(): QueryClient { + return new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60_000, + gcTime: 30 * 60_000, + retry: 1, + refetchOnWindowFocus: false, + }, + }, + }); +} diff --git a/src/ui/query/repoQueryKeys.ts b/src/ui/query/repoQueryKeys.ts new file mode 100644 index 000000000..f85971ee8 --- /dev/null +++ b/src/ui/query/repoQueryKeys.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2026 GitProxy Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const repoQueryKeys = { + all: ['repos'] as const, + list: (): readonly ['repos', 'list'] => [...repoQueryKeys.all, 'list'], + detail: (id: string): readonly ['repos', string] => [...repoQueryKeys.all, id], + scmMetadata: (id: string): readonly ['repos', string, 'scm-metadata'] => [ + ...repoQueryKeys.all, + id, + 'scm-metadata', + ], +}; diff --git a/src/ui/query/useDisplayNameQuery.ts b/src/ui/query/useDisplayNameQuery.ts new file mode 100644 index 000000000..66c4e92f1 --- /dev/null +++ b/src/ui/query/useDisplayNameQuery.ts @@ -0,0 +1,31 @@ +/** + * Copyright 2026 GitProxy Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useQuery } from '@tanstack/react-query'; +import { resolveDisplayNameByUsername } from '../services/user'; +import { userQueryKeys } from './userQueryKeys'; + +/** Display names change rarely; cache indefinitely within a session. */ +const DISPLAY_NAME_STALE_MS = Infinity; + +export function useDisplayNameQuery(username: string | undefined) { + return useQuery({ + queryKey: userQueryKeys.displayName(username ?? ''), + queryFn: () => resolveDisplayNameByUsername(username!), + enabled: Boolean(username), + staleTime: DISPLAY_NAME_STALE_MS, + }); +} diff --git a/src/ui/query/usePushPermissionsQuery.ts b/src/ui/query/usePushPermissionsQuery.ts new file mode 100644 index 000000000..69f40f488 --- /dev/null +++ b/src/ui/query/usePushPermissionsQuery.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2026 GitProxy Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useQuery } from '@tanstack/react-query'; +import { getPushPermissions } from '../services/git-push'; +import { pushQueryKeys } from './pushQueryKeys'; + +type PushPermissions = { canCancel: boolean; canApproveReject: boolean }; + +const DEFAULT_PERMISSIONS: PushPermissions = { canCancel: true, canApproveReject: true }; + +export function usePushPermissionsQuery(id: string | undefined) { + return useQuery({ + queryKey: pushQueryKeys.permissions(id ?? ''), + queryFn: async () => { + const result = await getPushPermissions(id!); + if (result.success && result.data) { + return result.data; + } + return DEFAULT_PERMISSIONS; + }, + enabled: Boolean(id), + initialData: DEFAULT_PERMISSIONS, + }); +} diff --git a/src/ui/query/usePushQuery.ts b/src/ui/query/usePushQuery.ts new file mode 100644 index 000000000..311fd1f95 --- /dev/null +++ b/src/ui/query/usePushQuery.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2026 GitProxy Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useQuery } from '@tanstack/react-query'; +import { useNavigate } from 'react-router'; +import { getPush } from '../services/git-push'; +import { pushQueryKeys } from './pushQueryKeys'; +import { PushActionView } from '../types'; + +export function usePushQuery(id: string | undefined) { + const navigate = useNavigate(); + + return useQuery({ + queryKey: pushQueryKeys.detail(id ?? ''), + queryFn: async () => { + const result = await getPush(id!); + if (result.success && result.data) { + return result.data; + } + if (result.status === 401) { + navigate('/login', { replace: true }); + } + throw new Error(result.message || 'Failed to load push'); + }, + enabled: Boolean(id), + }); +} diff --git a/src/ui/query/usePushesQuery.ts b/src/ui/query/usePushesQuery.ts new file mode 100644 index 000000000..fd73f25d5 --- /dev/null +++ b/src/ui/query/usePushesQuery.ts @@ -0,0 +1,41 @@ +/** + * Copyright 2026 GitProxy Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useQuery } from '@tanstack/react-query'; +import { useNavigate } from 'react-router'; +import { getPushes } from '../services/git-push'; +import type { GetPushesQuery } from '../services/git-push'; +import { pushQueryKeys } from './pushQueryKeys'; +import { PushActionView } from '../types'; + +export function usePushesQuery(filters: GetPushesQuery = {}, enabled = true) { + const navigate = useNavigate(); + + return useQuery({ + queryKey: pushQueryKeys.list(filters), + queryFn: async () => { + const result = await getPushes(filters); + if (result.success && result.data) { + return result.data; + } + if (result.status === 401) { + navigate('/login', { replace: true }); + } + throw new Error(result.message || 'Failed to load pushes'); + }, + enabled, + }); +} diff --git a/src/ui/query/useRepoQuery.ts b/src/ui/query/useRepoQuery.ts new file mode 100644 index 000000000..0fbc952dc --- /dev/null +++ b/src/ui/query/useRepoQuery.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2026 GitProxy Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useQuery } from '@tanstack/react-query'; +import { useNavigate } from 'react-router'; +import { getRepo } from '../services/repo'; +import { repoQueryKeys } from './repoQueryKeys'; +import { RepoView } from '../types'; + +export function useRepoQuery(id: string | undefined) { + const navigate = useNavigate(); + + return useQuery({ + queryKey: repoQueryKeys.detail(id ?? ''), + queryFn: async () => { + const result = await getRepo(id!); + if (result.success && result.data) { + return result.data; + } + if (result.status === 401) { + navigate('/login', { replace: true }); + } + throw new Error(result.message || 'Failed to load repository'); + }, + enabled: Boolean(id), + }); +} diff --git a/src/ui/query/useRepoScmMetadataQuery.ts b/src/ui/query/useRepoScmMetadataQuery.ts new file mode 100644 index 000000000..a23d5bd91 --- /dev/null +++ b/src/ui/query/useRepoScmMetadataQuery.ts @@ -0,0 +1,42 @@ +/** + * Copyright 2026 GitProxy Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useQuery } from '@tanstack/react-query'; +import { getRepoScmMetadata } from '../services/repo'; +import { SCMRepositoryMetadata } from '../types'; +import { repoQueryKeys } from './repoQueryKeys'; + +/** Aligns loosely with server SCM metadata success TTL (see `scmMetadata.ts`). */ +const SCM_METADATA_STALE_MS = 6 * 60 * 60 * 1000; + +export function useRepoScmMetadataQuery(repoId: string | undefined) { + return useQuery({ + queryKey: repoQueryKeys.scmMetadata(repoId ?? ''), + queryFn: async (): Promise => { + if (!repoId) { + return null; + } + const result = await getRepoScmMetadata(repoId); + if (result.success) { + return result.data ?? null; + } + console.warn(`Unable to load SCM metadata for repo ${repoId}:`, result.message ?? ''); + return null; + }, + enabled: Boolean(repoId), + staleTime: SCM_METADATA_STALE_MS, + }); +} diff --git a/src/ui/query/useRepoViewsListQuery.ts b/src/ui/query/useRepoViewsListQuery.ts new file mode 100644 index 000000000..5b41a8415 --- /dev/null +++ b/src/ui/query/useRepoViewsListQuery.ts @@ -0,0 +1,42 @@ +/** + * Copyright 2026 GitProxy Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useQuery } from '@tanstack/react-query'; +import { useNavigate } from 'react-router'; +import { fetchRepoViews } from '../services/repo'; +import { repoQueryKeys } from './repoQueryKeys'; + +/** + * Shared cache for GET /repo (unsorted). Consumers apply {@link sortRepoViews} locally. + */ +export function useRepoViewsListQuery(enabled: boolean) { + const navigate = useNavigate(); + + return useQuery({ + queryKey: repoQueryKeys.list(), + queryFn: async () => { + const result = await fetchRepoViews(); + if (result.success && result.data) { + return result.data; + } + if (result.status === 401) { + navigate('/login', { replace: true }); + } + throw new Error(result.message || 'Failed to load repositories'); + }, + enabled, + }); +} diff --git a/src/ui/query/useUserActivityQuery.ts b/src/ui/query/useUserActivityQuery.ts new file mode 100644 index 000000000..96994b883 --- /dev/null +++ b/src/ui/query/useUserActivityQuery.ts @@ -0,0 +1,34 @@ +/** + * Copyright 2026 GitProxy Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useQuery } from '@tanstack/react-query'; +import { getUserActivity } from '../services/git-push'; +import { userQueryKeys } from './userQueryKeys'; +import { PushActionView } from '../types'; + +export function useUserActivityQuery(username: string | undefined) { + return useQuery({ + queryKey: userQueryKeys.activity(username ?? ''), + queryFn: async () => { + const result = await getUserActivity(username!); + if (result.success && result.data) { + return result.data; + } + throw new Error(result.message || 'Failed to load user activity'); + }, + enabled: Boolean(username), + }); +} diff --git a/src/ui/query/useUserQuery.ts b/src/ui/query/useUserQuery.ts new file mode 100644 index 000000000..b812a8b38 --- /dev/null +++ b/src/ui/query/useUserQuery.ts @@ -0,0 +1,51 @@ +/** + * Copyright 2026 GitProxy Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useQuery } from '@tanstack/react-query'; +import { useNavigate } from 'react-router'; +import axios from 'axios'; +import { getAxiosConfig } from '../services/auth'; +import { getBaseUrl, getApiV1BaseUrl } from '../services/apiConfig'; +import { userQueryKeys } from './userQueryKeys'; +import { PublicUser } from '../../db/types'; + +async function fetchUser(id: string | null | undefined): Promise { + const baseUrl = await getBaseUrl(); + const apiV1BaseUrl = await getApiV1BaseUrl(); + const url = id ? `${apiV1BaseUrl}/user/${encodeURIComponent(id)}` : `${baseUrl}/api/auth/profile`; + const response = await axios(url, getAxiosConfig()); + return response.data; +} + +export function useUserQuery(id: string | null | undefined) { + const navigate = useNavigate(); + const queryKey = userQueryKeys.detail(id ?? 'profile'); + + return useQuery({ + queryKey, + queryFn: async () => { + try { + return await fetchUser(id); + } catch (error: unknown) { + const status = (error as any)?.response?.status; + if (status === 401) { + navigate('/login', { replace: true }); + } + throw error; + } + }, + }); +} diff --git a/src/ui/query/useUsernameByEmailQuery.ts b/src/ui/query/useUsernameByEmailQuery.ts new file mode 100644 index 000000000..07a8cfb43 --- /dev/null +++ b/src/ui/query/useUsernameByEmailQuery.ts @@ -0,0 +1,34 @@ +/** + * Copyright 2026 GitProxy Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useQuery } from '@tanstack/react-query'; +import { resolveUsernameByEmail } from '../services/user'; +import { userQueryKeys } from './userQueryKeys'; + +/** Email→username mappings change rarely; cache indefinitely within a session. */ +const USERNAME_BY_EMAIL_STALE_MS = Infinity; + +export function useUsernameByEmailQuery(email: string | undefined) { + return useQuery({ + queryKey: userQueryKeys.byEmail(email ?? ''), + queryFn: async () => { + const result = await resolveUsernameByEmail(email!); + return result.success ? (result.data ?? null) : null; + }, + enabled: Boolean(email), + staleTime: USERNAME_BY_EMAIL_STALE_MS, + }); +} diff --git a/src/ui/query/useUsersListQuery.ts b/src/ui/query/useUsersListQuery.ts new file mode 100644 index 000000000..9762048a5 --- /dev/null +++ b/src/ui/query/useUsersListQuery.ts @@ -0,0 +1,31 @@ +/** + * Copyright 2026 GitProxy Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useQuery } from '@tanstack/react-query'; +import { fetchUsersForAutocomplete } from '../services/user'; +import { userQueryKeys } from './userQueryKeys'; +import { PublicUser } from '../../db/types'; + +/** + * Shared cache for GET /user (unsorted). Consumers apply sorting locally. + */ +export function useUsersListQuery(enabled = true) { + return useQuery({ + queryKey: userQueryKeys.list(), + queryFn: () => fetchUsersForAutocomplete(), + enabled, + }); +} diff --git a/src/ui/query/userQueryKeys.ts b/src/ui/query/userQueryKeys.ts new file mode 100644 index 000000000..0031e3c5b --- /dev/null +++ b/src/ui/query/userQueryKeys.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2026 GitProxy Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const userQueryKeys = { + all: ['users'] as const, + list: () => [...userQueryKeys.all, 'list'] as const, + detail: (id: string) => [...userQueryKeys.all, id] as const, + activity: (username: string) => [...userQueryKeys.all, username, 'activity'] as const, + displayName: (username: string) => [...userQueryKeys.all, username, 'displayName'] as const, + byEmail: (email: string) => [...userQueryKeys.all, 'byEmail', email] as const, +}; From d3f7218a98f2a4ed7ae4b66d9109929abb3870a7 Mon Sep 17 00:00:00 2001 From: Andy Pols Date: Fri, 17 Apr 2026 13:57:16 +0000 Subject: [PATCH 06/25] chore: replace MUI shared components with Primer --- src/activity/activityPrimaryStatus.ts | 56 ++ src/index.tsx | 57 +- src/routes.tsx | 18 +- .../ActivityBadgeGroup/ActivityBadgeGroup.tsx | 103 +++ src/ui/components/Card/Card.tsx | 57 -- src/ui/components/Card/CardAvatar.tsx | 54 -- src/ui/components/Card/CardBody.tsx | 54 -- src/ui/components/Card/CardFooter.tsx | 60 -- src/ui/components/Card/CardHeader.tsx | 55 -- src/ui/components/Card/CardIcon.tsx | 50 -- src/ui/components/CustomButtons/Button.tsx | 85 --- .../CustomButtons/CodeActionButton.tsx | 167 ++--- src/ui/components/CustomTabs/CustomTabs.tsx | 119 --- .../ErrorBoundary/ErrorBoundary.tsx | 124 +--- src/ui/components/Filtering/Filtering.css | 55 -- src/ui/components/Filtering/Filtering.tsx | 80 --- src/ui/components/Footer/Footer.tsx | 44 +- .../GitProxyUnderlineNav.tsx | 42 ++ .../GitProxyUnderlinePanels.tsx | 39 + .../gitProxyUnderlineTabStripClass.ts} | 30 +- .../components/GitProxyUnderlineTabs/index.ts | 19 + src/ui/components/Grid/GridContainer.tsx | 43 -- .../ListFilterInput/ListFilterInput.tsx | 87 +++ .../Navbars/DashboardNavbarLinks.tsx | 137 ---- .../components/Navbars/DashboardUserMenu.tsx | 92 +++ src/ui/components/Navbars/Navbar.tsx | 261 +++++-- src/ui/components/Pagination/Pagination.css | 37 - src/ui/components/Pagination/Pagination.tsx | 41 +- src/ui/components/RouteGuard/RouteGuard.tsx | 11 +- src/ui/components/Search/Search.css | 18 - src/ui/components/Search/Search.tsx | 55 -- src/ui/components/Sidebar/Sidebar.tsx | 164 ----- src/ui/components/Snackbar/Snackbar.tsx | 98 --- .../components/Snackbar/SnackbarContent.tsx | 72 -- src/ui/components/TimedBanner/TimedBanner.tsx | 80 +++ src/ui/components/Typography/Danger.tsx | 14 +- .../UserDisplayLink/UserDisplayLink.tsx | 48 ++ .../components/UserIdentity/UserIdentity.tsx | 79 ++ src/ui/components/UserLink/UserLink.tsx | 18 +- .../UserTableNameCell/UserTableNameCell.tsx | 70 ++ src/ui/components/Warning/Warning.tsx | 59 ++ src/ui/hooks/useClientPagination.ts | 59 ++ src/ui/layouts/App.tsx | 94 +++ src/ui/layouts/Dashboard.tsx | 133 ---- src/ui/layouts/dashboardLayout.ts | 40 ++ src/ui/views/Login/Login.tsx | 222 +++--- src/ui/views/PushRequests/PushRequests.tsx | 229 ++++-- .../views/PushRequests/activityListQuery.ts | 111 +++ .../views/PushRequests/activityTabFilters.ts | 123 ++++ .../PushRequests/components/PushesTable.tsx | 680 +++++++++++++----- 50 files changed, 2398 insertions(+), 2145 deletions(-) create mode 100644 src/activity/activityPrimaryStatus.ts create mode 100644 src/ui/components/ActivityBadgeGroup/ActivityBadgeGroup.tsx delete mode 100644 src/ui/components/Card/Card.tsx delete mode 100644 src/ui/components/Card/CardAvatar.tsx delete mode 100644 src/ui/components/Card/CardBody.tsx delete mode 100644 src/ui/components/Card/CardFooter.tsx delete mode 100644 src/ui/components/Card/CardHeader.tsx delete mode 100644 src/ui/components/Card/CardIcon.tsx delete mode 100644 src/ui/components/CustomButtons/Button.tsx delete mode 100644 src/ui/components/CustomTabs/CustomTabs.tsx delete mode 100644 src/ui/components/Filtering/Filtering.css delete mode 100644 src/ui/components/Filtering/Filtering.tsx create mode 100644 src/ui/components/GitProxyUnderlineTabs/GitProxyUnderlineNav.tsx create mode 100644 src/ui/components/GitProxyUnderlineTabs/GitProxyUnderlinePanels.tsx rename src/ui/components/{Grid/GridItem.tsx => GitProxyUnderlineTabs/gitProxyUnderlineTabStripClass.ts} (51%) create mode 100644 src/ui/components/GitProxyUnderlineTabs/index.ts delete mode 100644 src/ui/components/Grid/GridContainer.tsx create mode 100644 src/ui/components/ListFilterInput/ListFilterInput.tsx delete mode 100644 src/ui/components/Navbars/DashboardNavbarLinks.tsx create mode 100644 src/ui/components/Navbars/DashboardUserMenu.tsx delete mode 100644 src/ui/components/Pagination/Pagination.css delete mode 100644 src/ui/components/Search/Search.css delete mode 100644 src/ui/components/Search/Search.tsx delete mode 100644 src/ui/components/Sidebar/Sidebar.tsx delete mode 100644 src/ui/components/Snackbar/Snackbar.tsx delete mode 100644 src/ui/components/Snackbar/SnackbarContent.tsx create mode 100644 src/ui/components/TimedBanner/TimedBanner.tsx create mode 100644 src/ui/components/UserDisplayLink/UserDisplayLink.tsx create mode 100644 src/ui/components/UserIdentity/UserIdentity.tsx create mode 100644 src/ui/components/UserTableNameCell/UserTableNameCell.tsx create mode 100644 src/ui/components/Warning/Warning.tsx create mode 100644 src/ui/hooks/useClientPagination.ts create mode 100644 src/ui/layouts/App.tsx delete mode 100644 src/ui/layouts/Dashboard.tsx create mode 100644 src/ui/layouts/dashboardLayout.ts create mode 100644 src/ui/views/PushRequests/activityListQuery.ts create mode 100644 src/ui/views/PushRequests/activityTabFilters.ts diff --git a/src/activity/activityPrimaryStatus.ts b/src/activity/activityPrimaryStatus.ts new file mode 100644 index 000000000..e9a40af79 --- /dev/null +++ b/src/activity/activityPrimaryStatus.ts @@ -0,0 +1,56 @@ +/** + * Copyright 2026 GitProxy Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** Activity list status tabs excluding the aggregate `all` tab. */ +export type ActivityStatusTab = 'pending' | 'approved' | 'canceled' | 'rejected' | 'error'; + +/** Minimal push fields used to compute the primary Activity tab bucket. */ +export type ActivityPrimaryStatusInput = { + error?: boolean; + rejected?: boolean; + canceled?: boolean; + authorised?: boolean; + blocked?: boolean; + allowPush?: boolean; +}; + +/** + * Single status bucket per push so tab counts partition the list (matches Activity UI). + * + * Priority: terminal outcomes first, then approved, blocked pending, then `allowPush` as approved; + * remaining rows default to pending. + */ +export function activityPrimaryStatusFromFlags(row: ActivityPrimaryStatusInput): ActivityStatusTab { + if (row.error === true) { + return 'error'; + } + if (row.rejected === true) { + return 'rejected'; + } + if (row.canceled === true) { + return 'canceled'; + } + if (row.authorised === true) { + return 'approved'; + } + if (row.blocked === true) { + return 'pending'; + } + if (row.allowPush === true) { + return 'approved'; + } + return 'pending'; +} diff --git a/src/index.tsx b/src/index.tsx index d162503de..cb567f021 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -15,39 +15,52 @@ */ import React from 'react'; -import ReactDOM from 'react-dom'; -import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom'; +import { createRoot } from 'react-dom/client'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router'; +import { ThemeProvider as PrimerThemeProvider, BaseStyles } from '@primer/react'; +import '@primer/primitives/dist/css/functional/themes/light.css'; // Styles -import 'font-awesome/css/font-awesome.min.css'; -import '@fontsource/roboto/300.css'; -import '@fontsource/roboto/400.css'; -import '@fontsource/roboto/500.css'; -import '@fontsource/roboto/700.css'; -import 'material-design-icons/iconfont/material-icons.css'; import 'diff2html/bundles/css/diff2html.min.css'; // Auth provider import { AuthProvider } from './ui/auth/AuthProvider'; +import { createAppQueryClient } from './ui/query/queryClient'; // Core components -import Dashboard from './ui/layouts/Dashboard'; +import App from './ui/layouts/App'; import Login from './ui/views/Login/Login'; import './ui/assets/css/material-dashboard-react.css'; +import './tailwind.css'; import NotAuthorized from './ui/views/Extras/NotAuthorized'; import NotFound from './ui/views/Extras/NotFound'; -ReactDOM.render( - - - - } /> - } /> - } /> - } /> - } /> - - - , - document.getElementById('root'), +const queryClient = createAppQueryClient(); + +const container = document.getElementById('root'); +if (!container) { + throw new Error('Root element #root not found'); +} +const root = createRoot(container); +root.render( + + + + + + + + } /> + + } /> + } /> + } /> + } /> + + + + + + , ); diff --git a/src/routes.tsx b/src/routes.tsx index 47ce3e44e..59675e466 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -16,7 +16,6 @@ import React from 'react'; import RouteGuard from './ui/components/RouteGuard/RouteGuard'; -import Person from '@material-ui/icons/Person'; import PushRequests from './ui/views/PushRequests/PushRequests'; import PushDetails from './ui/views/PushDetails/PushDetails'; import User from './ui/views/User/UserProfile'; @@ -25,16 +24,12 @@ import RepoDetails from './ui/views/RepoDetails/RepoDetails'; import RepoList from './ui/views/RepoList/RepoList'; import SettingsView from './ui/views/Settings/Settings'; -import { RepoIcon } from '@primer/octicons-react'; -import { AccountCircle, Dashboard, Group, Settings } from '@material-ui/icons'; - import { Route } from './ui/types'; const dashboardRoutes: Route[] = [ { path: '/repo', name: 'Repositories', - icon: RepoIcon, component: (props) => , layout: '/dashboard', visible: true, @@ -42,7 +37,6 @@ const dashboardRoutes: Route[] = [ { path: '/repo/:id', name: 'Repo Details', - icon: Person, component: (props) => ( ), @@ -51,8 +45,7 @@ const dashboardRoutes: Route[] = [ }, { path: '/push', - name: 'Dashboard', - icon: Dashboard, + name: 'Activity', component: (props) => , layout: '/dashboard', visible: true, @@ -60,7 +53,6 @@ const dashboardRoutes: Route[] = [ { path: '/push/:id', name: 'Open Push Requests', - icon: Person, component: (props) => ( ), @@ -70,15 +62,13 @@ const dashboardRoutes: Route[] = [ { path: '/profile', name: 'My Account', - icon: AccountCircle, component: (props) => , layout: '/dashboard', - visible: true, + visible: false, }, { path: '/admin/user', name: 'Users', - icon: Group, component: (props) => ( ), @@ -88,7 +78,6 @@ const dashboardRoutes: Route[] = [ { path: '/user/:id', name: 'User', - icon: Person, component: (props) => , layout: '/dashboard', visible: false, @@ -96,12 +85,11 @@ const dashboardRoutes: Route[] = [ { path: '/admin/settings', name: 'Settings', - icon: Settings, component: (props) => ( ), layout: '/dashboard', - visible: true, + visible: false, }, ]; diff --git a/src/ui/components/ActivityBadgeGroup/ActivityBadgeGroup.tsx b/src/ui/components/ActivityBadgeGroup/ActivityBadgeGroup.tsx new file mode 100644 index 000000000..e496b554f --- /dev/null +++ b/src/ui/components/ActivityBadgeGroup/ActivityBadgeGroup.tsx @@ -0,0 +1,103 @@ +/** + * Copyright 2026 GitProxy Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { Link, Stack } from '@primer/react'; +import { + AlertIcon, + BlockedIcon, + CheckCircleIcon, + EyeIcon, + XCircleIcon, +} from '@primer/octicons-react'; +import type { Icon } from '@primer/octicons-react'; +import { RepoActivityTabCounts } from '../../../db/types'; + +type ActivityStatus = keyof RepoActivityTabCounts; + +interface BadgeConfig { + status: ActivityStatus; + icon: Icon; + className: string; + label: (count: number) => string; +} + +const badgeConfigs: BadgeConfig[] = [ + { + status: 'pending', + icon: EyeIcon, + className: + 'inline-flex items-center gap-1 rounded-full border border-[var(--borderColor-attention-muted)] bg-[var(--bgColor-attention-muted)] px-2 py-0.5 text-xs font-medium text-[var(--fgColor-attention)] !no-underline hover:border-[var(--borderColor-attention-emphasis)]', + label: () => 'pending', + }, + { + status: 'approved', + icon: CheckCircleIcon, + className: + 'inline-flex items-center gap-1 rounded-full border border-[var(--borderColor-success-muted)] bg-[var(--bgColor-success-muted)] px-2 py-0.5 text-xs font-medium text-[var(--fgColor-success)] !no-underline hover:border-[var(--borderColor-success-emphasis)]', + label: () => 'approved', + }, + { + status: 'canceled', + icon: XCircleIcon, + className: + 'inline-flex items-center gap-1 rounded-full border border-[var(--borderColor-default)] bg-[var(--bgColor-muted)] px-2 py-0.5 text-xs font-medium text-[var(--fgColor-muted)] !no-underline hover:border-[var(--borderColor-neutral-emphasis)]', + label: () => 'canceled', + }, + { + status: 'rejected', + icon: BlockedIcon, + className: + 'inline-flex items-center gap-1 rounded-full border border-[var(--borderColor-danger-muted)] bg-[var(--bgColor-danger-muted)] px-2 py-0.5 text-xs font-medium text-[var(--fgColor-danger)] !no-underline hover:border-[var(--borderColor-danger-emphasis)]', + label: () => 'rejected', + }, + { + status: 'error', + icon: AlertIcon, + className: + 'inline-flex items-center gap-1 rounded-full border border-[var(--borderColor-danger-muted)] bg-[var(--bgColor-danger-muted)] px-2 py-0.5 text-xs font-medium text-[var(--fgColor-danger)] !no-underline hover:border-[var(--borderColor-danger-emphasis)]', + label: (count) => (count === 1 ? 'error' : 'errors'), + }, +]; + +interface ActivityBadgeGroupProps { + activity: RepoActivityTabCounts; + hrefForStatus: (status: ActivityStatus) => string; +} + +const ActivityBadgeGroup = ({ + activity, + hrefForStatus, +}: ActivityBadgeGroupProps): React.ReactElement => ( + + {badgeConfigs.map(({ status, icon: BadgeIcon, className, label }) => { + const count = activity[status]; + if (count <= 0) return null; + return ( + + {count} {label(count)} + + ); + })} + +); + +export default ActivityBadgeGroup; diff --git a/src/ui/components/Card/Card.tsx b/src/ui/components/Card/Card.tsx deleted file mode 100644 index f33ed533c..000000000 --- a/src/ui/components/Card/Card.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright 2026 GitProxy Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import clsx from 'clsx'; -import { makeStyles } from '@material-ui/core/styles'; -import styles from '../../assets/jss/material-dashboard-react/components/cardStyle'; - -const useStyles = makeStyles(styles); - -interface CardProps extends React.ComponentProps<'div'> { - className?: string; - plain?: boolean; - profile?: boolean; - chart?: boolean; - children?: React.ReactNode; -} - -const Card: React.FC = ({ - className = '', - children, - plain = false, - profile = false, - chart = false, - ...rest -}) => { - const classes = useStyles(); - - const cardClasses = clsx({ - [classes.card]: true, - [classes.cardPlain]: plain, - [classes.cardProfile]: profile, - [classes.cardChart]: chart, - [className]: className, - }); - - return ( -
- {children} -
- ); -}; - -export default Card; diff --git a/src/ui/components/Card/CardAvatar.tsx b/src/ui/components/Card/CardAvatar.tsx deleted file mode 100644 index bffa933f1..000000000 --- a/src/ui/components/Card/CardAvatar.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright 2026 GitProxy Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import clsx from 'clsx'; -import { makeStyles } from '@material-ui/core/styles'; -import styles from '../../assets/jss/material-dashboard-react/components/cardAvatarStyle'; - -const useStyles = makeStyles(styles); - -interface CardAvatarProps extends React.ComponentProps<'div'> { - children: React.ReactNode; - className?: string; - profile?: boolean; - plain?: boolean; -} - -const CardAvatar: React.FC = ({ - children, - className = '', - profile = false, - plain = false, - ...rest -}) => { - const classes = useStyles(); - - const cardAvatarClasses = clsx({ - [classes.cardAvatar]: true, - [classes.cardAvatarProfile]: profile, - [classes.cardAvatarPlain]: plain, - [className]: className, - }); - - return ( -
- {children} -
- ); -}; - -export default CardAvatar; diff --git a/src/ui/components/Card/CardBody.tsx b/src/ui/components/Card/CardBody.tsx deleted file mode 100644 index 48e7fac86..000000000 --- a/src/ui/components/Card/CardBody.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright 2026 GitProxy Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import clsx from 'clsx'; -import { makeStyles } from '@material-ui/core/styles'; -import styles from '../../assets/jss/material-dashboard-react/components/cardBodyStyle'; - -const useStyles = makeStyles(styles); - -interface CardBodyProps extends React.ComponentProps<'div'> { - className?: string; - plain?: boolean; - profile?: boolean; - children?: React.ReactNode; -} - -const CardBody: React.FC = ({ - className = '', - children, - plain = false, - profile = false, - ...rest -}) => { - const classes = useStyles(); - - const cardBodyClasses = clsx({ - [classes.cardBody]: true, - [classes.cardBodyPlain]: plain, - [classes.cardBodyProfile]: profile, - [className]: className, - }); - - return ( -
- {children} -
- ); -}; - -export default CardBody; diff --git a/src/ui/components/Card/CardFooter.tsx b/src/ui/components/Card/CardFooter.tsx deleted file mode 100644 index 89d1af3e1..000000000 --- a/src/ui/components/Card/CardFooter.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright 2026 GitProxy Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import clsx from 'clsx'; -import { makeStyles } from '@material-ui/core/styles'; -import styles from '../../assets/jss/material-dashboard-react/components/cardFooterStyle'; - -const useStyles = makeStyles(styles); - -interface CardFooterProps extends React.ComponentProps<'div'> { - className?: string; - plain?: boolean; - profile?: boolean; - stats?: boolean; - chart?: boolean; - children?: React.ReactNode; -} - -const CardFooter: React.FC = ({ - className, - children, - plain, - profile, - stats, - chart, - ...rest -}) => { - const classes = useStyles(); - - const cardFooterClasses = clsx({ - [classes.cardFooter]: true, - [classes.cardFooterPlain]: plain, - [classes.cardFooterProfile]: profile, - [classes.cardFooterStats]: stats, - [classes.cardFooterChart]: chart, - [className || '']: className !== undefined, - }); - - return ( -
- {children} -
- ); -}; - -export default CardFooter; diff --git a/src/ui/components/Card/CardHeader.tsx b/src/ui/components/Card/CardHeader.tsx deleted file mode 100644 index 61e2a65f5..000000000 --- a/src/ui/components/Card/CardHeader.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright 2026 GitProxy Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import clsx from 'clsx'; -import { makeStyles } from '@material-ui/core/styles'; -import styles from '../../assets/jss/material-dashboard-react/components/cardHeaderStyle'; - -const useStyles = makeStyles(styles); - -export type CardHeaderColor = 'warning' | 'success' | 'danger' | 'info' | 'primary' | 'rose'; - -interface CardHeaderProps extends React.ComponentProps<'div'> { - className?: string; - color?: CardHeaderColor; - plain?: boolean; - stats?: boolean; - icon?: boolean; - children?: React.ReactNode; -} - -const CardHeader: React.FC = (props) => { - const classes = useStyles(); - const { className, children, color, plain, stats, icon, ...rest } = props; - - const cardHeaderClasses = clsx({ - [classes.cardHeader]: true, - [color ? classes[`${color}CardHeader`] : '']: color, - [classes.cardHeaderPlain]: plain, - [classes.cardHeaderStats]: stats, - [classes.cardHeaderIcon]: icon, - [className || '']: className !== undefined, - }); - - return ( -
- {children} -
- ); -}; - -export default CardHeader; diff --git a/src/ui/components/Card/CardIcon.tsx b/src/ui/components/Card/CardIcon.tsx deleted file mode 100644 index 6dda34e9d..000000000 --- a/src/ui/components/Card/CardIcon.tsx +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright 2026 GitProxy Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import clsx from 'clsx'; -import { makeStyles } from '@material-ui/core/styles'; -import styles from '../../assets/jss/material-dashboard-react/components/cardIconStyle'; - -const useStyles = makeStyles(styles); - -type CardIconColor = 'warning' | 'success' | 'danger' | 'info' | 'primary' | 'rose'; - -interface CardIconProps { - className?: string; - color?: CardIconColor; - children?: React.ReactNode; - [key: string]: any; -} - -const CardIcon: React.FC = (props) => { - const classes = useStyles(); - const { className, children, color, ...rest } = props; - - const cardIconClasses = clsx({ - [classes.cardIcon]: true, - [color ? classes[`${color}CardHeader`] : '']: color, - [className || '']: className !== undefined, - }); - - return ( -
- {children} -
- ); -}; - -export default CardIcon; diff --git a/src/ui/components/CustomButtons/Button.tsx b/src/ui/components/CustomButtons/Button.tsx deleted file mode 100644 index a5f5e258b..000000000 --- a/src/ui/components/CustomButtons/Button.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright 2026 GitProxy Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import clsx from 'clsx'; -import { makeStyles } from '@material-ui/core/styles'; -import Button, { ButtonProps } from '@material-ui/core/Button'; -import styles from '../../assets/jss/material-dashboard-react/components/buttonStyle'; - -const useStyles = makeStyles(styles); - -type Color = - | 'primary' - | 'info' - | 'success' - | 'warning' - | 'danger' - | 'rose' - | 'white' - | 'transparent'; -type Size = 'sm' | 'lg'; - -interface RegularButtonProps extends Omit { - color?: Color; - round?: boolean; - disabled?: boolean; - simple?: boolean; - size?: Size; - block?: boolean; - link?: boolean; - justIcon?: boolean; - className?: string; - muiClasses?: Record; - children?: React.ReactNode; -} - -export default function RegularButton(props: RegularButtonProps) { - const classes = useStyles(); - const { - color, - round, - children, - disabled, - simple, - size, - block, - link, - justIcon, - className, - muiClasses, - ...rest - } = props; - - const btnClasses = clsx({ - [classes.button]: true, - [classes[size as Size]]: size, - [classes[color as Color]]: color, - [classes.round]: round, - [classes.disabled]: disabled, - [classes.simple]: simple, - [classes.block]: block, - [classes.link]: link, - [classes.justIcon]: justIcon, - [className || '']: className, - }); - - return ( - - ); -} diff --git a/src/ui/components/CustomButtons/CodeActionButton.tsx b/src/ui/components/CustomButtons/CodeActionButton.tsx index a88505a21..f13f51663 100644 --- a/src/ui/components/CustomButtons/CodeActionButton.tsx +++ b/src/ui/components/CustomButtons/CodeActionButton.tsx @@ -14,133 +14,70 @@ * limitations under the License. */ -import Popper from '@material-ui/core/Popper'; -import Paper from '@material-ui/core/Paper'; -import ClickAwayListener from '@material-ui/core/ClickAwayListener'; -import { - CheckIcon, - ChevronDownIcon, - CodeIcon, - CopyIcon, - TerminalIcon, -} from '@primer/octicons-react'; import React, { useState } from 'react'; -import { PopperPlacementType } from '@material-ui/core/Popper'; -import Button from './Button'; +import { ActionMenu, IconButton, Text } from '@primer/react'; +import { CheckIcon, CodeIcon, CopyIcon, TerminalIcon } from '@primer/octicons-react'; + +/** Git-style green (matches common "Code" affordance). */ +const codeButtonGreenClassName = + '!border-0 !shadow-none !bg-[#1a7f37] !text-white hover:!bg-[#136c2e] hover:!text-white active:!bg-[#115f2a]'; interface CodeActionButtonProps { cloneURL: string; } -const CodeActionButton: React.FC = ({ cloneURL }) => { - const [anchorEl, setAnchorEl] = useState(null); - const [open, setOpen] = useState(false); - const [placement, setPlacement] = useState(); - const [isCopied, setIsCopied] = useState(false); - - const handleClick = - (newPlacement: PopperPlacementType) => (event: React.MouseEvent) => { - setIsCopied(false); - setAnchorEl(event.currentTarget); - setOpen((prev) => placement !== newPlacement || !prev); - setPlacement(newPlacement); - }; +const CodeActionButton = ({ cloneURL }: CodeActionButtonProps) => { + const [isCopied, setIsCopied] = useState(false); - const handleClickAway = () => { - setOpen(false); + const copyCloneUrl = (): void => { + void navigator.clipboard.writeText(cloneURL); + setIsCopied(true); }; return ( - <> - - + - - -
- {' '} - - Clone - -
-
- - {cloneURL} - - - {!isCopied && ( - { - navigator.clipboard.writeText(`git clone ${cloneURL}`); - setIsCopied(true); - }} - > - - - )} - {isCopied && ( - - - - )} - -
-
-
- - Use Git and run this command in your IDE or Terminal 👍 - -
-
-
-
-
- +
+
+ + Clone +
+
+ + {cloneURL} + + { + event.preventDefault(); + copyCloneUrl(); + }} + /> +
+ + Clone using the git URL. + +
+ + ); }; diff --git a/src/ui/components/CustomTabs/CustomTabs.tsx b/src/ui/components/CustomTabs/CustomTabs.tsx deleted file mode 100644 index e24243cf4..000000000 --- a/src/ui/components/CustomTabs/CustomTabs.tsx +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Copyright 2026 GitProxy Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useState } from 'react'; -import clsx from 'clsx'; -import { makeStyles } from '@material-ui/core/styles'; -import Tabs from '@material-ui/core/Tabs'; -import Tab from '@material-ui/core/Tab'; -import Card from '../Card/Card'; -import CardBody from '../Card/CardBody'; -import CardHeader from '../Card/CardHeader'; -import styles from '../../assets/jss/material-dashboard-react/components/customTabsStyle'; -import { SvgIconProps } from '@material-ui/core'; -import Badge from '@material-ui/core/Badge'; - -const useStyles = makeStyles(styles as any); - -type HeaderColor = 'warning' | 'success' | 'danger' | 'info' | 'primary' | 'rose'; - -export type TabItem = { - tabName: string; - tabIcon?: React.ComponentType; - tabContent: React.ReactNode; - badge?: number; -}; - -interface CustomTabsProps { - headerColor?: HeaderColor; - title?: string; - tabs: TabItem[]; - rtlActive?: boolean; - plainTabs?: boolean; - defaultTab?: number; -} - -const CustomTabs: React.FC = ({ - headerColor = 'primary', - plainTabs = false, - tabs, - title, - rtlActive = false, - defaultTab = 0, -}) => { - const [value, setValue] = useState(defaultTab); - const classes = useStyles(); - - const handleChange = (event: React.ChangeEvent, newValue: number) => { - setValue(newValue); - }; - - const cardTitle = clsx({ - [classes.cardTitle]: true, - [classes.cardTitleRTL]: rtlActive, - }); - - return ( - - - {title !== undefined ?
{title}
: null} - - {tabs.map((prop, key) => { - const icon = prop.tabIcon ? { icon: } : {}; - const label = prop.badge ? ( - - {prop.tabName} - - ) : ( - prop.tabName - ); - return ( - - ); - })} - -
- - {tabs.map((prop, key) => ( -
- {prop.tabContent} -
- ))} -
-
- ); -}; - -export default CustomTabs; diff --git a/src/ui/components/ErrorBoundary/ErrorBoundary.tsx b/src/ui/components/ErrorBoundary/ErrorBoundary.tsx index 922884b57..8803dfd20 100644 --- a/src/ui/components/ErrorBoundary/ErrorBoundary.tsx +++ b/src/ui/components/ErrorBoundary/ErrorBoundary.tsx @@ -15,94 +15,33 @@ */ import React, { Component, ErrorInfo, PropsWithChildren, ReactNode, useState } from 'react'; -import Paper from '@material-ui/core/Paper'; -import Typography from '@material-ui/core/Typography'; -import Button from '@material-ui/core/Button'; -import Collapse from '@material-ui/core/Collapse'; -import { makeStyles } from '@material-ui/core/styles'; +import { Button, Label, Stack, Text } from '@primer/react'; const IS_DEV = process.env.NODE_ENV !== 'production'; -const useStyles = makeStyles((theme) => ({ - wrapper: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - height: '100%', - minHeight: '60vh', - padding: theme.spacing(2), - }, - root: { - padding: theme.spacing(4), - borderLeft: `4px solid ${theme.palette.error.main}`, - maxWidth: 560, - width: '100%', - }, - title: { - color: theme.palette.error.main, - marginBottom: theme.spacing(1), - }, - message: { - marginBottom: theme.spacing(2), - color: theme.palette.text.secondary, - }, - hint: { - marginBottom: theme.spacing(2), - color: theme.palette.text.secondary, - fontStyle: 'italic', - }, - actions: { - display: 'flex', - gap: theme.spacing(1), - alignItems: 'center', - marginBottom: theme.spacing(1), - }, - stack: { - marginTop: theme.spacing(2), - padding: theme.spacing(2), - backgroundColor: theme.palette.grey[100], - borderRadius: theme.shape.borderRadius, - overflowX: 'auto', - fontSize: '0.75rem', - fontFamily: 'monospace', - whiteSpace: 'pre-wrap', - wordBreak: 'break-word', - }, - devBadge: { - display: 'inline-block', - marginBottom: theme.spacing(2), - padding: '2px 8px', - backgroundColor: theme.palette.warning.main, - color: theme.palette.warning.contrastText, - borderRadius: theme.shape.borderRadius, - fontSize: '0.7rem', - fontWeight: 700, - letterSpacing: '0.05em', - textTransform: 'uppercase', - }, -})); +const errorPanelClass = + 'max-w-[560px] w-full rounded-md border border-(--borderColor-default) border-l-4 border-l-(--borderColor-danger-emphasis) bg-(--bgColor-default) p-6 shadow-sm'; const ProdFallback = ({ reset }: { reset: () => void }) => { - const classes = useStyles(); return ( -
- - +
+
+ Something went wrong - - + + An unexpected error occurred. Please try again — if the problem persists, contact your administrator. - -
- - -
- + +
); }; @@ -116,36 +55,37 @@ const DevFallback = ({ name?: string; reset: () => void; }) => { - const classes = useStyles(); const [showDetails, setShowDetails] = useState(false); const context = name ? ` in ${name}` : ''; return ( -
- -
dev
- +
+
+ + Something went wrong{context} - - + + {error.message} - -
- {error.stack && ( - )} -
- {error.stack && ( - -
{error.stack}
-
+ + {error.stack && showDetails && ( +
+            {error.stack}
+          
)} - +
); }; diff --git a/src/ui/components/Filtering/Filtering.css b/src/ui/components/Filtering/Filtering.css deleted file mode 100644 index a83724cb2..000000000 --- a/src/ui/components/Filtering/Filtering.css +++ /dev/null @@ -1,55 +0,0 @@ -.filtering-container { - position: relative; - display: inline-block; - padding-bottom: 10px; -} - -.dropdown-toggle { - padding: 10px 10px; - padding-right: 10px; - border: 1px solid #ccc; - border-radius: 5px; - background-color: #fff; - color: #333; - cursor: pointer; - font-size: 14px; - text-align: left; - width: 130px; - display: inline-flex; - align-items: center; - justify-content: space-between; -} - -.dropdown-toggle:hover { - background-color: #f0f0f0; -} - -.dropdown-arrow { - border: none; - background: none; - cursor: pointer; - font-size: 15px; - margin-left: 1px; - margin-right: 10px; -} - -.dropdown-menu { - position: absolute; - background-color: #fff; - border: 1px solid #ccc; - border-radius: 5px; - margin-top: 5px; - z-index: 1000; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); -} - -.dropdown-item { - padding: 10px 15px; - cursor: pointer; - font-size: 14px; - color: #333; -} - -.dropdown-item:hover { - background-color: #f0f0f0; -} diff --git a/src/ui/components/Filtering/Filtering.tsx b/src/ui/components/Filtering/Filtering.tsx deleted file mode 100644 index 83be90848..000000000 --- a/src/ui/components/Filtering/Filtering.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright 2026 GitProxy Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useState } from 'react'; -import './Filtering.css'; - -export type FilterOption = 'Date Modified' | 'Date Created' | 'Alphabetical' | 'Sort by'; -export type SortOrder = 'asc' | 'desc'; - -interface FilteringProps { - onFilterChange: (option: FilterOption, order: SortOrder) => void; -} - -const Filtering: React.FC = ({ onFilterChange }) => { - const [isOpen, setIsOpen] = useState(false); - const [selectedOption, setSelectedOption] = useState('Sort by'); - const [sortOrder, setSortOrder] = useState('asc'); - - const toggleDropdown = () => { - setIsOpen(!isOpen); - }; - - const toggleSortOrder = (e: React.MouseEvent) => { - e.stopPropagation(); - if (selectedOption !== 'Sort by') { - const newSortOrder = sortOrder === 'asc' ? 'desc' : 'asc'; - setSortOrder(newSortOrder); - onFilterChange(selectedOption, newSortOrder); - } - }; - - const handleOptionClick = (option: FilterOption) => { - setSelectedOption(option); - onFilterChange(option, sortOrder); - setIsOpen(false); - }; - - return ( -
-
- - - {isOpen && ( -
-
handleOptionClick('Date Modified')} className='dropdown-item'> - Date Modified -
-
handleOptionClick('Date Created')} className='dropdown-item'> - Date Created -
-
handleOptionClick('Alphabetical')} className='dropdown-item'> - Alphabetical -
-
- )} -
-
- ); -}; - -export default Filtering; diff --git a/src/ui/components/Footer/Footer.tsx b/src/ui/components/Footer/Footer.tsx index 8518f0d47..cef22a710 100644 --- a/src/ui/components/Footer/Footer.tsx +++ b/src/ui/components/Footer/Footer.tsx @@ -15,37 +15,29 @@ */ import React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; -import ListItem from '@material-ui/core/ListItem'; -import List from '@material-ui/core/List'; -import styles from '../../assets/jss/material-dashboard-react/components/footerStyle'; +import { Text } from '@primer/react'; import { MarkGithubIcon } from '@primer/octicons-react'; -const useStyles = makeStyles(styles); - -const Footer: React.FC = () => { - const classes = useStyles(); +const Footer = () => { + const year = new Date().getFullYear(); return ( -