diff --git a/package-lock.json b/package-lock.json index 7df9140..87505ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,15 +16,19 @@ "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-navigation-menu": "^1.2.1", + "@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-scroll-area": "^1.2.1", "@radix-ui/react-select": "^2.1.2", + "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.1", "@tanstack/react-table": "^8.20.5", "@xterm/addon-attach": "^0.11.0", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "cmdk": "^1.0.0", "jotai-zustand": "^0.6.0", "lucide-react": "^0.454.0", "next-themes": "^0.3.0", @@ -309,6 +313,18 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", @@ -883,9 +899,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.2.tgz", - "integrity": "sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", + "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1565,6 +1581,43 @@ } } }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.2.tgz", + "integrity": "sha512-u2HRUyWW+lOiA2g0Le0tMmT55FGOEWHwPFt1EPfbLly7uXQExFo5duNKqG2DzmFXIdqOeNd+TpE8baHWJCyP9w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.1", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.0", + "@radix-ui/react-portal": "1.1.2", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.6.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz", @@ -1803,6 +1856,29 @@ } } }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.0.tgz", + "integrity": "sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", @@ -1821,6 +1897,36 @@ } } }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.1.tgz", + "integrity": "sha512-3GBUDmP2DvzmtYLMsHmpA1GtR46ZDZ+OreXM/N+kkQJOPIgytFWWTfDQmBQKBvaFS0Vno0FktdbVzN28KGrMdw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-roving-focus": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", @@ -3088,6 +3194,384 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz", + "integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "1.0.5", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-dialog": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", + "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", + "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-escape-keydown": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-focus-guards": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", + "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", + "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-portal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", + "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-presence": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", + "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", + "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/react-remove-scroll": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", + "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.3", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3167,9 +3651,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -5071,6 +5555,12 @@ "url": "https://github.com/Mermade/oas-kit?sponsor=1" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", diff --git a/package.json b/package.json index 747de86..cd75587 100644 --- a/package.json +++ b/package.json @@ -18,15 +18,19 @@ "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-navigation-menu": "^1.2.1", + "@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-scroll-area": "^1.2.1", "@radix-ui/react-select": "^2.1.2", + "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.1", "@tanstack/react-table": "^8.20.5", "@xterm/addon-attach": "^0.11.0", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "cmdk": "^1.0.0", "jotai-zustand": "^0.6.0", "lucide-react": "^0.454.0", "next-themes": "^0.3.0", diff --git a/src/api/notification-group.ts b/src/api/notification-group.ts new file mode 100644 index 0000000..23c102a --- /dev/null +++ b/src/api/notification-group.ts @@ -0,0 +1,14 @@ +import { ModelNotificationGroupForm } from "@/types" +import { fetcher, FetcherMethod } from "./api" + +export const createNotificationGroup = async (data: ModelNotificationGroupForm): Promise => { + return fetcher(FetcherMethod.POST, '/api/v1/notification-group', data); +} + +export const updateNotificationGroup = async (id: number, data: ModelNotificationGroupForm): Promise => { + return fetcher(FetcherMethod.PATCH, `/api/v1/notification-group/${id}`, data); +} + +export const deleteNotificationGroups = async (id: number[]): Promise => { + return fetcher(FetcherMethod.POST, `/api/v1/batch-delete/notification-group`, id) +} diff --git a/src/api/server-group.ts b/src/api/server-group.ts new file mode 100644 index 0000000..e38418c --- /dev/null +++ b/src/api/server-group.ts @@ -0,0 +1,18 @@ +import { ModelServerGroupForm, ModelServerGroupResponseItem } from "@/types" +import { fetcher, FetcherMethod } from "./api" + +export const createServerGroup = async (data: ModelServerGroupForm): Promise => { + return fetcher(FetcherMethod.POST, '/api/v1/server-group', data); +} + +export const updateServerGroup = async (id: number, data: ModelServerGroupForm): Promise => { + return fetcher(FetcherMethod.PATCH, `/api/v1/server-group/${id}`, data); +} + +export const deleteServerGroups = async (id: number[]): Promise => { + return fetcher(FetcherMethod.POST, `/api/v1/batch-delete/server-group`, id) +} + +export const getServerGroups = async (): Promise => { + return fetcher(FetcherMethod.GET, '/api/v1/server-group', null) +} diff --git a/src/api/server.ts b/src/api/server.ts index 3bc90ff..470f00d 100644 --- a/src/api/server.ts +++ b/src/api/server.ts @@ -1,4 +1,4 @@ -import { ModelServerForm } from "@/types" +import { ModelServer, ModelServerForm } from "@/types" import { fetcher, FetcherMethod } from "./api" export const updateServer = async (id: number, data: ModelServerForm): Promise => { @@ -8,3 +8,7 @@ export const updateServer = async (id: number, data: ModelServerForm): Promise => { return fetcher(FetcherMethod.POST, '/api/v1/batch-delete/server', id) } + +export const getServers = async (): Promise => { + return fetcher(FetcherMethod.GET, '/api/v1/server', null) +} diff --git a/src/components/group-tab.tsx b/src/components/group-tab.tsx new file mode 100644 index 0000000..efa98f0 --- /dev/null +++ b/src/components/group-tab.tsx @@ -0,0 +1,23 @@ +import { + Tabs, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs" +import { Link, useLocation } from "react-router-dom" + +export const GroupTab = () => { + const location = useLocation(); + + return ( + + + + Server + + + Notification + + + + ) +} diff --git a/src/components/header.tsx b/src/components/header.tsx index 4cf89f3..3a30dc3 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -21,8 +21,8 @@ export default function Header() { const location = useLocation(); - return
- + return
+ @@ -52,6 +52,11 @@ export default function Header() { NAT Traversal + + + Groups + + } diff --git a/src/components/notification-group.tsx b/src/components/notification-group.tsx new file mode 100644 index 0000000..3a6825d --- /dev/null +++ b/src/components/notification-group.tsx @@ -0,0 +1,136 @@ +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { ScrollArea } from "@/components/ui/scroll-area" +import { useForm } from "react-hook-form" +import { z } from "zod" +import { zodResolver } from "@hookform/resolvers/zod" +import { ModelNotificationGroupResponseItem } from "@/types" +import { useState } from "react" +import { KeyedMutator } from "swr" +import { IconButton } from "@/components/xui/icon-button" +import { createNotificationGroup, updateNotificationGroup } from "@/api/notification-group" +import { conv } from "@/lib/utils" + +interface NotificationGroupCardProps { + data?: ModelNotificationGroupResponseItem; + mutate: KeyedMutator; +} + +const notificationGroupFormSchema = z.object({ + name: z.string().min(1), + notifications: z.array(z.string()).transform((v => { + return v.filter(Boolean).map(Number); + })), +}); + +export const NotificationGroupCard: React.FC = ({ data, mutate }) => { + const form = useForm>({ + resolver: zodResolver(notificationGroupFormSchema), + defaultValues: data ? data : { + name: "", + notifications: [], + }, + resetOptions: { + keepDefaultValues: false, + } + }) + + const [open, setOpen] = useState(false); + + const onSubmit = async (values: z.infer) => { + data?.group.id ? await updateNotificationGroup(data.group.id, values) : await createNotificationGroup(values); + setOpen(false); + await mutate(); + form.reset(); + } + + return ( + + + {data + ? + + : + + } + + + +
+ + New Server Group + + +
+ + ( + + Name + + + + + + )} + /> + ( + + Notification Methods + + { + const arr = conv.strToArr(e.target.value); + field.onChange(arr); + }} + /> + + + + )} + /> + + + + + + + + +
+
+
+
+ ) +} diff --git a/src/components/server-group.tsx b/src/components/server-group.tsx new file mode 100644 index 0000000..b8e9239 --- /dev/null +++ b/src/components/server-group.tsx @@ -0,0 +1,142 @@ +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { ScrollArea } from "@/components/ui/scroll-area" +import { useForm } from "react-hook-form" +import { z } from "zod" +import { zodResolver } from "@hookform/resolvers/zod" +import { ModelServerGroupResponseItem } from "@/types" +import { useState } from "react" +import { KeyedMutator } from "swr" +import { IconButton } from "@/components/xui/icon-button" +import { createServerGroup, updateServerGroup } from "@/api/server-group" +import { MultiSelect } from "@/components/xui/multi-select"; +import { useServer } from "@/hooks/useServer" + +interface ServerGroupCardProps { + data?: ModelServerGroupResponseItem; + mutate: KeyedMutator; +} + +const serverGroupFormSchema = z.object({ + name: z.string().min(1), + servers: z.array(z.string()).transform((v => { + return v.filter(Boolean).map(Number); + })), +}); + +export const ServerGroupCard: React.FC = ({ data, mutate }) => { + const form = useForm>({ + resolver: zodResolver(serverGroupFormSchema), + defaultValues: data ? { + name: data.group.name, + servers: data.servers, + } : { + name: "", + servers: [], + }, + resetOptions: { + keepDefaultValues: false, + } + }) + + const [open, setOpen] = useState(false); + + const onSubmit = async (values: z.infer) => { + data?.group.id ? await updateServerGroup(data.group.id, values) : await createServerGroup(values); + setOpen(false); + await mutate(); + form.reset(); + } + + const { servers } = useServer(); + const serverList = servers?.map(s => ({ + value: `${s.id}`, + label: s.name, + })) || [{ value: "", label: "" }]; + + return ( + + + {data + ? + + : + + } + + + +
+ + New Server Group + + +
+ + ( + + Name + + + + + + )} + /> + ( + + Servers + + + + + + )} + /> + + + + + + + + +
+
+
+
+ ) +} diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..f000e3e --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx new file mode 100644 index 0000000..f0e4e3f --- /dev/null +++ b/src/components/ui/command.tsx @@ -0,0 +1,151 @@ +import * as React from "react" +import { type DialogProps } from "@radix-ui/react-dialog" +import { Command as CommandPrimitive } from "cmdk" +import { Search } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Dialog, DialogContent } from "@/components/ui/dialog" + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +const CommandDialog = ({ children, ...props }: DialogProps) => { + return ( + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +CommandShortcut.displayName = "CommandShortcut" + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 0000000..bbba7e0 --- /dev/null +++ b/src/components/ui/popover.tsx @@ -0,0 +1,29 @@ +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent } diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx new file mode 100644 index 0000000..6d7f122 --- /dev/null +++ b/src/components/ui/separator.tsx @@ -0,0 +1,29 @@ +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "@/lib/utils" + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( + + ) +) +Separator.displayName = SeparatorPrimitive.Root.displayName + +export { Separator } diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx new file mode 100644 index 0000000..f57fffd --- /dev/null +++ b/src/components/ui/tabs.tsx @@ -0,0 +1,53 @@ +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/src/components/xui/multi-select.tsx b/src/components/xui/multi-select.tsx new file mode 100644 index 0000000..285cff4 --- /dev/null +++ b/src/components/xui/multi-select.tsx @@ -0,0 +1,403 @@ +/** + * SPDX-License-Identifier: MIT + * MIT License + + * Copyright (c) 2024 sersavan + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { + CheckIcon, + ChevronDown, + XIcon, + WandSparkles, +} from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { Separator } from "@/components/ui/separator"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "@/components/ui/command"; + +/** + * Variants for the multi-select component to handle different styles. + * Uses class-variance-authority (cva) to define different styles based on "variant" prop. + */ +const multiSelectVariants = cva( + "m-1 transition ease-in-out delay-150 hover:-translate-y-1 duration-300", + { + variants: { + variant: { + default: + "border-foreground/10 text-foreground bg-card hover:bg-card/80", + secondary: + "border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + inverted: "inverted", + }, + }, + defaultVariants: { + variant: "default", + }, + } +); + +/** + * Props for MultiSelect component + */ +interface MultiSelectProps + extends React.ButtonHTMLAttributes, + VariantProps { + /** + * An array of option objects to be displayed in the multi-select component. + * Each option object has a label, value, and an optional icon. + */ + options: { + /** The text to display for the option. */ + label: string; + /** The unique value associated with the option. */ + value: string; + /** Optional icon component to display alongside the option. */ + icon?: React.ComponentType<{ className?: string }>; + }[]; + + /** + * Callback function triggered when the selected values change. + * Receives an array of the new selected values. + */ + onValueChange: (value: string[]) => void; + + /** The default selected values when the component mounts. */ + defaultValue?: string[]; + + /** + * Placeholder text to be displayed when no values are selected. + * Optional, defaults to "Select options". + */ + placeholder?: string; + + /** + * Animation duration in seconds for the visual effects (e.g., bouncing badges). + * Optional, defaults to 0 (no animation). + */ + animation?: number; + + /** + * Maximum number of items to display. Extra selected items will be summarized. + * Optional, defaults to 3. + */ + maxCount?: number; + + /** + * The modality of the popover. When set to true, interaction with outside elements + * will be disabled and only popover content will be visible to screen readers. + * Optional, defaults to false. + */ + modalPopover?: boolean; + + /** + * If true, renders the multi-select component as a child of another component. + * Optional, defaults to false. + */ + asChild?: boolean; + + /** + * Additional class names to apply custom styles to the multi-select component. + * Optional, can be used to add custom styles. + */ + className?: string; +} + +export const MultiSelect = React.forwardRef< + HTMLButtonElement, + MultiSelectProps +>( + ( + { + options, + onValueChange, + variant, + defaultValue = [], + placeholder = "Select options", + animation = 0, + maxCount = 3, + modalPopover = false, + asChild = false, + className, + ...props + }, + ref + ) => { + const [selectedValues, setSelectedValues] = + React.useState(defaultValue); + const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); + const [isAnimating, setIsAnimating] = React.useState(false); + + const handleInputKeyDown = ( + event: React.KeyboardEvent + ) => { + if (event.key === "Enter") { + setIsPopoverOpen(true); + } else if (event.key === "Backspace" && !event.currentTarget.value) { + const newSelectedValues = [...selectedValues]; + newSelectedValues.pop(); + setSelectedValues(newSelectedValues); + onValueChange(newSelectedValues); + } + }; + + const toggleOption = (option: string) => { + const newSelectedValues = selectedValues.includes(option) + ? selectedValues.filter((value) => value !== option) + : [...selectedValues, option]; + setSelectedValues(newSelectedValues); + onValueChange(newSelectedValues); + }; + + const handleClear = () => { + setSelectedValues([]); + onValueChange([]); + }; + + const handleTogglePopover = () => { + setIsPopoverOpen((prev) => !prev); + }; + + const clearExtraOptions = () => { + const newSelectedValues = selectedValues.slice(0, maxCount); + setSelectedValues(newSelectedValues); + onValueChange(newSelectedValues); + }; + + const toggleAll = () => { + if (selectedValues.length === options.length) { + handleClear(); + } else { + const allValues = options.map((option) => option.value); + setSelectedValues(allValues); + onValueChange(allValues); + } + }; + + return ( + + + + + setIsPopoverOpen(false)} + > + + + + No results found. + + +
+ +
+ (Select All) +
+ {options.map((option) => { + const isSelected = selectedValues.includes(option.value); + return ( + toggleOption(option.value)} + className="cursor-pointer" + > +
+ +
+ {option.icon && ( + + )} + {option.label} +
+ ); + })} +
+ + +
+ {selectedValues.length > 0 && ( + <> + + Clear + + + + )} + setIsPopoverOpen(false)} + className="flex-1 justify-center cursor-pointer max-w-full" + > + Close + +
+
+
+
+
+ {animation > 0 && selectedValues.length > 0 && ( + setIsAnimating(!isAnimating)} + /> + )} +
+ ); + } +); + +MultiSelect.displayName = "MultiSelect"; diff --git a/src/hooks/useMainStore.ts b/src/hooks/useMainStore.ts index 3e340e4..e957273 100644 --- a/src/hooks/useMainStore.ts +++ b/src/hooks/useMainStore.ts @@ -1,4 +1,4 @@ -import { MainStore, ModelUser as User } from '@/types' +import { MainStore } from '@/types' import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' @@ -6,7 +6,7 @@ export const useMainStore = create( persist( (set, get) => ({ profile: get()?.profile, - setProfile: (profile: User | undefined) => set({ profile }), + setProfile: profile => set({ profile }), }), { name: 'mainStore', diff --git a/src/hooks/useServer.tsx b/src/hooks/useServer.tsx new file mode 100644 index 0000000..731afca --- /dev/null +++ b/src/hooks/useServer.tsx @@ -0,0 +1,52 @@ +import { createContext, useContext, useEffect, useMemo } from "react" +import { useServerStore } from "./useServerStore" +import { getServerGroups } from "@/api/server-group" +import { getServers } from "@/api/server" +import { ServerContextProps } from "@/types" + +const ServerContext = createContext({}); + +interface ServerProviderProps { + children: React.ReactNode; + withServer?: boolean; + withServerGroup?: boolean; +} + +export const ServerProvider: React.FC = ({ children, withServer, withServerGroup }) => { + const serverGroup = useServerStore(store => store.serverGroup); + const setServerGroup = useServerStore(store => store.setServerGroup); + + const server = useServerStore(store => store.server); + const setServer = useServerStore(store => store.setServer); + + useEffect(() => { + if (withServerGroup) + (async () => { + try { + const sg = await getServerGroups(); + setServerGroup(sg); + } catch (error) { + setServerGroup(undefined); + } + })(); + if (withServer) + (async () => { + try { + const s = await getServers(); + setServer(s); + } catch (error) { + setServer(undefined); + } + })(); + }, []) + + const value: ServerContextProps = useMemo(() => ({ + servers: server, + serverGroups: serverGroup, + }), [server, serverGroup]); + return {children}; +} + +export const useServer = () => { + return useContext(ServerContext); +}; diff --git a/src/hooks/useServerStore.ts b/src/hooks/useServerStore.ts new file mode 100644 index 0000000..42338c9 --- /dev/null +++ b/src/hooks/useServerStore.ts @@ -0,0 +1,18 @@ +import { ServerStore } from '@/types' +import { create } from 'zustand' +import { persist, createJSONStorage } from 'zustand/middleware' + +export const useServerStore = create( + persist( + (set, get) => ({ + server: get()?.server, + serverGroup: get()?.serverGroup, + setServer: server => set({ server }), + setServerGroup: serverGroup => set({ serverGroup }), + }), + { + name: 'serverStore', + storage: createJSONStorage(() => localStorage), + }, + ), +) diff --git a/src/main.tsx b/src/main.tsx index f62dc3a..a5d5e50 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -17,6 +17,9 @@ import { AuthProvider } from './hooks/useAuth'; import { TerminalPage } from './components/terminal'; import DDNSPage from './routes/ddns'; import NATPage from './routes/nat'; +import ServerGroupPage from './routes/server-group'; +import NotificationGroupPage from './routes/notification-group'; +import { ServerProvider } from './hooks/useServer'; const router = createBrowserRouter([ { @@ -30,7 +33,7 @@ const router = createBrowserRouter([ }, { path: "/dashboard", - element: , + element: , }, { path: "/dashboard/service", @@ -44,6 +47,14 @@ const router = createBrowserRouter([ path: "/dashboard/nat", element: , }, + { + path: "/dashboard/server-group", + element: , + }, + { + path: "/dashboard/notification-group", + element: , + }, { path: "/dashboard/terminal/:id", element: , diff --git a/src/routes/notification-group.tsx b/src/routes/notification-group.tsx new file mode 100644 index 0000000..1260530 --- /dev/null +++ b/src/routes/notification-group.tsx @@ -0,0 +1,161 @@ +import { swrFetcher } from "@/api/api" +import { Checkbox } from "@/components/ui/checkbox" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" +import useSWR from "swr" +import { useEffect } from "react" +import { ActionButtonGroup } from "@/components/action-button-group" +import { HeaderButtonGroup } from "@/components/header-button-group" +import { Skeleton } from "@/components/ui/skeleton" +import { toast } from "sonner" +import { ModelNotificationGroupResponseItem } from "@/types" +import { deleteNotificationGroups } from "@/api/notification-group" +import { GroupTab } from "@/components/group-tab" +import { NotificationGroupCard } from "@/components/notification-group" + +export default function NotificationGroupPage() { + const { data, mutate, error, isLoading } = useSWR("/api/v1/notification-group", swrFetcher); + + useEffect(() => { + if (error) + toast("Error", { + description: `Error fetching resource: ${error.message}.`, + }) + }, [error]) + + const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + }, + { + header: "ID", + accessorKey: "id", + accessorFn: row => row.group.id, + }, + { + header: "Name", + accessorKey: "name", + cell: ({ row }) => { + const s = row.original; + return ( +
+ {s.group.name} +
+ ) + } + }, + { + header: "Notification methods (ID)", + accessorKey: "notifications", + accessorFn: row => row.notifications, + }, + { + id: "actions", + header: "Actions", + cell: ({ row }) => { + const s = row.original + return ( + + + + ) + }, + }, + ] + + const table = useReactTable({ + data: data ?? [], + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const selectedRows = table.getSelectedRowModel().rows; + + return ( +
+
+ + r.original.group.id), + mutate: mutate + }}> + + +
+ {isLoading ? ( +
+ + + +
+ ) : ( + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+ )} +
+ ) +} diff --git a/src/routes/server-group.tsx b/src/routes/server-group.tsx new file mode 100644 index 0000000..7a61632 --- /dev/null +++ b/src/routes/server-group.tsx @@ -0,0 +1,161 @@ +import { swrFetcher } from "@/api/api" +import { Checkbox } from "@/components/ui/checkbox" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" +import useSWR from "swr" +import { useEffect } from "react" +import { ActionButtonGroup } from "@/components/action-button-group" +import { HeaderButtonGroup } from "@/components/header-button-group" +import { Skeleton } from "@/components/ui/skeleton" +import { toast } from "sonner" +import { ModelServerGroupResponseItem } from "@/types" +import { deleteServerGroups } from "@/api/server-group" +import { GroupTab } from "@/components/group-tab" +import { ServerGroupCard } from "@/components/server-group" + +export default function ServerGroupPage() { + const { data, mutate, error, isLoading } = useSWR("/api/v1/server-group", swrFetcher); + + useEffect(() => { + if (error) + toast("Error", { + description: `Error fetching resource: ${error.message}.`, + }) + }, [error]) + + const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + }, + { + header: "ID", + accessorKey: "id", + accessorFn: row => row.group.id, + }, + { + header: "Name", + accessorKey: "name", + cell: ({ row }) => { + const s = row.original; + return ( +
+ {s.group.name} +
+ ) + } + }, + { + header: "Servers (ID)", + accessorKey: "servers", + accessorFn: row => row.servers, + }, + { + id: "actions", + header: "Actions", + cell: ({ row }) => { + const s = row.original + return ( + + + + ) + }, + }, + ] + + const table = useReactTable({ + data: data ?? [], + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const selectedRows = table.getSelectedRowModel().rows; + + return ( +
+
+ + r.original.group.id), + mutate: mutate + }}> + + +
+ {isLoading ? ( +
+ + + +
+ ) : ( + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+ )} +
+ ) +} diff --git a/src/routes/server.tsx b/src/routes/server.tsx index c30043b..3b51168 100644 --- a/src/routes/server.tsx +++ b/src/routes/server.tsx @@ -15,9 +15,11 @@ import { IconButton } from "@/components/xui/icon-button" import { InstallCommandsMenu } from "@/components/install-commands" import { NoteMenu } from "@/components/note-menu" import { TerminalButton } from "@/components/terminal" +import { useServer } from "@/hooks/useServer" export default function ServerPage() { const { data, mutate, error, isLoading } = useSWR('/api/v1/server', swrFetcher); + const { serverGroups } = useServer(); useEffect(() => { if (error) @@ -69,7 +71,11 @@ export default function ServerPage() { { header: "Groups", accessorKey: "groups", - accessorFn: row => "stub", + accessorFn: row => { + return serverGroups?.filter(sg => sg.servers.includes(row.id)) + .map(sg => sg.group.id) + || []; + }, }, { id: "ip", diff --git a/src/types/api.ts b/src/types/api.ts index fe28ee0..387fe67 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -117,15 +117,8 @@ export interface GithubComNaibaNezhaModelCommonResponseUint64 { success: boolean; } -export interface GormDeletedAt { - time?: string; - /** Valid is true if Time is not NULL */ - valid?: boolean; -} - export interface ModelAlertRule { created_at: string; - deleted_at: GormDeletedAt; enable: boolean; /** 失败时执行的触发任务id */ fail_trigger_tasks: number[]; @@ -201,7 +194,6 @@ export interface ModelCron { cover: number; created_at: string; cron_job_id: number; - deleted_at: GormDeletedAt; id: number; /** 最后一次执行时间 */ last_executed_at: string; @@ -273,7 +265,6 @@ export interface ModelDDNSProfile { access_id: string; access_secret: string; created_at: string; - deleted_at: GormDeletedAt; domains: string[]; enable_ipv4: boolean; enable_ipv6: boolean; @@ -337,7 +328,6 @@ export interface ModelLoginResponse { export interface ModelNAT { created_at: string; - deleted_at: GormDeletedAt; domain: string; host: string; id: number; @@ -356,7 +346,6 @@ export interface ModelNATForm { export interface ModelNotification { created_at: string; - deleted_at: GormDeletedAt; id: number; name: string; request_body: string; @@ -382,7 +371,6 @@ export interface ModelNotificationForm { export interface ModelNotificationGroup { created_at: string; - deleted_at: GormDeletedAt; id: number; name: string; updated_at: string; @@ -433,7 +421,6 @@ export interface ModelServer { created_at: string; /** DDNS配置 */ ddns_profiles: number[]; - deleted_at: GormDeletedAt; /** 展示排序,越大越靠前 */ display_index: number; /** 启用DDNS */ @@ -474,7 +461,6 @@ export interface ModelServerForm { export interface ModelServerGroup { created_at: string; - deleted_at: GormDeletedAt; id: number; name: string; updated_at: string; @@ -494,7 +480,6 @@ export interface ModelServerGroupResponseItem { export interface ModelService { cover: number; created_at: string; - deleted_at: GormDeletedAt; duration: number; enable_show_in_service: boolean; enable_trigger_task: boolean; @@ -601,7 +586,6 @@ export interface ModelTerminalForm { export interface ModelUser { created_at: string; - deleted_at: GormDeletedAt; id: number; password: string; updated_at: string; diff --git a/src/types/index.ts b/src/types/index.ts index e1babf3..3bb57bc 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -3,3 +3,5 @@ export * from './authContext'; export * from './api'; export * from './service'; export * from './ddns'; +export * from './serverStore'; +export * from './serverContext'; diff --git a/src/types/serverContext.ts b/src/types/serverContext.ts new file mode 100644 index 0000000..a619893 --- /dev/null +++ b/src/types/serverContext.ts @@ -0,0 +1,6 @@ +import { ModelServerGroupResponseItem, ModelServer } from "@/types"; + +export interface ServerContextProps { + servers?: ModelServer[]; + serverGroups?: ModelServerGroupResponseItem[]; +} diff --git a/src/types/serverStore.ts b/src/types/serverStore.ts new file mode 100644 index 0000000..9df3b40 --- /dev/null +++ b/src/types/serverStore.ts @@ -0,0 +1,8 @@ +import { ModelServer, ModelServerGroupResponseItem } from "@/types"; + +export interface ServerStore { + server?: ModelServer[]; + serverGroup?: ModelServerGroupResponseItem[]; + setServer: (server?: ModelServer[]) => void; + setServerGroup: (serverGroup?: ModelServerGroupResponseItem[]) => void; +}