diff --git a/.env.local b/.env.local index 87c293b3..f5993b0c 100644 --- a/.env.local +++ b/.env.local @@ -5,7 +5,7 @@ SAGITTARIUS_GRAPHQL_URL=http://localhost:80/graphql SAGITTARIUS_CABLE_URL=http://localhost:80/cable NEXT_PUBLIC_SCULPTOR_VERSION=0.0.0 -NEXT_PUBLIC_PICTOR_VERSION=0.11.1 +NEXT_PUBLIC_PICTOR_VERSION=0.14.0 NEXT_PUBLIC_ALLOWED_REDIRECT_DOMAINS=*.code0.tech,*.codezero.build NEXT_PUBLIC_OTEL_SERVICE_NAME=#"sculptor-client" diff --git a/next.config.ts b/next.config.ts index 6ebc416e..b6474de6 100644 --- a/next.config.ts +++ b/next.config.ts @@ -38,7 +38,10 @@ const nextConfig: NextConfig = { ] }, reactStrictMode: true, - reactCompiler: true, + // Enable React Compiler in production builds only. In dev it forces a + // per-file Babel pass (the compiler is a Babel plugin) which is the + // dominant cost of Turbopack dev compile time and memory here. + reactCompiler: process.env.NODE_ENV === "production", turbopack: { resolveAlias: { "@edition": path.resolve(__dirname, `src/packages/${EDITION}/src`), diff --git a/package-lock.json b/package-lock.json index 4218634f..4e450bd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "0.0.0", "dependencies": { "@apollo/client": "^4.0.9", - "@code0-tech/pictor": "^0.11.1", - "@code0-tech/triangulum": "^0.27.0", + "@code0-tech/pictor": "^0.14.0", + "@code0-tech/triangulum": "^0.27.1", "@codemirror/lang-javascript": "^6.2.5", "@codemirror/lint": "^6.9.5", "@icons-pack/react-simple-icons": "^13.13.0", @@ -28,6 +28,7 @@ "@uidotdev/usehooks": "^2.4.1", "@vercel/otel": "^2.1.1", "@xyflow/react": "^12.11.0", + "border-beam": "^1.3.0", "date-fns": "^4.1.0", "graphql": "^16.12.0", "graphql-ruby-client": "^1.15.1", @@ -39,6 +40,8 @@ "react": "19.2.6", "react-dom": "19.2.6", "react-hotkeys-hook": "^5.2.4", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1", "rxjs": "^7.8.2", "sass": "^1.93.3", "use-debounce": "^10.1.1" @@ -1895,9 +1898,9 @@ } }, "node_modules/@code0-tech/pictor": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/@code0-tech/pictor/-/pictor-0.11.1.tgz", - "integrity": "sha512-VDpvhdLU0MRNYWFlO0oGZphzh0OCo8D+W4l7rQWrB3aNOxUt/BIpiy36IWij+jesjwcxf1jHizKOiHIpTu8FJQ==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@code0-tech/pictor/-/pictor-0.14.0.tgz", + "integrity": "sha512-Io16vkTpDJtkJelWKKxyu6DxiCId5JtUTh38zuHEGukxCmBNJox9E4vIDEG8HyARASR0+QwMoXXdZbPE03r5Bg==", "peerDependencies": { "@ark-ui/react": "^5.36.2", "@codemirror/autocomplete": "^6.20.0", @@ -1906,12 +1909,13 @@ "@codemirror/lint": "^6.9.4", "@codemirror/state": "^6.5.4", "@codemirror/view": "^6.39.13", + "@icons-pack/react-simple-icons": "^13.13.0", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-hover-card": "^1.1.15", - "@radix-ui/react-one-time-password-field": "^0.1.8", + "@radix-ui/react-one-time-password-field": "^0.1.13", "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-scroll-area": "^1.2.10", @@ -1946,9 +1950,9 @@ "peer": true }, "node_modules/@code0-tech/triangulum": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@code0-tech/triangulum/-/triangulum-0.27.0.tgz", - "integrity": "sha512-eierLMovuDIq+l9lfVD+P9xHgT0YCgxRWxvhUDYOVLAkdzzIcH7wsRa0dKXBgOaVdGyWC4glFe7m7nzeQafWLg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@code0-tech/triangulum/-/triangulum-0.27.1.tgz", + "integrity": "sha512-qHN4FgbOfmIOdC0EGWZJ+/B+Y8XEiGNaT+vyio39Xi4/ihkiFMpRe2zW/zT1pJzjIkPbcQH/ix50bqIvhOdZCA==", "peerDependencies": { "@code0-tech/sagittarius-graphql-types": "0.0.0-experimental-2668386432-315fff7e9283580109793944cb24db116d9cc264", "@typescript/vfs": "^1.6.4", @@ -2363,6 +2367,7 @@ "resolved": "https://registry.npmjs.org/@icons-pack/react-simple-icons/-/react-simple-icons-13.13.0.tgz", "integrity": "sha512-B5HhQMIpcSH4z8IZ8HFhD59CboHceKYMpPC9kAwGyKntvPdyJJv26DLu4Z1wAjcCLyrJhf11tMhiQGom9Rxb9g==", "license": "MIT", + "peer": true, "engines": { "node": ">=24", "pnpm": ">=10" @@ -4363,24 +4368,180 @@ } }, "node_modules/@radix-ui/react-one-time-password-field": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", - "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.13.tgz", + "integrity": "sha512-reLtbZtEBsMcqXkjd/wOga4e8t9uxzFHdX9W/j/ZfGznTNJxLGjRrDNGnGOOWcBazMH1BI/b7Cx+hblSWSD7aw==", "license": "MIT", - "peer": true, "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-is-hydrated": "0.1.0", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/number": "1.1.2", + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-collection": "1.1.12", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-roving-focus": "1.1.16", + "@radix-ui/react-use-controllable-state": "1.2.4", + "@radix-ui/react-use-effect-event": "0.0.3", + "@radix-ui/react-use-is-hydrated": "0.1.1", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "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-one-time-password-field/node_modules/@radix-ui/number": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.2.tgz", + "integrity": "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-one-time-password-field/node_modules/@radix-ui/primitive": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.6.tgz", + "integrity": "sha512-w9hl+724uYEgCGR3bhuRepjBtrNB/6gkhCnAf58Ke+SLbHPPQqVZZB59z60roB+5H+nh3nWTcdJhQdFMEydWmw==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-one-time-password-field/node_modules/@radix-ui/react-collection": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.12.tgz", + "integrity": "sha512-nb67INpE0IahJKN7EYPp9m9YGwYeKlnzxT3MwXVkgCskaSJia97kG4T0ywpjNUSSnoJk/uvk12V8vbrEHEj+/Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-slot": "1.3.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-one-time-password-field/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz", + "integrity": "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field/node_modules/@radix-ui/react-context": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.0.tgz", + "integrity": "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field/node_modules/@radix-ui/react-direction": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.2.tgz", + "integrity": "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field/node_modules/@radix-ui/react-id": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.2.tgz", + "integrity": "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field/node_modules/@radix-ui/react-primitive": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.7.tgz", + "integrity": "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.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-one-time-password-field/node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.16.tgz", + "integrity": "sha512-w7lLsTSd3940vFYEshKkHw+NGf7H0QDJPHYsy8NRjDCVbO6ZdKW1X/xoJSYHZtttnrdZiYqbN2O/2uHGB0zasw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-collection": "1.1.12", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.2.0", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.4", + "@radix-ui/react-use-is-hydrated": "0.1.1", + "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", @@ -4397,6 +4558,92 @@ } } }, + "node_modules/@radix-ui/react-one-time-password-field/node_modules/@radix-ui/react-slot": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz", + "integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz", + "integrity": "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.4.tgz", + "integrity": "sha512-cx2DixxmSfjCcEoRvDvy1NLd6SWK94XFcEEOZUcharUlXbmahFQGKCfwdKZL2ub34iIwOPOEFVF80xb+yfLYiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.6", + "@radix-ui/react-use-effect-event": "0.0.3", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field/node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.3.tgz", + "integrity": "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz", + "integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", @@ -4930,13 +5177,10 @@ } }, "node_modules/@radix-ui/react-use-is-hydrated": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", - "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.1.tgz", + "integrity": "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==", "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.5.0" - }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -5423,6 +5667,15 @@ "@types/d3-selection": "*" } }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -5434,9 +5687,26 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -5458,6 +5728,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.13.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", @@ -5478,7 +5763,6 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -5496,6 +5780,12 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.52.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.52.0.tgz", @@ -5881,6 +6171,12 @@ "react-dom": ">=18.0.0" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "license": "ISC" + }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", @@ -7558,6 +7854,16 @@ "@babel/types": "^7.26.0" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -7584,7 +7890,17 @@ "dev": true, "license": "ISC" }, - "node_modules/brace-expansion": { + "node_modules/border-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/border-beam/-/border-beam-1.3.0.tgz", + "integrity": "sha512-oZSISp3aQau4ASTmzm5pzxeBinO6Fh2HW0gyEixJgtpUZvRiU0vNpP+IwGRrNpIyqPjA7Rf5EBnVqDSJ49cm5w==", + "license": "MIT", + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", @@ -7736,6 +8052,56 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -7801,6 +8167,16 @@ "@codemirror/view": "^6.0.0" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -8174,6 +8550,19 @@ } } }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -8227,6 +8616,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", @@ -8246,6 +8644,19 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/direction": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/direction/-/direction-1.0.4.tgz", @@ -9063,6 +9474,16 @@ "node": ">=4.0" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -9073,6 +9494,12 @@ "node": ">=0.10.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -9628,6 +10055,46 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -9645,6 +10112,16 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -9703,6 +10180,12 @@ "node": ">=0.8.19" } }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -9718,6 +10201,30 @@ "node": ">= 0.4" } }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -9883,6 +10390,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -9942,6 +10459,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-hotkey": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.2.0.tgz", @@ -10001,6 +10528,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", @@ -10380,6 +10919,16 @@ "dev": true, "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -10420,6 +10969,16 @@ "yallist": "^3.0.2" } }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -10430,94 +10989,939 @@ "node": ">= 0.4" } }, - "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/merge-props": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/merge-props/-/merge-props-6.0.0.tgz", - "integrity": "sha512-ORZFZMGKE5PuAi7YfVCfPz3jiS9V0t2XXE2AGYiwMrcudRuj0hkXKEzsl17pUF07r+Digf9YlTzteX2LFE6vAQ==", + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", "license": "MIT", - "peer": true + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "license": "MIT", "engines": { - "node": ">= 8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "devOptional": true, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", "license": "MIT", "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" }, - "engines": { - "node": ">=8.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, - "engines": { - "node": "*" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/module-details-from-path": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", - "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", - "license": "MIT" + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, - "node_modules/motion": { - "version": "12.40.0", - "resolved": "https://registry.npmjs.org/motion/-/motion-12.40.0.tgz", - "integrity": "sha512-yjrHUrBFW6kQvjJwRsoiPSAhC5tRwRqNGJWmiJ4CrGnbKp0V88AdzkhBmDoqIsIPfarOe0Uddd37Xq43/gIocA==", + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", "license": "MIT", - "peer": true, "dependencies": { - "framer-motion": "^12.40.0", - "tslib": "^2.4.0" + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/merge-props": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/merge-props/-/merge-props-6.0.0.tgz", + "integrity": "sha512-ORZFZMGKE5PuAi7YfVCfPz3jiS9V0t2XXE2AGYiwMrcudRuj0hkXKEzsl17pUF07r+Digf9YlTzteX2LFE6vAQ==", + "license": "MIT", + "peer": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" + }, + "node_modules/motion": { + "version": "12.40.0", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.40.0.tgz", + "integrity": "sha512-yjrHUrBFW6kQvjJwRsoiPSAhC5tRwRqNGJWmiJ4CrGnbKp0V88AdzkhBmDoqIsIPfarOe0Uddd37Xq43/gIocA==", + "license": "MIT", + "peer": true, + "dependencies": { + "framer-motion": "^12.40.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { @@ -10901,6 +12305,31 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -11082,6 +12511,16 @@ "react-is": "^16.13.1" } }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/proxy-compare": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.1.tgz", @@ -11171,6 +12610,33 @@ "dev": true, "license": "MIT" }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/react-remove-scroll": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", @@ -11381,6 +12847,72 @@ "regjsparser": "bin/parser" } }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/require-in-the-middle": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", @@ -11898,6 +13430,16 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -12032,6 +13574,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -12048,6 +13604,24 @@ "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", "license": "MIT" }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -12185,6 +13759,26 @@ "node": ">=8.0" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", @@ -12429,6 +14023,93 @@ "node": ">=4" } }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unrs-resolver": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", @@ -12575,6 +14256,34 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/w3c-keyname": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", @@ -12775,6 +14484,16 @@ "optional": true } } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index 4d58dc8e..13d52b31 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ }, "dependencies": { "@apollo/client": "^4.0.9", - "@code0-tech/pictor": "^0.11.1", - "@code0-tech/triangulum": "^0.27.0", + "@code0-tech/pictor": "^0.14.0", + "@code0-tech/triangulum": "^0.27.1", "@codemirror/lang-javascript": "^6.2.5", "@codemirror/lint": "^6.9.5", "@icons-pack/react-simple-icons": "^13.13.0", @@ -34,6 +34,7 @@ "@uidotdev/usehooks": "^2.4.1", "@vercel/otel": "^2.1.1", "@xyflow/react": "^12.11.0", + "border-beam": "^1.3.0", "date-fns": "^4.1.0", "graphql": "^16.12.0", "graphql-ruby-client": "^1.15.1", @@ -45,6 +46,8 @@ "react": "19.2.6", "react-dom": "19.2.6", "react-hotkeys-hook": "^5.2.4", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1", "rxjs": "^7.8.2", "sass": "^1.93.3", "use-debounce": "^10.1.1" diff --git a/src/app/(dashboard)/@bar/default.tsx b/src/app/(dashboard)/@bar/default.tsx deleted file mode 100644 index 2b1a1ac3..00000000 --- a/src/app/(dashboard)/@bar/default.tsx +++ /dev/null @@ -1,5 +0,0 @@ -"use client"; - -import {ApplicationBarView} from "@edition/application/views/ApplicationBarView"; - -export default ApplicationBarView diff --git a/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/members/add/page.tsx b/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/members/add/page.tsx new file mode 100644 index 00000000..5d3ecb1a --- /dev/null +++ b/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/members/add/page.tsx @@ -0,0 +1,3 @@ +import {MemberAddPage} from "@edition/member/pages/MemberAddPage"; + +export default MemberAddPage diff --git a/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/project/[projectId]/module/[moduleId]/page.tsx b/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/project/[projectId]/module/[moduleId]/page.tsx new file mode 100644 index 00000000..4bb6a603 --- /dev/null +++ b/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/project/[projectId]/module/[moduleId]/page.tsx @@ -0,0 +1,3 @@ +import {ModuleConfigurationPage} from "@ce-internal/module/pages/ModuleConfigurationPage"; + +export default ModuleConfigurationPage diff --git a/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/project/[projectId]/settings/page.tsx b/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/project/[projectId]/settings/page.tsx new file mode 100644 index 00000000..a1b0b501 --- /dev/null +++ b/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/project/[projectId]/settings/page.tsx @@ -0,0 +1,3 @@ +import {ProjectSettingsPage} from "@edition/project/pages/ProjectSettingsPage"; + +export default ProjectSettingsPage diff --git a/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/project/create/page.tsx b/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/project/create/page.tsx new file mode 100644 index 00000000..5c51667a --- /dev/null +++ b/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/project/create/page.tsx @@ -0,0 +1,3 @@ +import {NamespaceProjectsCreatePage} from "@edition/namespace/pages/NamespaceProjectsCreatePage"; + +export default NamespaceProjectsCreatePage diff --git a/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/roles/[roleId]/settings/page.tsx b/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/roles/[roleId]/settings/page.tsx new file mode 100644 index 00000000..33950d46 --- /dev/null +++ b/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/roles/[roleId]/settings/page.tsx @@ -0,0 +1,3 @@ +import {RoleSettingsPage} from "@edition/role/pages/RoleSettingsPage"; + +export default RoleSettingsPage diff --git a/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/roles/create/page.tsx b/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/roles/create/page.tsx new file mode 100644 index 00000000..2c73f731 --- /dev/null +++ b/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/roles/create/page.tsx @@ -0,0 +1,3 @@ +import {RoleCreatePage} from "@edition/role/pages/RoleCreatePage"; + +export default RoleCreatePage diff --git a/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/runtimes/[runtimeId]/settings/page.tsx b/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/runtimes/[runtimeId]/settings/page.tsx new file mode 100644 index 00000000..604ea5b6 --- /dev/null +++ b/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/runtimes/[runtimeId]/settings/page.tsx @@ -0,0 +1,3 @@ +import {RuntimeSettingsPage} from "@edition/runtime/pages/RuntimeSettingsPage"; + +export default RuntimeSettingsPage diff --git a/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/runtimes/create/page.tsx b/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/runtimes/create/page.tsx new file mode 100644 index 00000000..5a060d60 --- /dev/null +++ b/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/runtimes/create/page.tsx @@ -0,0 +1,3 @@ +import {RuntimeCreatePage} from "@edition/runtime/pages/RuntimeCreatePage"; + +export default RuntimeCreatePage diff --git a/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/settings/page.tsx b/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/settings/page.tsx new file mode 100644 index 00000000..4dca2828 --- /dev/null +++ b/src/app/(dashboard)/@modal/(.)namespace/[namespaceId]/settings/page.tsx @@ -0,0 +1,3 @@ +import {NamespaceSettingsPage} from "@edition/namespace/pages/NamespaceSettingsPage"; + +export default NamespaceSettingsPage diff --git a/src/app/(dashboard)/@modal/(.)runtimes/[runtimeId]/settings/page.tsx b/src/app/(dashboard)/@modal/(.)runtimes/[runtimeId]/settings/page.tsx new file mode 100644 index 00000000..604ea5b6 --- /dev/null +++ b/src/app/(dashboard)/@modal/(.)runtimes/[runtimeId]/settings/page.tsx @@ -0,0 +1,3 @@ +import {RuntimeSettingsPage} from "@edition/runtime/pages/RuntimeSettingsPage"; + +export default RuntimeSettingsPage diff --git a/src/app/(dashboard)/@modal/(.)runtimes/create/page.tsx b/src/app/(dashboard)/@modal/(.)runtimes/create/page.tsx new file mode 100644 index 00000000..5a060d60 --- /dev/null +++ b/src/app/(dashboard)/@modal/(.)runtimes/create/page.tsx @@ -0,0 +1,3 @@ +import {RuntimeCreatePage} from "@edition/runtime/pages/RuntimeCreatePage"; + +export default RuntimeCreatePage diff --git a/src/app/(dashboard)/@modal/(.)settings/page.tsx b/src/app/(dashboard)/@modal/(.)settings/page.tsx new file mode 100644 index 00000000..cd72c1e8 --- /dev/null +++ b/src/app/(dashboard)/@modal/(.)settings/page.tsx @@ -0,0 +1,3 @@ +import {ApplicationSettingsPage} from "@edition/application/pages/ApplicationSettingsPage"; + +export default ApplicationSettingsPage diff --git a/src/app/(dashboard)/@modal/(.)users/invite/page.tsx b/src/app/(dashboard)/@modal/(.)users/invite/page.tsx new file mode 100644 index 00000000..048cb028 --- /dev/null +++ b/src/app/(dashboard)/@modal/(.)users/invite/page.tsx @@ -0,0 +1,3 @@ +import {UserInvitePage} from "@edition/user/pages/UserInvitePage"; + +export default UserInvitePage diff --git a/src/app/(dashboard)/@modal/(.)workspaces/create/page.tsx b/src/app/(dashboard)/@modal/(.)workspaces/create/page.tsx new file mode 100644 index 00000000..63404eec --- /dev/null +++ b/src/app/(dashboard)/@modal/(.)workspaces/create/page.tsx @@ -0,0 +1,3 @@ +import {OrganizationCreatePage} from "@edition/organization/pages/OrganizationCreatePage"; + +export default OrganizationCreatePage diff --git a/src/app/(dashboard)/@sidebar/default.tsx b/src/app/(dashboard)/@sidebar/default.tsx new file mode 100644 index 00000000..a3f41b48 --- /dev/null +++ b/src/app/(dashboard)/@sidebar/default.tsx @@ -0,0 +1,3 @@ +export default () => { + return null +} \ No newline at end of file diff --git a/src/app/(dashboard)/@sidebar/namespace/[namespaceId]/page.tsx b/src/app/(dashboard)/@sidebar/namespace/[namespaceId]/page.tsx new file mode 100644 index 00000000..e272971f --- /dev/null +++ b/src/app/(dashboard)/@sidebar/namespace/[namespaceId]/page.tsx @@ -0,0 +1,17 @@ +"use client" + +import {ProjectListView} from "@edition/project/views/ProjectListView"; +import {FlowRecentListView} from "@edition/flow/views/FlowRecentListView"; +import {NamespaceMenuView} from "@edition/namespace/views/NamespaceMenuView"; +import {Flex} from "@code0-tech/pictor"; + +export default () => { + return + +
+ +
+ +
+ +} diff --git a/src/app/(dashboard)/@sidebar/namespace/[namespaceId]/project/[projectId]/default.tsx b/src/app/(dashboard)/@sidebar/namespace/[namespaceId]/project/[projectId]/default.tsx new file mode 100644 index 00000000..835a914d --- /dev/null +++ b/src/app/(dashboard)/@sidebar/namespace/[namespaceId]/project/[projectId]/default.tsx @@ -0,0 +1,14 @@ +"use client" + +import {Flex} from "@code0-tech/pictor"; +import {FlowFolderView} from "@edition/flow/views/FlowFolderView"; +import {ProjectMenuView} from "@edition/project/views/ProjectMenuView"; + +export default () => { + return + +
+ +
+
+} diff --git a/src/app/(dashboard)/@sidebar/page.tsx b/src/app/(dashboard)/@sidebar/page.tsx new file mode 100644 index 00000000..721aac38 --- /dev/null +++ b/src/app/(dashboard)/@sidebar/page.tsx @@ -0,0 +1,13 @@ +"use client" + +import {Flex} from "@code0-tech/pictor"; +import {NamespaceListView} from "@edition/namespace/views/NamespaceListView"; +import {ProjectRecentListView} from "@edition/project/views/ProjectRecentListView"; + +export default () => { + return + +
+ + +} \ No newline at end of file diff --git a/src/app/(dashboard)/@sidebar/users/[userId]/page.tsx b/src/app/(dashboard)/@sidebar/users/[userId]/page.tsx new file mode 100644 index 00000000..6fed6730 --- /dev/null +++ b/src/app/(dashboard)/@sidebar/users/[userId]/page.tsx @@ -0,0 +1,5 @@ +"use client" + +import {UserProfileView} from "@edition/user/views/UserProfileView"; + +export default UserProfileView diff --git a/src/app/(dashboard)/@tab/default.tsx b/src/app/(dashboard)/@tab/default.tsx deleted file mode 100644 index 2edf7dc7..00000000 --- a/src/app/(dashboard)/@tab/default.tsx +++ /dev/null @@ -1,5 +0,0 @@ -"use client" - -import {ApplicationTabView} from "@edition/application/views/ApplicationTabView"; - -export default ApplicationTabView \ No newline at end of file diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index c740179c..812b2dce 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -2,10 +2,10 @@ import React from "react"; import {useApolloClient} from "@apollo/client/react"; -import {AuroraBackground, ContextStoreProvider, Flex} from "@code0-tech/pictor"; +import {ContextStoreProvider} from "@code0-tech/pictor"; import {UserService} from "@edition/user/services/User.service"; import {GraphqlClient} from "@core/util/graphql-client"; -import {usePathname} from "next/navigation"; +import {useParams, usePathname} from "next/navigation"; import {OrganizationService} from "@edition/organization/services/Organization.service"; import {MemberService} from "@edition/member/services/Member.service"; import {NamespaceService} from "@edition/namespace/services/Namespace.service"; @@ -13,30 +13,53 @@ import {usePersistentReactiveArrayService} from "@/hooks/usePersistentReactiveAr import {RuntimeService} from "@edition/runtime/services/Runtime.service"; import {ProjectService} from "@edition/project/services/Project.service"; import {RoleService} from "@edition/role/services/Role.service"; -import Image from "next/image"; import {Application, ApplicationService} from "@edition/application/services/Application.service"; import {useUserSession} from "@edition/user/hooks/User.session.hook"; import {OrganizationView} from "@edition/organization/services/Organization.view"; import {RoleView} from "@edition/role/services/Role.view"; import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout"; -import {Namespace, NamespaceMember, NamespaceProject, Runtime, User} from "@code0-tech/sagittarius-graphql-types"; +import { + DataType, + Flow, FlowType, + FunctionDefinition, + Namespace, + NamespaceMember, + NamespaceProject, + Runtime, RuntimeModule, + User +} from "@code0-tech/sagittarius-graphql-types"; import {ApplicationMiddlewareComponent} from "@edition/application/components/ApplicationMiddlewareComponent"; +import {FullScreen} from "@code0-tech/pictor/dist/components/fullscreen/FullScreen"; +import {ApplicationNavigationView} from "@edition/application/views/ApplicationNavigationView"; +import {FlowService} from "@edition/flow/services/Flow.service"; +import {FunctionService} from "@edition/function/services/Function.service"; +import {DatatypeService} from "@edition/datatype/services/Datatype.service"; +import {FlowTypeService} from "@edition/flowtype/services/FlowType.service"; +import {ModuleService} from "@edition/module/services/Module.service"; +import {AIService, Model} from "@edition/ai/services/AI.service"; interface ApplicationLayoutProps { children: React.ReactNode - bar: React.ReactNode - tab: React.ReactNode + sidebar: React.ReactNode modal: React.ReactNode } -const ApplicationLayout: React.FC = ({children, bar, tab, modal}) => { +const ApplicationLayout: React.FC = ({children, sidebar, modal}) => { const client = useApolloClient() const pathname = usePathname() + const params = useParams() const currentSession = useUserSession() const graphqlClient = React.useMemo(() => new GraphqlClient(client), [client]) + const namespaceIndex = params.namespaceId as any as number + const projectIndex = params.projectId as any as number + const flowIndex = params.flowId as any as number + const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` + const projectId: NamespaceProject['id'] = `gid://sagittarius/NamespaceProject/${projectIndex}` + + const application = usePersistentReactiveArrayService(`dashboard::application::${currentSession?.id}`, (store) => new ApplicationService(graphqlClient, store)) const user = usePersistentReactiveArrayService(`dashboard::users::${currentSession?.id}`, (store) => new UserService(graphqlClient, store)) const organization = usePersistentReactiveArrayService(`dashboard::organizations::${currentSession?.id}`, (store) => new OrganizationService(graphqlClient, store)) const member = usePersistentReactiveArrayService(`dashboard::members::${currentSession?.id}`, (store) => new MemberService(graphqlClient, store)) @@ -44,54 +67,44 @@ const ApplicationLayout: React.FC = ({children, bar, tab const runtime = usePersistentReactiveArrayService(`dashboard::global_runtimes::${currentSession?.id}`, (store) => new RuntimeService(graphqlClient, store)) const project = usePersistentReactiveArrayService(`dashboard::projects::${currentSession?.id}`, (store) => new ProjectService(graphqlClient, store)) const role = usePersistentReactiveArrayService(`dashboard::roles::${currentSession?.id}`, (store) => new RoleService(graphqlClient, store)) - const application = usePersistentReactiveArrayService(`dashboard::application::${currentSession?.id}`, (store) => new ApplicationService(graphqlClient, store)) + const flow = usePersistentReactiveArrayService(`dashboard::flows::${currentSession?.id}`, (store) => new FlowService(graphqlClient, store)) + const functions = usePersistentReactiveArrayService(`dashboard::functions::${currentSession?.id}`, (store) => new FunctionService(graphqlClient, store)) + const datatype = usePersistentReactiveArrayService(`dashboard::datatypes::${currentSession?.id}`, (store) => new DatatypeService(graphqlClient, store)) + const flowtype = usePersistentReactiveArrayService(`dashboard::flowtypes::${currentSession?.id}`, (store) => new FlowTypeService(graphqlClient, store)) + const module = usePersistentReactiveArrayService(`dashboard::modules::${currentSession?.id}`, (store) => new ModuleService(graphqlClient, store)) + const ai = usePersistentReactiveArrayService(`dashboard::ai::${currentSession?.id}`, (store) => new AIService(graphqlClient, store)) + + const runtimeId = React.useMemo(() => project[1].getById(projectId, {namespaceId})?.primaryRuntime?.id, [projectId, project[0], namespaceId]) + + React.useEffect(() => { + if (!currentSession || !namespaceId || !projectId) return + + flow[1].values({namespaceId, projectId}) + functions[1].values({namespaceId, projectId, runtimeId}) + datatype[1].values({namespaceId, projectId, runtimeId}) + flowtype[1].values({namespaceId, projectId, runtimeId}) + module[1].values({namespaceId, projectId, runtimeId}) + }, [runtimeId, namespaceId, projectId, currentSession, flow, functions, datatype, flowtype]) const isAddLicensePage = pathname === '/licenses/add'; return + services={[application, user, organization, member, namespace, runtime, project, role, flow, functions, datatype, flowtype, module, ai]}> - - {isAddLicensePage ? ( - - ) : ( -
-
- - -
- )} - {"CodeZero - {tab} - - }> - {bar}}> - <> - - <>{children} - - {modal} - + + }> + {sidebar}
}> + <> + {children} + {modal} + +
- +
} diff --git a/src/app/(dashboard)/namespace/[namespaceId]/members/page.tsx b/src/app/(dashboard)/namespace/[namespaceId]/members/page.tsx deleted file mode 100644 index 3e37f24d..00000000 --- a/src/app/(dashboard)/namespace/[namespaceId]/members/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -"use client" - -import {MembersPage} from "@edition/member/pages/MembersPage"; - -export default function Page() { - return -} \ No newline at end of file diff --git a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx b/src/app/(dashboard)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx similarity index 82% rename from src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx rename to src/app/(dashboard)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx index ef7f3e7d..e4fde369 100644 --- a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx +++ b/src/app/(dashboard)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx @@ -2,7 +2,7 @@ import {Button, Flex,} from "@code0-tech/pictor"; import React from "react"; -import {IconFile, IconPlayerPlay} from "@tabler/icons-react"; +import {IconPencilFilled, IconPlayerPlayFilled} from "@tabler/icons-react"; import {useParams} from "next/navigation"; import {Flow, Namespace, NamespaceProject} from "@code0-tech/sagittarius-graphql-types"; import {FlowBuilderComponent} from "@edition/flow/components/builder/FlowBuilderComponent"; @@ -61,18 +61,18 @@ export default function Page() { return - + }> @@ -80,12 +80,8 @@ export default function Page() { + style={{borderRadius: "1rem"}} + > { @@ -93,7 +89,7 @@ export default function Page() { <> + style={{borderRadius: "1rem"}}> @@ -105,7 +101,7 @@ export default function Page() { <> + style={{borderRadius: "1rem"}}> diff --git a/src/app/(dashboard)/namespace/[namespaceId]/project/[projectId]/layout.tsx b/src/app/(dashboard)/namespace/[namespaceId]/project/[projectId]/layout.tsx new file mode 100644 index 00000000..2823590b --- /dev/null +++ b/src/app/(dashboard)/namespace/[namespaceId]/project/[projectId]/layout.tsx @@ -0,0 +1,6 @@ +"use client" + +import {ProjectLayout} from "@edition/project/layout/ProjectLayout"; +import '@xyflow/react/dist/style.css'; + +export default ProjectLayout diff --git a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/module/[moduleId]/page.tsx b/src/app/(dashboard)/namespace/[namespaceId]/project/[projectId]/module/[moduleId]/page.tsx similarity index 100% rename from src/app/(flow)/namespace/[namespaceId]/project/[projectId]/module/[moduleId]/page.tsx rename to src/app/(dashboard)/namespace/[namespaceId]/project/[projectId]/module/[moduleId]/page.tsx diff --git a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/module/page.tsx b/src/app/(dashboard)/namespace/[namespaceId]/project/[projectId]/module/page.tsx similarity index 100% rename from src/app/(flow)/namespace/[namespaceId]/project/[projectId]/module/page.tsx rename to src/app/(dashboard)/namespace/[namespaceId]/project/[projectId]/module/page.tsx diff --git a/src/app/(dashboard)/namespace/[namespaceId]/project/[projectId]/page.tsx b/src/app/(dashboard)/namespace/[namespaceId]/project/[projectId]/page.tsx new file mode 100644 index 00000000..4013fd22 --- /dev/null +++ b/src/app/(dashboard)/namespace/[namespaceId]/project/[projectId]/page.tsx @@ -0,0 +1,5 @@ +"use client" + +import {ProjectOverviewPage} from "@edition/project/pages/ProjectOverviewPage"; + +export default ProjectOverviewPage \ No newline at end of file diff --git a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/settings/page.tsx b/src/app/(dashboard)/namespace/[namespaceId]/project/[projectId]/settings/page.tsx similarity index 100% rename from src/app/(flow)/namespace/[namespaceId]/project/[projectId]/settings/page.tsx rename to src/app/(dashboard)/namespace/[namespaceId]/project/[projectId]/settings/page.tsx diff --git a/src/app/(dashboard)/namespace/[namespaceId]/projects/create/page.tsx b/src/app/(dashboard)/namespace/[namespaceId]/project/create/page.tsx similarity index 91% rename from src/app/(dashboard)/namespace/[namespaceId]/projects/create/page.tsx rename to src/app/(dashboard)/namespace/[namespaceId]/project/create/page.tsx index 9b9fff72..ca51c4f5 100644 --- a/src/app/(dashboard)/namespace/[namespaceId]/projects/create/page.tsx +++ b/src/app/(dashboard)/namespace/[namespaceId]/project/create/page.tsx @@ -1,5 +1,7 @@ +"use client" + import {NamespaceProjectsCreatePage} from "@edition/namespace/pages/NamespaceProjectsCreatePage"; export default function Page() { return -} \ No newline at end of file +} diff --git a/src/app/(dashboard)/namespace/[namespaceId]/roles/page.tsx b/src/app/(dashboard)/namespace/[namespaceId]/roles/page.tsx deleted file mode 100644 index e703a14b..00000000 --- a/src/app/(dashboard)/namespace/[namespaceId]/roles/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import {RolesPage} from "@edition/role/pages/RolesPage"; - -export default function Page() { - return
- -
-} \ No newline at end of file diff --git a/src/app/(dashboard)/namespace/[namespaceId]/runtimes/page.tsx b/src/app/(dashboard)/namespace/[namespaceId]/runtimes/page.tsx deleted file mode 100644 index 27fcc879..00000000 --- a/src/app/(dashboard)/namespace/[namespaceId]/runtimes/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import {RuntimesPage} from "@edition/runtime/pages/RuntimesPage"; - -export default function Page() { - return -} \ No newline at end of file diff --git a/src/app/(dashboard)/runtimes/[runtimeId]/settings/page.tsx b/src/app/(dashboard)/runtimes/[runtimeId]/settings/page.tsx index 1b30dce7..8245f121 100644 --- a/src/app/(dashboard)/runtimes/[runtimeId]/settings/page.tsx +++ b/src/app/(dashboard)/runtimes/[runtimeId]/settings/page.tsx @@ -1,5 +1,3 @@ import {RuntimeSettingsPage} from "@edition/runtime/pages/RuntimeSettingsPage"; -export default function Page() { - return -} \ No newline at end of file +export default RuntimeSettingsPage \ No newline at end of file diff --git a/src/app/(dashboard)/runtimes/create/page.tsx b/src/app/(dashboard)/runtimes/create/page.tsx index e149b9e1..3cbd7747 100644 --- a/src/app/(dashboard)/runtimes/create/page.tsx +++ b/src/app/(dashboard)/runtimes/create/page.tsx @@ -1,5 +1,3 @@ import {RuntimeCreatePage} from "@edition/runtime/pages/RuntimeCreatePage"; -export default function Page() { - return -} \ No newline at end of file +export default RuntimeCreatePage \ No newline at end of file diff --git a/src/app/(dashboard)/runtimes/page.tsx b/src/app/(dashboard)/runtimes/page.tsx deleted file mode 100644 index b0f607f2..00000000 --- a/src/app/(dashboard)/runtimes/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -"use client" - -import {RuntimesPage} from "@edition/runtime/pages/RuntimesPage"; - -export default function Page() { - return -} diff --git a/src/app/(dashboard)/users/page.tsx b/src/app/(dashboard)/users/page.tsx deleted file mode 100644 index 57051d43..00000000 --- a/src/app/(dashboard)/users/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import {UsersPage} from "@edition/user/pages/UsersPage"; - -export default function Page() { - return -} diff --git a/src/app/(dashboard)/workspaces/page.tsx b/src/app/(dashboard)/workspaces/page.tsx deleted file mode 100644 index 9b5c4961..00000000 --- a/src/app/(dashboard)/workspaces/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -"use client" - -import {NamespacesPage} from "@edition/namespace/pages/NamespacesPage"; - -export default NamespacesPage diff --git a/src/app/(flow)/@bar/default.tsx b/src/app/(flow)/@bar/default.tsx deleted file mode 100644 index 2722f04c..00000000 --- a/src/app/(flow)/@bar/default.tsx +++ /dev/null @@ -1,5 +0,0 @@ -"use client"; - -import {ApplicationBarView} from "@edition/application/views/ApplicationBarView"; - -export default ApplicationBarView \ No newline at end of file diff --git a/src/app/(flow)/@tab/default.tsx b/src/app/(flow)/@tab/default.tsx deleted file mode 100644 index 0e93b0db..00000000 --- a/src/app/(flow)/@tab/default.tsx +++ /dev/null @@ -1,5 +0,0 @@ -"use client" - -import {ApplicationTabView} from "@edition/application/views/ApplicationTabView"; - -export default ApplicationTabView diff --git a/src/app/(flow)/layout.tsx b/src/app/(flow)/layout.tsx deleted file mode 100644 index 02d9e00d..00000000 --- a/src/app/(flow)/layout.tsx +++ /dev/null @@ -1,123 +0,0 @@ -"use client" - -import {useApolloClient} from "@apollo/client/react"; -import {useParams, useRouter} from "next/navigation"; -import {AuroraBackground, ContextStoreProvider, Flex,} from "@code0-tech/pictor"; -import React from "react"; -import {GraphqlClient} from "@core/util/graphql-client"; -import { - DataType, - Flow, - FlowType, - FunctionDefinition, - Namespace, NamespaceMember, - NamespaceProject, - Runtime, RuntimeModule, User -} from "@code0-tech/sagittarius-graphql-types"; -import {usePersistentReactiveArrayService} from "@/hooks/usePersistentReactiveArrayService"; -import {UserService} from "@edition/user/services/User.service"; -import {OrganizationService} from "@edition/organization/services/Organization.service"; -import {MemberService} from "@edition/member/services/Member.service"; -import {NamespaceService} from "@edition/namespace/services/Namespace.service"; -import {RuntimeService} from "@edition/runtime/services/Runtime.service"; -import {ProjectService} from "@edition/project/services/Project.service"; -import {RoleService} from "@edition/role/services/Role.service"; -import {FlowService} from "@edition/flow/services/Flow.service"; -import {FunctionService} from "@edition/function/services/Function.service"; -import {DatatypeService} from "@edition/datatype/services/Datatype.service"; -import {FlowTypeService} from "@edition/flowtype/services/FlowType.service"; -import Image from "next/image"; -import {OrganizationView} from "@edition/organization/services/Organization.view"; -import {RoleView} from "@edition/role/services/Role.view"; -import {useUserSession} from "@edition/user/hooks/User.session.hook"; -import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout"; -import {ModuleService} from "@edition/module/services/Module.service"; -import {AIService, Model} from "@edition/ai/services/AI.service"; - -export default function FlowLayout({bar, tab, children}: { - bar: React.ReactNode, - tab: React.ReactNode, - children: React.ReactNode -}) { - const client = useApolloClient() - const router = useRouter() - const params = useParams() - const currentSession = useUserSession() - - const graphqlClient = React.useMemo(() => new GraphqlClient(client), [client]) - - const namespaceIndex = params.namespaceId as any as number - const projectIndex = params.projectId as any as number - const flowIndex = params.flowId as any as number - const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` - const projectId: NamespaceProject['id'] = `gid://sagittarius/NamespaceProject/${projectIndex}` - const flowId: Flow['id'] = `gid://sagittarius/Flow/${flowIndex}` - - if (currentSession === null) router.push("/login") - - const user = usePersistentReactiveArrayService(`dashboard::users::${currentSession?.id}`, (store) => new UserService(graphqlClient, store)) - const organization = usePersistentReactiveArrayService(`dashboard::organizations::${currentSession?.id}`, (store) => new OrganizationService(graphqlClient, store)) - const member = usePersistentReactiveArrayService(`dashboard::members::${currentSession?.id}`, (store) => new MemberService(graphqlClient, store)) - const namespace = usePersistentReactiveArrayService(`dashboard::namespaces::${currentSession?.id}`, (store) => new NamespaceService(graphqlClient, store)) - const runtime = usePersistentReactiveArrayService(`dashboard::global_runtimes::${currentSession?.id}`, (store) => new RuntimeService(graphqlClient, store)) - const project = usePersistentReactiveArrayService(`dashboard::projects::${currentSession?.id}`, (store) => new ProjectService(graphqlClient, store)) - const role = usePersistentReactiveArrayService(`dashboard::roles::${currentSession?.id}`, (store) => new RoleService(graphqlClient, store)) - const flow = usePersistentReactiveArrayService(`dashboard::flows::${currentSession?.id}`, (store) => new FlowService(graphqlClient, store)) - const functions = usePersistentReactiveArrayService(`dashboard::functions::${currentSession?.id}`, (store) => new FunctionService(graphqlClient, store)) - const datatype = usePersistentReactiveArrayService(`dashboard::datatypes::${currentSession?.id}`, (store) => new DatatypeService(graphqlClient, store)) - const flowtype = usePersistentReactiveArrayService(`dashboard::flowtypes::${currentSession?.id}`, (store) => new FlowTypeService(graphqlClient, store)) - const module = usePersistentReactiveArrayService(`dashboard::modules::${currentSession?.id}`, (store) => new ModuleService(graphqlClient, store)) - const ai = usePersistentReactiveArrayService(`dashboard::ai::${currentSession?.id}`, (store) => new AIService(graphqlClient, store)) - - const runtimeId = React.useMemo(() => project[1].getById(projectId, {namespaceId})?.primaryRuntime?.id, [projectId, project[0], namespaceId]) - - React.useEffect(() => { - if (!currentSession) return - - flow[1].values({namespaceId, projectId}) - functions[1].values({namespaceId, projectId, runtimeId}) - datatype[1].values({namespaceId, projectId, runtimeId}) - flowtype[1].values({namespaceId, projectId, runtimeId}) - module[1].values({namespaceId, projectId, runtimeId}) - }, [runtimeId, namespaceId, projectId, currentSession, flow, functions, datatype, flowtype]) - - return - -
-
- - -
- {"CodeZero - {tab} - - }> - {bar}}> - - <> - {children} - - - - - -} \ No newline at end of file diff --git a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/layout.tsx b/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/layout.tsx deleted file mode 100644 index b1955ec6..00000000 --- a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/layout.tsx +++ /dev/null @@ -1,6 +0,0 @@ -"use client" - -import {FlowLayout} from "@edition/flow/layout/FlowLayout"; -import '@xyflow/react/dist/style.css'; - -export default FlowLayout diff --git a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/page.tsx b/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/page.tsx deleted file mode 100644 index 45049222..00000000 --- a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -"use client" - -import {FlowOverviewPage} from "@edition/flow/pages/FlowOverviewPage"; - -export default FlowOverviewPage \ No newline at end of file diff --git a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/module/[moduleId]/layout.tsx b/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/module/[moduleId]/layout.tsx deleted file mode 100644 index 95f0a572..00000000 --- a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/module/[moduleId]/layout.tsx +++ /dev/null @@ -1,20 +0,0 @@ -"use client" - -import React from "react"; -import {Col, Flex} from "@code0-tech/pictor"; - -export default function AuthLayout({children}: Readonly<{ children: React.ReactNode }>) { - return
- - - {children} - - -
-} \ No newline at end of file diff --git a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/runtime/page.tsx b/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/runtime/page.tsx deleted file mode 100644 index b18846ea..00000000 --- a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/runtime/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import {RuntimeAssignPage} from "@edition/runtime/pages/RuntimeAssignPage"; - -export default RuntimeAssignPage \ No newline at end of file diff --git a/src/app/global.scss b/src/app/global.scss index 1b5ca7d6..542f4d65 100644 --- a/src/app/global.scss +++ b/src/app/global.scss @@ -23,4 +23,13 @@ a { .scroll-area__viewport > div { display: block !important; height: 100%; +} + +[data-sonner-toast] { + width: 100%; + filter: drop-shadow(0px 0px 1rem #070514); +} + +[data-sonner-toaster][data-y-position=top] { + top: 1rem; } \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 04197b57..45ee7951 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -14,12 +14,11 @@ import React, {Suspense} from "react"; import "./global.scss" import {setContext} from "@apollo/client/link/context"; import {ErrorLink} from "@apollo/client/link/error"; -import {usePathname, useRouter} from "next/navigation"; +import {useRouter} from "next/navigation"; import {Toaster} from "sonner"; import {Error} from "@code0-tech/sagittarius-graphql-types"; import {Inter} from 'next/font/google' import {GraphQLFormattedError} from "graphql/error"; -import {addIslandErrorNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; import {toast as pictorToast} from "@code0-tech/pictor/dist/components/toast/Toast"; import {createConsumer} from "@rails/actioncable"; import ActionCableLink from "graphql-ruby-client/subscriptions/ActionCableLink"; @@ -136,22 +135,15 @@ const ErrorCodeDescription: Record = { WRONG_TOTP: "Wrong TOTP code", } -const AUTH_ROUTES = ["/login", "/register", "/email", "/password", "/redirect"] - -const isAuthRoute = (pathname: string | null): boolean => - !!pathname && AUTH_ROUTES.some(route => pathname === route || pathname.startsWith(`${route}/`)) - /** * Root layout component that sets up Apollo Client and error handling */ export default function RootLayout({children}: Readonly<{ children: React.ReactNode }>) { const router = useRouter() - const pathname = usePathname() /** - * Handles error toasts based on error type. On (auth) pages the Island is not - * mounted, so fall back to pictor's standalone toast. + * Handles error toasts based on error type. */ const toastHandler = (error: Error | GraphQLFormattedError) => { console.error("[ERROR]", error) @@ -159,11 +151,7 @@ export default function RootLayout({children}: Readonly<{ children: React.ReactN ? (ErrorCodeDescription[(error.errorCode as string)] ?? "Internal error") : "Internal error" - if (isAuthRoute(pathname)) { - pictorToast({title: message, color: "error", dismissible: true}) - } else { - addIslandErrorNotification({message}) - } + pictorToast({title: message, color: "error"}) } /** @@ -297,7 +285,7 @@ export default function RootLayout({children}: Readonly<{ children: React.ReactN - + {children} diff --git a/src/packages/ce/src/ai/components/AIChatComponent.tsx b/src/packages/ce/src/ai/components/AIChatComponent.tsx index 9af5d1c5..f572937b 100644 --- a/src/packages/ce/src/ai/components/AIChatComponent.tsx +++ b/src/packages/ce/src/ai/components/AIChatComponent.tsx @@ -19,7 +19,6 @@ import { useStore } from "@code0-tech/pictor"; import Link from "next/link"; -import {StreamLanguage} from "@codemirror/language"; import CardSection from "@code0-tech/pictor/dist/components/card/CardSection"; import {Select} from "@radix-ui/react-select"; import {IconChevronDown, IconPlayerStop, IconSend, IconSparkles2Filled, IconX} from "@tabler/icons-react"; @@ -28,6 +27,7 @@ import {motion} from "framer-motion"; import {AiGenerateFlowSubscriptionPayload, Flow, NamespaceProject} from "@code0-tech/sagittarius-graphql-types"; import {useAIGenerationStore} from "@edition/ai/hooks/AI.generation.hook"; import {AIGeneratingMessageComponent} from "@edition/ai/components/AIGeneratingMessageComponent"; +import BorderBeam from "border-beam"; export interface AIChatComponentProps { projectId: NamespaceProject['id'] @@ -46,6 +46,8 @@ export const AIChatComponent: React.FC = (props) => { const aiStore = useStore(AIService) const addGeneration = useAIGenerationStore(s => s.addGeneration) const removeGeneration = useAIGenerationStore(s => s.removeGeneration) + const suppressToast = useAIGenerationStore(s => s.suppressToast) + const unsuppressToast = useAIGenerationStore(s => s.unsuppressToast) const [promptState, setPromptState] = React.useState(prompt) const [model, setModel] = React.useState(undefined) @@ -70,6 +72,15 @@ export const AIChatComponent: React.FC = (props) => { const aiLoading = !!executionIdentifier && isThisGenerating + // While this chat is mounted it displays its own generation inline, so the + // global watcher toast stays hidden for it (suppression is set synchronously in + // onSend to avoid a flash). Unmounting (e.g. navigating away) releases the + // suppression so the toast appears where the generation is no longer visible. + React.useEffect(() => { + if (!executionIdentifier) return + return () => unsuppressToast(executionIdentifier) + }, [executionIdentifier, unsuppressToast]) + const onSend = React.useCallback(() => { setAiErrorMessage(null) aiService.generateFlow({ @@ -81,6 +92,9 @@ export const AIChatComponent: React.FC = (props) => { if ((payload?.errors?.length ?? 0) <= 0 && payload?.executionIdentifier) { const id = payload.executionIdentifier setExecutionIdentifier(id) + // Suppress synchronously together with adding the generation so the + // watcher's first render already sees it hidden (no toast flash). + suppressToast(id) addGeneration({ executionIdentifier: id, onData: (data) => onData?.(data), @@ -88,7 +102,7 @@ export const AIChatComponent: React.FC = (props) => { }) } }) - }, [aiService, model, promptState, projectId, flowId, onData, addGeneration]) + }, [aiService, model, promptState, projectId, flowId, onData, addGeneration, suppressToast]) const onStop = React.useCallback(() => { if (executionIdentifier) removeGeneration(executionIdentifier) @@ -117,150 +131,158 @@ export const AIChatComponent: React.FC = (props) => { mass: 0.8 }} > + - - { - models.length <= 0 && ( - - - - - - - - - - - ) - } - { - aiLoading ? ( -
- -
- ) : ( - { - setPromptState(value) - }} - wrapperComponent={{ - style: { - background: "transparent", - boxShadow: "none" - } - }} - placeholder={"Describe what you want to edit..."}/> - ) - } - - - - - - + + + + + + + - - - - {model.name} - - - ({model.tokenCost}x) - - + + Generate + - ))} - - - - - - - {!aiLoading && aiErrorMessage ? ( - - {aiErrorMessage} - - ) : null} - {aiLoading ? ( - - ) : ( - - )} - {onClose ? ( - - ) : null} + + + + + + + + {!aiLoading && aiErrorMessage ? ( + + {aiErrorMessage} + + ) : null} + {aiLoading ? ( + + ) : ( + + )} + {onClose ? ( + + ) : null} + - - -
+ +
+ { { - - const generations = useAIGenerationStore(s => s.generations) - const islandToastIdRef = React.useRef(null) - const addToast = useIsland(s => s.addToast) - const removeToast = useIsland(s => s.removeToast) +// Owns a single watcher toast for one generation. The toast is shown unless the +// generation is currently displayed inline by a mounted AIChatComponent (i.e. the +// page where it was triggered). Unmounting this component dismisses its toast, so +// removing a generation from the store cleans up automatically. +const AIGenerationToastComponent: React.FC<{ suppressed: boolean }> = ({suppressed}) => { - const isGenerating = generations.length > 0 + const toastIdRef = React.useRef(null) React.useEffect(() => { - if (isGenerating && islandToastIdRef.current === null) { - const id = Date.now() - islandToastIdRef.current = id - addToast({ - id, + if (!suppressed && toastIdRef.current === null) { + toastIdRef.current = toast({ duration: Infinity, - index: 2, - icon: , - message: , + wrapper: (children) => { + return + {children} + + }, + icon: , + title: , }) - } else if (!isGenerating && islandToastIdRef.current !== null) { - removeToast(islandToastIdRef.current) - islandToastIdRef.current = null + } else if (suppressed && toastIdRef.current !== null) { + sonnerToast.dismiss(toastIdRef.current) + toastIdRef.current = null } - }, [isGenerating, addToast, removeToast]) + }, [suppressed]) React.useEffect(() => () => { - if (islandToastIdRef.current !== null) { - removeToast(islandToastIdRef.current) - islandToastIdRef.current = null + if (toastIdRef.current !== null) { + sonnerToast.dismiss(toastIdRef.current) + toastIdRef.current = null } - }, [removeToast]) + }, []) + + return null +} + +export const AIGenerationWatcherComponent: React.FC = () => { + + const generations = useAIGenerationStore(s => s.generations) + const suppressedIdentifiers = useAIGenerationStore(s => s.suppressedIdentifiers) return <> {generations.map(generation => ( - + + + + ))} } diff --git a/src/packages/ce/src/ai/hooks/AI.generation.hook.ts b/src/packages/ce/src/ai/hooks/AI.generation.hook.ts index 620df243..8c32b7de 100644 --- a/src/packages/ce/src/ai/hooks/AI.generation.hook.ts +++ b/src/packages/ce/src/ai/hooks/AI.generation.hook.ts @@ -9,21 +9,34 @@ export interface AIGeneration { interface AIGenerationState { generations: AIGeneration[] + suppressedIdentifiers: string[] addGeneration: (generation: AIGeneration) => void removeGeneration: (executionIdentifier: string) => void hasGeneration: (executionIdentifier: string) => boolean + suppressToast: (executionIdentifier: string) => void + unsuppressToast: (executionIdentifier: string) => void } export const useAIGenerationStore = create((setState, getState) => ({ generations: [], + suppressedIdentifiers: [], addGeneration: (generation) => setState((state) => ({ generations: state.generations.some(g => g.executionIdentifier === generation.executionIdentifier) ? state.generations : [...state.generations, generation] })), removeGeneration: (executionIdentifier) => setState((state) => ({ - generations: state.generations.filter(g => g.executionIdentifier !== executionIdentifier) + generations: state.generations.filter(g => g.executionIdentifier !== executionIdentifier), + suppressedIdentifiers: state.suppressedIdentifiers.filter(id => id !== executionIdentifier) })), hasGeneration: (executionIdentifier) => getState().generations.some(g => g.executionIdentifier === executionIdentifier), + suppressToast: (executionIdentifier) => setState((state) => ({ + suppressedIdentifiers: state.suppressedIdentifiers.includes(executionIdentifier) + ? state.suppressedIdentifiers + : [...state.suppressedIdentifiers, executionIdentifier] + })), + unsuppressToast: (executionIdentifier) => setState((state) => ({ + suppressedIdentifiers: state.suppressedIdentifiers.filter(id => id !== executionIdentifier) + })), })) diff --git a/src/packages/ce/src/application/components/ApplicationAttentionFlowsComponent.tsx b/src/packages/ce/src/application/components/ApplicationAttentionFlowsComponent.tsx new file mode 100644 index 00000000..de0c1e6e --- /dev/null +++ b/src/packages/ce/src/application/components/ApplicationAttentionFlowsComponent.tsx @@ -0,0 +1,137 @@ +"use client" + +import React from "react"; +import { + Avatar, + Card, + DataTable, + DataTableColumn, + Flex, + hashToColor, + Spacing, + Text, + useService, + useStore +} from "@code0-tech/pictor"; +import {UserService} from "@edition/user/services/User.service"; +import {NamespaceService} from "@edition/namespace/services/Namespace.service"; +import {ProjectService} from "@edition/project/services/Project.service"; +import {FlowService} from "@edition/flow/services/Flow.service"; +import {FlowView} from "@edition/flow/services/Flow.view"; +import {Namespace} from "@code0-tech/sagittarius-graphql-types"; +import {useUserSession} from "@edition/user/hooks/User.session.hook"; +import {IconChevronRight, IconPointFilled} from "@tabler/icons-react"; +import {formatDistanceToNow} from "date-fns"; +import {useRouter} from "next/navigation"; + +export interface ApplicationAttentionFlowsComponentProps { + namespaceId?: Namespace['id'] +} + +export const ApplicationAttentionFlowsComponent: React.FC = ({namespaceId}) => { + + const userService = useService(UserService) + const userStore = useStore(UserService) + const namespaceService = useService(NamespaceService) + const namespaceStore = useStore(NamespaceService) + const projectService = useService(ProjectService) + const projectStore = useStore(ProjectService) + const flowService = useService(FlowService) + const flowStore = useStore(FlowService) + + const [open, setOpen] = React.useState(false) + const router = useRouter() + const userSession = useUserSession() + const currentUser = React.useMemo(() => userService.getById(userSession?.user?.id), [userStore, userSession]) + + const memberships = React.useMemo( + () => currentUser?.namespaceMemberships?.nodes ?? [], + [currentUser?.namespaceMemberships?.nodes?.length] + ) + + const namespaces = React.useMemo( + () => memberships + .map(membership => namespaceService.getById(membership?.namespace?.id)) + .filter((namespace): namespace is Namespace => !!namespace) + .filter(namespace => !namespaceId || namespace.id === namespaceId), + [memberships.length, namespaceStore, namespaceId] + ) + + const projects = React.useMemo( + () => namespaces.flatMap(namespace => projectService.values({namespaceId: namespace.id})), + [namespaces, projectStore] + ) + + const flows = React.useMemo( + () => projects.flatMap(project => flowService.values({ + namespaceId: project.namespace?.id, + projectId: project.id, + })), + [projects, flowStore] + ) + + const projectNames = React.useMemo( + () => new Map(projects.map(project => [project.id ?? "", project.name ?? ""])), + [projects] + ) + + const projectNameOf = (flow: FlowView): string => + projectNames.get(flow.project?.id ?? "") || "Unknown project" + + const updatedAgoOf = (flow: FlowView): string => { + const date = flow.updatedAt ? new Date(flow.updatedAt) : null + if (!date || isNaN(date.getTime())) return "Unknown" + return `${formatDistanceToNow(date)} ago` + } + + const openFlow = (flow: FlowView): void => { + const namespaceIndex = flow.project?.namespace?.id?.match(/Namespace\/(\d+)$/)?.[1] + const projectIndex = flow.project?.id?.match(/NamespaceProject\/(\d+)$/)?.[1] + const flowIndex = flow.id?.match(/Flow\/(\d+)$/)?.[1] + router.push(`/namespace/${namespaceIndex}/project/${projectIndex}/flow/${flowIndex}`) + } + + const attentionFlows: FlowView[] = React.useMemo( + () => flows.filter(flow => flow.validationStatus === "INVALID"), + [flows] + ) + + if (attentionFlows.length === 0) return null + + const count = attentionFlows.length + + return setOpen(previous => !previous)}> + + + + {count} {count === 1 ? "flow needs" : "flows need"} attention + + + + {open && ( +
e.stopPropagation()}> + + flow && openFlow(flow)}> + {(flow) => <> + + {flow.name} + + + + + {projectNameOf(flow)} + + + + + {updatedAgoOf(flow)} + + + } + +
+ )} +
+} diff --git a/src/packages/ce/src/application/components/ApplicationSettingsDialogComponent.tsx b/src/packages/ce/src/application/components/ApplicationSettingsDialogComponent.tsx new file mode 100644 index 00000000..db6fc2ed --- /dev/null +++ b/src/packages/ce/src/application/components/ApplicationSettingsDialogComponent.tsx @@ -0,0 +1,60 @@ +"use client" + +import React from "react"; +import {Button, Text} from "@code0-tech/pictor"; +import {TabList, TabTrigger} from "@code0-tech/pictor/dist/components/tab/Tab"; +import {IconServer, IconSettings2, IconShieldLock, IconUsers} from "@tabler/icons-react"; +import {ApplicationUsersView} from "@edition/application/views/ApplicationUsersView"; +import {ApplicationServersView} from "@edition/application/views/ApplicationServersView"; +import {ApplicationGeneralSettingsView} from "@edition/application/views/ApplicationGeneralSettingsView"; +import {ApplicationRestrictionsView} from "@edition/application/views/ApplicationRestrictionsView"; +import {ApplicationLicensesView} from "@edition/application/views/ApplicationLicensesView"; +import {ApplicationLicensesTabTriggerView} from "@edition/application/views/ApplicationLicensesTabTriggerView"; +import {SettingDialog} from "@core/components/SettingDialog"; + +export interface ApplicationSettingsDialogComponentProps { + open?: boolean + onOpenChange?: (open: boolean) => void +} + +export const ApplicationSettingsDialogComponent: React.FC = (props) => { + + const {open, onOpenChange} = props + + return onOpenChange?.(open)} + title={"Application settings"} + description={"General settings and restrictions for your Sculptor application. These settings affect all users and organizations."} + trigger={ + + + + + + + + + + + + + + }> + + + + + + +} diff --git a/src/packages/ce/src/application/pages/ApplicationPage.tsx b/src/packages/ce/src/application/pages/ApplicationPage.tsx index 3f3bffb7..22727696 100644 --- a/src/packages/ce/src/application/pages/ApplicationPage.tsx +++ b/src/packages/ce/src/application/pages/ApplicationPage.tsx @@ -1,306 +1,38 @@ -import {PersonalProjectsView} from "@edition/project/views/PersonalProjectsView"; import { AuroraBackground, - Avatar, - Badge, - Button, - ButtonGroup, - Card, - Col, - Flex, - Row, ScrollArea, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport, - Spacing, - Text, - useService, - useStore + Spacing } from "@code0-tech/pictor"; -import {UserService} from "@edition/user/services/User.service"; -import {NamespaceService} from "@edition/namespace/services/Namespace.service"; -import {OrganizationService} from "@edition/organization/services/Organization.service"; -import {OrganizationView} from "@edition/organization/services/Organization.view"; -import {ProjectService} from "@edition/project/services/Project.service"; -import {Namespace, Organization} from "@code0-tech/sagittarius-graphql-types"; import React from "react"; -import {NamespacesTopView} from "@edition/namespace/views/NamespacesTopView"; -import {useUserSession} from "@edition/user/hooks/User.session.hook"; -import {IconArrowUpRight, IconMail, IconSparkles, IconTrendingUp, IconUser} from "@tabler/icons-react"; -import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout"; -import Link from "next/link"; - -const withinDays = (time: string | null | undefined, days: number): boolean => - !!time && Date.now() - new Date(time).getTime() < days * 24 * 60 * 60 * 1000 +import {NamespaceRowView} from "@edition/namespace/views/NamespaceRowView"; +import {ApplicationStatsView} from "@edition/application/views/ApplicationStatsView"; +import {ApplicationAttentionFlowsComponent} from "@edition/application/components/ApplicationAttentionFlowsComponent"; export const ApplicationPage = () => { - - const userStore = useStore(UserService) - const userService = useService(UserService) - const namespaceService = useService(NamespaceService) - const namespaceStore = useStore(NamespaceService) - const organizationService = useService(OrganizationService) - const organizationStore = useStore(OrganizationService) - const projectService = useService(ProjectService) - const projectStore = useStore(ProjectService) - - const userSession = useUserSession() - const currentUser = React.useMemo(() => userService.getById(userSession?.user?.id), [userStore, userSession]) - const namespaceIndex = currentUser?.namespace?.id?.match(/Namespace\/(\d+)$/)?.[1] - - const memberships = React.useMemo( - () => currentUser?.namespaceMemberships?.nodes ?? [], - [currentUser?.namespaceMemberships?.nodes?.length] - ) - - const namespaces = React.useMemo( - () => memberships - .map(membership => namespaceService.getById(membership?.namespace?.id)) - .filter((namespace): namespace is Namespace => !!namespace), - [memberships.length, namespaceStore] - ) - - const organizationNamespaces = React.useMemo( - () => namespaces.filter(namespace => namespace.parent?.__typename === "Organization"), - [namespaces] - ) - - const organizations = React.useMemo( - () => organizationNamespaces - .map(namespace => organizationService.getById((namespace.parent as Organization).id)) - .filter((organization): organization is OrganizationView => !!organization), - [organizationNamespaces, organizationStore] - ) - - const projects = React.useMemo( - () => namespaces.flatMap(namespace => projectService.values({namespaceId: namespace.id})), - [namespaces, projectStore] - ) - - const ownedOrganizationsCount = organizations.filter(organization => organization.userAbilities?.deleteOrganization).length - const memberOrganizationsCount = organizationNamespaces.length - ownedOrganizationsCount - const newOrganizationsLastMonth = organizations.filter(organization => withinDays(organization.createdAt, 30)).length - - const totalProjectsCount = namespaces.reduce((sum, namespace) => sum + (namespace.projects?.count ?? 0), 0) - const personalProjectsCount = namespaces.find(namespace => namespace.id === currentUser?.namespace?.id)?.projects?.count ?? 0 - const organizationProjectsCount = totalProjectsCount - personalProjectsCount - const newProjectsLastWeek = projects.filter(project => withinDays(project.createdAt, 7)).length - - const totalFlowsCount = projects.reduce((sum, project) => sum + (project.flows?.count ?? 0), 0) - const newFlowsLastMonth = projects - .flatMap(project => project.flows?.nodes ?? []) - .filter(flow => withinDays(flow?.createdAt, 30)).length - - const leftContent = - - -
- - - 👋 - -
- - {currentUser?.firstname} {currentUser?.lastname} - - - - - @{currentUser?.username} - - - - {currentUser?.email} - - - - BASIC - - - - - - - - -
-
-
- - return -
- - - - Overview - - - - Track the current state of your organizations, projects and flows at a single glance
- and keep an eye on how your activity has developed across every area over the recent - weeks -
- - - - - - - Organizations - - {newOrganizationsLastMonth > 0 && - - +{newOrganizationsLastMonth} last month - } - - - - - - - {organizationNamespaces.length} - - - You are the owner of {ownedOrganizationsCount} organizations
and a - member of {memberOrganizationsCount} -
-
-
-
- - - - - - Projects - - {newProjectsLastWeek > 0 && - - +{newProjectsLastWeek} last week - } - - - - - - - {totalProjectsCount} - - - {personalProjectsCount} in your personal workspace
and - {" "}{organizationProjectsCount} in your organizations -
-
-
-
- - - - - - Flows - - {newFlowsLastMonth > 0 && - - +{newFlowsLastMonth} last month - } - - - - - - - {totalFlowsCount} - - - Spread across {totalProjectsCount} projects
and - {" "}{namespaces.length} workspaces -
-
-
-
- -
+ return
+ + +
+ - - Your work - - - - A quick summary of the personal projects you own and the organizations you belong to
- so you can jump straight back into your recent work or start something completely - new -
+ - - - - - - Personal projects - - - - - - - - - - - - - - - - - - - - - - Top workspaces - - - - - - - - - - - - - - - - - - - - - - -
- + +
+
+ + + +
+
} \ No newline at end of file diff --git a/src/packages/ce/src/application/pages/ApplicationSettingsPage.tsx b/src/packages/ce/src/application/pages/ApplicationSettingsPage.tsx index f99b4e9c..39a4915b 100644 --- a/src/packages/ce/src/application/pages/ApplicationSettingsPage.tsx +++ b/src/packages/ce/src/application/pages/ApplicationSettingsPage.tsx @@ -2,23 +2,14 @@ import React from "react"; import {useService, useStore} from "@code0-tech/pictor"; +import {notFound, useRouter} from "next/navigation"; import {UserService} from "@edition/user/services/User.service"; -import {notFound} from "next/navigation"; -import {Tab} from "@code0-tech/pictor/dist/components/tab/Tab"; import {useUserSession} from "@edition/user/hooks/User.session.hook"; -import { - ResizableHandle, - ResizablePanel, - ResizablePanelGroup -} from "@code0-tech/pictor/dist/components/resizable/Resizable"; -import {SidebarComponent} from "@core/components/SidebarComponent"; -import {ApplicationGeneralSettingsView} from "@edition/application/views/ApplicationGeneralSettingsView"; -import {ApplicationRestrictionsView} from "@edition/application/views/ApplicationRestrictionsView"; -import {ApplicationTabListView} from "@edition/application/views/ApplicationTabListView"; -import {ApplicationLicensesView} from "@edition/application/views/ApplicationLicensesView"; +import {ApplicationSettingsDialogComponent} from "@edition/application/components/ApplicationSettingsDialogComponent"; export const ApplicationSettingsPage: React.FC = () => { + const router = useRouter() const userStore = useStore(UserService) const userService = useService(UserService) @@ -33,21 +24,18 @@ export const ApplicationSettingsPage: React.FC = () => { notFound() } - return - - - - - - - <> - - - - - - - -} \ No newline at end of file + return { + if (open) return + + const nav = (window as unknown as { navigation?: { entries(): { url: string }[], currentEntry?: { index: number } } }).navigation + if (!nav?.entries) { + router.back() + return + } + + const index = nav.currentEntry?.index ?? 0 + if (index > 0) router.back() + else router.push("/") + }}/> +} diff --git a/src/packages/ce/src/application/views/ApplicationBarView.tsx b/src/packages/ce/src/application/views/ApplicationBarView.tsx deleted file mode 100644 index 304e3ac3..00000000 --- a/src/packages/ce/src/application/views/ApplicationBarView.tsx +++ /dev/null @@ -1,212 +0,0 @@ -"use client" - -import {Button, Flex, MenuItem, MenuSeparator, Text, useService, useStore} from "@code0-tech/pictor"; -import {UserService} from "@edition/user/services/User.service"; -import {useParams, usePathname, useRouter} from "next/navigation"; -import React from "react"; -import Link from "next/link"; -import { - IconArrowLeft, - IconArrowUpRight, - IconBuilding, - IconFolders, - IconInbox, - IconLogout, - IconSearch, IconSettings, - IconUser -} from "@tabler/icons-react"; -import {ApplicationBreadcrumbView} from "@edition/application/views/ApplicationBreadcrumbView"; -import UserMenuComponent from "@edition/user/components/UserMenuComponent"; -import {useUserSession} from "@edition/user/hooks/User.session.hook"; -import {Island} from "@code0-tech/pictor/dist/components/island/Island"; -import {ButtonGroup} from "@code0-tech/pictor/dist/components/button-group/ButtonGroup"; -import {NamespaceService} from "@edition/namespace/services/Namespace.service"; -import {OrganizationService} from "@edition/organization/services/Organization.service"; -import {Namespace} from "@code0-tech/sagittarius-graphql-types"; - -export const ApplicationBarView: React.FC = () => { - - const currentSession = useUserSession() - const userService = useService(UserService) - const userStore = useStore(UserService) - const namespaceService = useService(NamespaceService) - const namespaceStore = useStore(NamespaceService) - const organizationService = useService(OrganizationService) - const organizationStore = useStore(OrganizationService) - const router = useRouter() - const pathname = usePathname() - const [loading, startTransition] = React.useTransition() - const params = useParams() - - const namespaceIndex = params.namespaceId as string | undefined - const projectIndex = params.projectId as string | undefined - const currentUser = React.useMemo(() => userService.getById(currentSession?.user?.id), [userStore, currentSession]) - const userNamespaceIndex = React.useMemo(() => currentUser?.namespace?.id?.match(/Namespace\/(\d+)$/)?.[1], [currentUser]) - const userIndex = currentUser?.id?.match(/User\/(\d+)$/)?.[1] - const currentStep = projectIndex ? "project" : namespaceIndex ? "namespace" : "home"; - - const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex as any as number}` - const namespace = React.useMemo(() => namespaceService.getById(namespaceId), [namespaceStore, namespaceId]) - const parentOrganization = React.useMemo(() => namespace?.parent?.__typename === "Organization" ? organizationService.getById(namespace?.parent?.id) : null, [organizationStore, namespace]) - - const homeTabs = React.useMemo(() => { - return [ - , - , - ...(currentUser?.admin ? [ - , - , - - ] : []) - ] - }, [currentUser, pathname]) - - const namespaceTabs = React.useMemo(() => { - const baseLink = `/namespace/${namespaceIndex}` - const showSettings = (parentOrganization && (parentOrganization.userAbilities?.deleteOrganization - || parentOrganization?.userAbilities?.updateOrganization)) - || namespace?.userAbilities?.createLicense - //TODO add license check for enterprise features - - return [ - , - , - , - , - ...(showSettings ? [ - - ] : []) - ] - }, [namespace, parentOrganization, namespaceIndex, pathname]) - - const projectTabs = React.useMemo(() => { - const baseLink = `/namespace/${namespaceIndex}/project/${projectIndex}` - - return [ - , - , - , - - ] - }, [namespaceIndex, projectIndex, pathname]) - - const userMenu = React.useMemo(() => { - - if (!currentUser || !currentSession) { - return null - } - - const userLogout = () => { - startTransition(async () => { - await userService.usersLogout({ - userSessionId: currentSession.id!! - }).then(payload => { - window.localStorage.removeItem("ide_code-zero_session") - router.push("/login") - }) - }) - } - - return - - - Profile - - - - - Settings - - - - - - Workspaces - - - - - Personal Workspace - - - - - Logout - - - }, [currentUser, currentSession, userNamespaceIndex]) - - return - {currentStep === "project" ? - - : currentStep === "namespace" ? - - :
} - - - - {currentStep === "project" ? projectTabs : currentStep === "namespace" ? namespaceTabs : homeTabs} - - - - - - - {userMenu} - - -} diff --git a/src/packages/ce/src/application/views/ApplicationBreadcrumbView.tsx b/src/packages/ce/src/application/views/ApplicationBreadcrumbView.tsx deleted file mode 100644 index 950b02c5..00000000 --- a/src/packages/ce/src/application/views/ApplicationBreadcrumbView.tsx +++ /dev/null @@ -1,46 +0,0 @@ -"use client" - -import React from "react"; -import {Breadcrumb, Text} from "@code0-tech/pictor"; -import {useParams, usePathname} from "next/navigation"; - -export const ApplicationBreadcrumbView: React.FC = () => { - - const path = usePathname() - const params = useParams() - - const namespaceIndex = params.namespaceId as string | undefined - const projectIndex = params.projectId as string | undefined - - return - { - (namespaceIndex || projectIndex) ? ( - - Home - - ) : null - } - { - (namespaceIndex || projectIndex) ? ( - - ... - - ) : null - } - { - namespaceIndex && projectIndex ? path.split("/").slice(3, path.split("/").length).map((crumb, index) => { - return - {crumb} - - }) : namespaceIndex ? path.split("/").slice(1, path.split("/").length).map((crumb, index) => { - return - {crumb} - - }) : path.split("/").filter(value => value != "").slice(0, path.split("/").length).map((crumb, index) => { - return - {crumb} - - }) - } - -} diff --git a/src/packages/ce/src/application/views/ApplicationGeneralSettingsView.tsx b/src/packages/ce/src/application/views/ApplicationGeneralSettingsView.tsx index 11d809ee..6baa08d2 100644 --- a/src/packages/ce/src/application/views/ApplicationGeneralSettingsView.tsx +++ b/src/packages/ce/src/application/views/ApplicationGeneralSettingsView.tsx @@ -1,9 +1,21 @@ import React, {startTransition} from "react"; -import {Badge, Button, Card, Flex, NumberInput, Spacing, Text, TextInput, useForm, useService, useStore} from "@code0-tech/pictor"; +import { + Badge, + Button, + Card, + Flex, + NumberInput, + Spacing, + Text, + TextInput, + useForm, + useService, + useStore +} from "@code0-tech/pictor"; import CardSection from "@code0-tech/pictor/dist/components/card/CardSection"; import {TabContent} from "@code0-tech/pictor/dist/components/tab/Tab"; import {ApplicationService} from "@edition/application/services/Application.service"; -import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {toast} from "@code0-tech/pictor/dist/components/toast/Toast"; export const ApplicationGeneralSettingsView: React.FC = () => { @@ -46,9 +58,7 @@ export const ApplicationGeneralSettingsView: React.FC = () => { runtimeMaxHeartbeatIntervalMinutes: !!runtimeMaxHeartbeatIntervalMinutes ? Number(runtimeMaxHeartbeatIntervalMinutes) : null, }).then(payload => { if ((payload?.errors?.length ?? 0) <= 0) { - addIslandSuccessNotification({ - message: "Updated application" - }) + toast({title: "Updated application", color: "success"}) } }) }) @@ -56,8 +66,18 @@ export const ApplicationGeneralSettingsView: React.FC = () => { }) return - General - + + Settings + + + + + General configuration for your Sculptor application, including version information, legal links and runtime + behaviour. + + + Versions + @@ -90,12 +110,9 @@ export const ApplicationGeneralSettingsView: React.FC = () => { - - - Legal url's - - - + + Legal URLs + @@ -122,12 +139,9 @@ export const ApplicationGeneralSettingsView: React.FC = () => { - - - Runtimes - - - + + Runtimes + diff --git a/src/packages/ce/src/application/views/ApplicationLicensesTabTriggerView.tsx b/src/packages/ce/src/application/views/ApplicationLicensesTabTriggerView.tsx new file mode 100644 index 00000000..47407ebe --- /dev/null +++ b/src/packages/ce/src/application/views/ApplicationLicensesTabTriggerView.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +export const ApplicationLicensesTabTriggerView: React.FC = () => { + return null +} diff --git a/src/packages/ce/src/application/views/ApplicationNavigationView.tsx b/src/packages/ce/src/application/views/ApplicationNavigationView.tsx new file mode 100644 index 00000000..d1efdc3b --- /dev/null +++ b/src/packages/ce/src/application/views/ApplicationNavigationView.tsx @@ -0,0 +1,176 @@ +"use client" + +import React, {startTransition} from "react"; +import { + Avatar, + Button, + Flex, + Icon, + Menu, + MenuContent, + MenuItem, + MenuLabel, + MenuPortal, + MenuSeparator, + MenuTrigger, + Text, + Tooltip, + TooltipContent, + TooltipPortal, + TooltipTrigger, + useService, + useStore +} from "@code0-tech/pictor"; +import { + IconAdjustmentsFilled, + IconApps, + IconArrowAutofitLeft, + IconArrowAutofitLeftFilled, + IconHomeFilled, + IconSettingsFilled, + IconUser +} from "@tabler/icons-react"; +import Link from "next/link"; +import {UserService} from "@edition/user/services/User.service"; +import {useUserSession} from "@edition/user/hooks/User.session.hook"; +import {useRouter} from "next/navigation"; + +export const ApplicationNavigationView: React.FC = () => { + + + const userService = useService(UserService) + const userStore = useStore(UserService) + + const router = useRouter() + const currentSession = useUserSession() + const currentUser = React.useMemo( + () => userService.getById(currentSession?.user?.id), + [userStore, currentSession] + ) + const userNamespaceIndex = React.useMemo( + () => currentUser?.namespace?.id?.match(/Namespace\/(\d+)$/)?.[1], + [currentUser] + ) + + const userLogout = () => { + + if (!currentSession || !currentSession.id) return + + startTransition(async () => { + await userService.usersLogout({ + userSessionId: currentSession.id!! + }).then(payload => { + window.localStorage.removeItem("ide_code-zero_session") + router.push("/login") + }) + }) + } + + return + + + + + + + + + + Home + + + + + {currentUser?.admin && ( + + + + + + + + + + Application Settings + + + + + )} + + + + + + + + + + + User Settings + + + + + + + + + + + + Logout + + + + + + + + + + + + User menu + + + + Profile + + + + + Settings + + + + + + Workspaces + + + + + Personal Workspace + + + + + Logout + + + + + + +} \ No newline at end of file diff --git a/src/packages/ce/src/application/views/ApplicationRestrictionsView.tsx b/src/packages/ce/src/application/views/ApplicationRestrictionsView.tsx index 508d520f..2c441c9e 100644 --- a/src/packages/ce/src/application/views/ApplicationRestrictionsView.tsx +++ b/src/packages/ce/src/application/views/ApplicationRestrictionsView.tsx @@ -3,7 +3,7 @@ import {Button, Card, Flex, Spacing, SwitchInput, Text, useForm, useService, use import CardSection from "@code0-tech/pictor/dist/components/card/CardSection"; import {TabContent} from "@code0-tech/pictor/dist/components/tab/Tab"; import {ApplicationService} from "@edition/application/services/Application.service"; -import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {toast} from "@code0-tech/pictor/dist/components/toast/Toast"; export const ApplicationRestrictionsView: React.FC = () => { @@ -36,9 +36,7 @@ export const ApplicationRestrictionsView: React.FC = () => { userRegistrationEnabled: values.userRegistrationEnabled, }).then(payload => { if ((payload?.errors?.length ?? 0) <= 0) { - addIslandSuccessNotification({ - message: "Updated application" - }) + toast({title: "Updated application", color: "success"}) } }) }) @@ -47,10 +45,14 @@ export const ApplicationRestrictionsView: React.FC = () => { return - Restrictions - + Restrictions + - + + + Control who can register, create organizations, and view administrator status across the application. + + diff --git a/src/packages/ce/src/application/views/ApplicationServersView.tsx b/src/packages/ce/src/application/views/ApplicationServersView.tsx new file mode 100644 index 00000000..65af796d --- /dev/null +++ b/src/packages/ce/src/application/views/ApplicationServersView.tsx @@ -0,0 +1,133 @@ +"use client" + +import React from "react"; +import { + Badge, + Button, + ButtonGroup, + Flex, + Menu, + MenuContent, + MenuItem, + MenuLabel, + MenuPortal, + MenuTrigger, + Spacing, + Text, + useService, + useStore +} from "@code0-tech/pictor"; +import {TabContent} from "@code0-tech/pictor/dist/components/tab/Tab"; +import {DataTableSortProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; +import {IconAdjustmentsHorizontal, IconArrowsSort, IconCheck, IconPlus} from "@tabler/icons-react"; +import Link from "next/link"; +import {useRouter} from "next/navigation"; +import {Runtime} from "@code0-tech/sagittarius-graphql-types"; +import {RuntimeService} from "@edition/runtime/services/Runtime.service"; +import {RuntimeDataTableComponent} from "@edition/runtime/components/RuntimeDataTableComponent"; + +const filterLabels = { + all: "All", + connected: "Connected", + disconnected: "Disconnected", +} as const + +const sortLabels = { + name: "Name", + createdAt: "Created", + updatedAt: "Updated", +} as const + +// Global runtimes are the ones not bound to a namespace. +const isGlobal = (runtime: Runtime) => !runtime?.namespace?.id + +export const ApplicationServersView: React.FC = () => { + + const router = useRouter() + const runtimeService = useService(RuntimeService) + const runtimeStore = useStore(RuntimeService) + + const runtimes = React.useMemo(() => runtimeService.values(), [runtimeStore]) + + const [filter, setFilter] = React.useState("all") + const [sort, setSort] = React.useState("name") + + const preFilter = React.useCallback((runtime: Runtime) => { + if (!isGlobal(runtime)) return false + if (filter === "connected") return runtime.status === "CONNECTED" + if (filter === "disconnected") return runtime.status !== "CONNECTED" + return true + }, [filter]) + + const visibleCount = React.useMemo(() => runtimes.filter(preFilter).length, [runtimes, preFilter]) + + const sortProps: DataTableSortProps = {[sort]: "asc"} + + return + + + Servers + {visibleCount} + + + {/* Filter */} + + + + + + + Filter + {(Object.keys(filterLabels) as (keyof typeof filterLabels)[]).map(k => ( + setFilter(k)}> + + {filterLabels[k]} + + ))} + + + + + {/* Sort */} + + + + + + + Sort by + {(Object.keys(sortLabels) as (keyof typeof sortLabels)[]).map(k => ( + setSort(k)}> + + {sortLabels[k]} + + ))} + + + + + {/* Create */} + + + + + + + + Global runtimes are the environments where your applications run. These are available across all workspaces. + + + { + const number = runtime?.id?.match(/Runtime\/(\d+)$/)?.[1] + if (number) router.push(`/runtimes/${number}/settings`) + }}/> + +} diff --git a/src/packages/ce/src/application/views/ApplicationStatsView.tsx b/src/packages/ce/src/application/views/ApplicationStatsView.tsx new file mode 100644 index 00000000..3a91dfd0 --- /dev/null +++ b/src/packages/ce/src/application/views/ApplicationStatsView.tsx @@ -0,0 +1,125 @@ +"use client" + +import React from "react"; +import {Badge, Card, Flex, Text, useService, useStore} from "@code0-tech/pictor"; +import {UserService} from "@edition/user/services/User.service"; +import {NamespaceService} from "@edition/namespace/services/Namespace.service"; +import {ProjectService} from "@edition/project/services/Project.service"; +import {FlowService} from "@edition/flow/services/Flow.service"; +import {Namespace} from "@code0-tech/sagittarius-graphql-types"; +import {useUserSession} from "@edition/user/hooks/User.session.hook"; +import {IconAlertTriangle, IconTrendingUp} from "@tabler/icons-react"; + +// Each node run is assumed to save this many seconds of manual work. +const SAVED_SECONDS_PER_NODE_RUN = 60 + +const withinDays = (time: string | null | undefined, days: number): boolean => + !!time && Date.now() - new Date(time).getTime() < days * 24 * 60 * 60 * 1000 + +export const ApplicationStatsView: React.FC = () => { + + const userService = useService(UserService) + const userStore = useStore(UserService) + const namespaceService = useService(NamespaceService) + const namespaceStore = useStore(NamespaceService) + const projectService = useService(ProjectService) + const projectStore = useStore(ProjectService) + const flowService = useService(FlowService) + const flowStore = useStore(FlowService) + + const userSession = useUserSession() + const currentUser = React.useMemo(() => userService.getById(userSession?.user?.id), [userStore, userSession]) + + const memberships = React.useMemo( + () => currentUser?.namespaceMemberships?.nodes ?? [], + [currentUser?.namespaceMemberships?.nodes?.length] + ) + + const namespaces = React.useMemo( + () => memberships + .map(membership => namespaceService.getById(membership?.namespace?.id)) + .filter((namespace): namespace is Namespace => !!namespace), + [memberships.length, namespaceStore] + ) + + const projects = React.useMemo( + () => namespaces.flatMap(namespace => projectService.values({namespaceId: namespace.id})), + [namespaces, projectStore] + ) + + const flows = React.useMemo( + () => projects.flatMap(project => flowService.values({ + namespaceId: project.namespace?.id, + projectId: project.id, + })), + [projects, flowStore] + ) + + const workspacesCount = namespaces.length + + const projectsCount = React.useMemo( + () => namespaces.reduce((sum, namespace) => sum + (namespace.projects?.count ?? 0), 0), + [namespaces] + ) + + // Saved work = executed nodes across a set of flows, pressed into whole minutes. + const savedMinutesOf = (flowSet: typeof flows) => Math.floor(flowSet.reduce((sum, flow) => + sum + (flow.nodes?.count ?? 0) * (flow.executionResults?.count ?? 0), 0) + * SAVED_SECONDS_PER_NODE_RUN / 60) + + const savedMinutes = React.useMemo(() => savedMinutesOf(flows), [flows]) + + const invalidFlows = React.useMemo( + () => flows.filter(flow => flow.validationStatus === "INVALID"), + [flows] + ) + + // Each badge shows the amount added within the last 30 days ("new this month"). + const trends = React.useMemo(() => { + const recentFlows = flows.filter(flow => withinDays(flow.createdAt, 30)) + return { + workspaces: namespaces.filter(namespace => withinDays(namespace.createdAt, 30)).length, + projects: projects.filter(project => withinDays(project.createdAt, 30)).length, + savedMinutes: savedMinutesOf(recentFlows), + invalidFlows: recentFlows.filter(flow => flow.validationStatus === "INVALID").length, + } + }, [namespaces, projects, flows]) + + const stats: { label: string, value: number, trend: number, tone: "success" | "warning" }[] = [ + {label: "Workspaces", value: workspacesCount, trend: trends.workspaces, tone: "success"}, + {label: "Projects", value: projectsCount, trend: trends.projects, tone: "success"}, + {label: "Saved minutes", value: savedMinutes, trend: trends.savedMinutes, tone: "success"}, + {label: "Invalid flows", value: invalidFlows.length, trend: trends.invalidFlows, tone: "warning"}, + ] + + return + + {stats.map((stat, index) => ( + + + + {stat.label} + {stat.value !== 0 && ( + + {stat.tone === "warning" + ? + : } + {stat.trend} + + )} + + {stat.value} + + {index < stats.length - 1 && ( +
+ )} + + ))} + + +} diff --git a/src/packages/ce/src/application/views/ApplicationTabListView.tsx b/src/packages/ce/src/application/views/ApplicationTabListView.tsx deleted file mode 100644 index f98cf985..00000000 --- a/src/packages/ce/src/application/views/ApplicationTabListView.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from "react"; -import {TabList, TabTrigger} from "@code0-tech/pictor/dist/components/tab/Tab"; -import {Button, Text} from "@code0-tech/pictor"; - -export const ApplicationTabListView: React.FC = () => { - return - - - - - - - -} \ No newline at end of file diff --git a/src/packages/ce/src/application/views/ApplicationTabView.tsx b/src/packages/ce/src/application/views/ApplicationTabView.tsx deleted file mode 100644 index e0397fc9..00000000 --- a/src/packages/ce/src/application/views/ApplicationTabView.tsx +++ /dev/null @@ -1,21 +0,0 @@ -"use client" - -import React from "react"; -import {Button} from "@code0-tech/pictor"; -import {IconBuilding, IconHome} from "@tabler/icons-react"; -import {useRouter} from "next/navigation"; - -export const ApplicationTabView: React.FC = () => { - - const router = useRouter() - - return <> - - - -} diff --git a/src/packages/ce/src/application/views/ApplicationUsersView.tsx b/src/packages/ce/src/application/views/ApplicationUsersView.tsx new file mode 100644 index 00000000..0e51448c --- /dev/null +++ b/src/packages/ce/src/application/views/ApplicationUsersView.tsx @@ -0,0 +1,128 @@ +"use client" + +import React from "react"; +import { + Badge, + Button, + ButtonGroup, + Flex, + Menu, + MenuContent, + MenuItem, + MenuLabel, + MenuPortal, + MenuTrigger, + Spacing, + Text, + useService, + useStore +} from "@code0-tech/pictor"; +import {TabContent} from "@code0-tech/pictor/dist/components/tab/Tab"; +import {DataTableSortProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; +import {IconAdjustmentsHorizontal, IconArrowsSort, IconCheck, IconPlus} from "@tabler/icons-react"; +import Link from "next/link"; +import {useRouter} from "next/navigation"; +import {User} from "@code0-tech/sagittarius-graphql-types"; +import {UserService} from "@edition/user/services/User.service"; +import {UserDataTableComponent} from "@edition/user/components/UserDataTableComponent"; + +const filterLabels = { + all: "All", + admins: "Admins", + blocked: "Blocked", +} as const + +const sortLabels = { + username: "Username", + createdAt: "Created", + updatedAt: "Updated", +} as const + +export const ApplicationUsersView: React.FC = () => { + + const router = useRouter() + const userService = useService(UserService) + const userStore = useStore(UserService) + + const users = React.useMemo(() => userService.values(), [userStore]) + + const [filter, setFilter] = React.useState("all") + const [sort, setSort] = React.useState("username") + + const preFilter = React.useCallback((user: User) => { + if (filter === "admins") return !!user.admin + if (filter === "blocked") return !!user.blocked + return true + }, [filter]) + + const visibleCount = React.useMemo(() => users.filter(preFilter).length, [users, preFilter]) + + const sortProps: DataTableSortProps = {[sort]: "asc"} + + return + + + Users + {visibleCount} + + + {/* Filter */} + + + + + + + Filter + {(Object.keys(filterLabels) as (keyof typeof filterLabels)[]).map(k => ( + setFilter(k)}> + + {filterLabels[k]} + + ))} + + + + + {/* Sort */} + + + + + + + Sort by + {(Object.keys(sortLabels) as (keyof typeof sortLabels)[]).map(k => ( + setSort(k)}> + + {sortLabels[k]} + + ))} + + + + + {/* Invite */} + + + + + + + + Manage users who have access to your instance. You can invite new users and remove existing ones. + + + { + if (item?.id) router.push(`/users/${item.id.split("/").pop()}/settings`) + }}/> + +} diff --git a/src/packages/ce/src/datatype/components/inputs/DataTypeInputControlsComponent.tsx b/src/packages/ce/src/datatype/components/inputs/DataTypeInputControlsComponent.tsx index 6bebde52..c9376dfd 100644 --- a/src/packages/ce/src/datatype/components/inputs/DataTypeInputControlsComponent.tsx +++ b/src/packages/ce/src/datatype/components/inputs/DataTypeInputControlsComponent.tsx @@ -187,7 +187,7 @@ export const DataTypeInputControlsComponent: React.FC - @@ -258,7 +258,7 @@ export const DataTypeInputControlsComponent: React.FC ) : <>} -
) } @@ -139,7 +139,7 @@ export const DataTypeJSONInputTreeComponent: React.FC ) : null return ( -
  • +
  • { e.stopPropagation() @@ -162,5 +162,5 @@ export const DataTypeJSONInputTreeComponent: React.FC{validNodes} + return
      {validNodes}
    } \ No newline at end of file diff --git a/src/packages/ce/src/datatype/components/inputs/select/DataTypeSelectInputComponent.tsx b/src/packages/ce/src/datatype/components/inputs/select/DataTypeSelectInputComponent.tsx index 2d276fa5..0eeedf84 100644 --- a/src/packages/ce/src/datatype/components/inputs/select/DataTypeSelectInputComponent.tsx +++ b/src/packages/ce/src/datatype/components/inputs/select/DataTypeSelectInputComponent.tsx @@ -3,6 +3,7 @@ import React from "react"; import {useDebouncedCallback} from "use-debounce"; import { Button, + ButtonGroup, Flex, InputDescription, InputLabel, @@ -48,16 +49,18 @@ export const DataTypeSelectInputComponent: React.FC { - formValidation?.setValue?.(null) - onChangeDebounced(null) - }} paddingSize={"xxs"}> - - + + + } rightType={"action"}> - + diff --git a/src/packages/ce/src/datatype/components/inputs/type/DataTypeTypeInputComponent.style.scss b/src/packages/ce/src/datatype/components/inputs/type/DataTypeTypeInputComponent.style.scss index 65f252f9..953796f9 100644 --- a/src/packages/ce/src/datatype/components/inputs/type/DataTypeTypeInputComponent.style.scss +++ b/src/packages/ce/src/datatype/components/inputs/type/DataTypeTypeInputComponent.style.scss @@ -2,7 +2,7 @@ @use "@core/style/box"; @use "@core/style/variables"; -ul { +.json-tree { list-style: none; margin: 0; margin-inline-start: 2px; @@ -15,7 +15,7 @@ ul { gap: variables.$xxs; } -li { +.json-tree__item { position: relative; padding-inline-start: variables.$xs; list-style: none; diff --git a/src/packages/ce/src/flow/components/FlowCreateDialogComponent.tsx b/src/packages/ce/src/flow/components/FlowCreateDialogComponent.tsx index 56da13ac..95b948c1 100644 --- a/src/packages/ce/src/flow/components/FlowCreateDialogComponent.tsx +++ b/src/packages/ce/src/flow/components/FlowCreateDialogComponent.tsx @@ -41,7 +41,7 @@ import {FlowService} from "@edition/flow/services/Flow.service"; import Link from "next/link"; import {icon, IconString} from "@core/util/icons"; import {FALLBACK_FLOW_TYPE_NAME} from "@core/util/fallback-translations"; -import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {toast} from "@code0-tech/pictor/dist/components/toast/Toast"; export interface FlowCreateDialogComponentProps { open?: boolean @@ -159,9 +159,7 @@ export const FlowCreateDialogComponent: React.FC projectId: projectId }).then(payload => { if ((payload?.errors?.length ?? 0) <= 0) { - addIslandSuccessNotification({ - message: "Created flow" - }) + toast({title: "Created flow", color: "success"}) } }) @@ -213,22 +211,17 @@ export const FlowCreateDialogComponent: React.FC )} - - - - - - - - - - - - + + + + + + + diff --git a/src/packages/ce/src/flow/components/FlowWorkerProvider.tsx b/src/packages/ce/src/flow/components/FlowWorkerProvider.tsx index 75bb2223..e2e5e808 100644 --- a/src/packages/ce/src/flow/components/FlowWorkerProvider.tsx +++ b/src/packages/ce/src/flow/components/FlowWorkerProvider.tsx @@ -90,10 +90,15 @@ export const WorkerProvider: React.FC<{ children: React.ReactNode }> = ({childre const workerRef = React.useRef(null) const resolvers = React.useRef>(new Map()) - React.useEffect(() => { - workerRef.current = new Worker(new URL('./Flow.worker.js', import.meta.url)) - - workerRef.current.onmessage = (e) => { + // Lazily (re)create the worker. Runs client-side only. This must not depend on + // effect ordering: child components may trigger worker actions in their own mount + // effects, which fire before this provider's effect (React runs effects child-first). + const ensureWorker = React.useCallback((): Worker | null => { + if (typeof Worker === "undefined") return null + if (workerRef.current) return workerRef.current + + const worker = new Worker(new URL('./Flow.worker.js', import.meta.url)) + worker.onmessage = (e) => { const {id, data, error} = e.data const deferred = resolvers.current.get(id) @@ -107,20 +112,30 @@ export const WorkerProvider: React.FC<{ children: React.ReactNode }> = ({childre } } - return () => workerRef.current?.terminate() + workerRef.current = worker + return worker }, []) + React.useEffect(() => { + ensureWorker() + return () => { + workerRef.current?.terminate() + workerRef.current = null + } + }, [ensureWorker]) + const calculate = React.useCallback((action: FlowWorkerActions, payload: FlowWorkerPayload) => { return new Promise((resolve, reject) => { - if (!workerRef.current) { + const worker = ensureWorker() + if (!worker) { return reject("Worker ist nicht initialisiert"); } const id = Math.random().toString(36).substring(2, 9); resolvers.current.set(id, {resolve, reject}); - workerRef.current.postMessage({id, action, payload}); + worker.postMessage({id, action, payload}); }); - }, []); + }, [ensureWorker]); return ( diff --git a/src/packages/ce/src/flow/components/folder/FlowFolderComponent.tsx b/src/packages/ce/src/flow/components/folder/FlowFolderComponent.tsx index 3021a991..a4a79be6 100644 --- a/src/packages/ce/src/flow/components/folder/FlowFolderComponent.tsx +++ b/src/packages/ce/src/flow/components/folder/FlowFolderComponent.tsx @@ -265,7 +265,7 @@ export const DFlowFolderGroup: React.FC = (props) }} {...contextMenuProps}>
    setOpen(prevState => !prevState)} {...mergeComponentProps(`d-folder`, code0Props)}> - {open ? : } + {open ? : } {name}
    @@ -335,7 +335,7 @@ export const DFlowFolderItem: React.FC = (props) = ref={wrapperRef} {...mergeComponentProps(`d-folder__item ${active ? "d-folder__item--active" : ""}`, code0Props)} onClick={() => onSelect?.(flow)}> - + {text}
    @@ -345,7 +345,7 @@ export const DFlowFolderItem: React.FC = (props) =
    onSelect?.(flow)} style={{padding: "0.35rem 0.7rem"}}> - + {props.name}
    diff --git a/src/packages/ce/src/flow/components/folder/FlowFolderContextMenuComponent.tsx b/src/packages/ce/src/flow/components/folder/FlowFolderContextMenuComponent.tsx index 3ea47cb5..4c9b4ac9 100644 --- a/src/packages/ce/src/flow/components/folder/FlowFolderContextMenuComponent.tsx +++ b/src/packages/ce/src/flow/components/folder/FlowFolderContextMenuComponent.tsx @@ -16,6 +16,7 @@ import { } from "@code0-tech/pictor/dist/components/context-menu/ContextMenu"; import {Flex, Text, useService, useStore} from "@code0-tech/pictor"; import {FlowTypeService} from "@edition/flowtype/services/FlowType.service"; +import {ProjectService} from "@edition/project/services/Project.service"; import {FALLBACK_FLOW_TYPE_NAME} from "@core/util/fallback-translations"; export interface FlowFolderContextMenuComponentGroupData { @@ -37,12 +38,26 @@ export interface FlowFolderContextMenuComponentProps extends FlowFolderComponent export const FlowFolderContextMenuComponent: React.FC = (props) => { - const {children} = props + const {children, namespaceId, projectId} = props const flowTypeService = useService(FlowTypeService) const flowTypeStore = useStore(FlowTypeService) + const projectService = useService(ProjectService) + const projectStore = useStore(ProjectService) - const flowTypes = React.useMemo(() => flowTypeService.values(), [flowTypeStore]) + const project = React.useMemo( + () => projectService.getById(projectId, {namespaceId}), + [projectStore, projectId, namespaceId] + ) + + // Scope the flow types to this project's primary runtime, otherwise the store + // still holds the previously visited project's flow types. Without a runtime + // there is nothing to create, and values() would fall back to the whole store. + const flowTypes = React.useMemo(() => { + const runtimeId = project?.primaryRuntime?.id + if (!runtimeId) return [] + return flowTypeService.values({runtimeId, projectId, namespaceId}) + }, [flowTypeStore, project, projectId, namespaceId]) return <> diff --git a/src/packages/ce/src/flow/components/panels/FlowPanelControlComponent.tsx b/src/packages/ce/src/flow/components/panels/FlowPanelControlComponent.tsx index c283d78d..a3fc0931 100644 --- a/src/packages/ce/src/flow/components/panels/FlowPanelControlComponent.tsx +++ b/src/packages/ce/src/flow/components/panels/FlowPanelControlComponent.tsx @@ -33,7 +33,7 @@ import {HoverCard, HoverCardContent, HoverCardPortal, HoverCardTrigger} from "@r import 'ldrs/react/ChaoticOrbit.css' import {AIChatComponent} from "@edition/ai/components/AIChatComponent"; import {mapAiGenerationFlowToFlowInput} from "@edition/ai/util/AI.flow.mapper"; -import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {toast} from "@code0-tech/pictor/dist/components/toast/Toast"; import {useFlowCompareStore} from "@edition/flow/hooks/Flow.compare.hook"; import {FlowView} from "@edition/flow/services/Flow.view"; import {FlowExecuteDialogComponent} from "@edition/flow/components/FlowExecuteDialogComponent"; @@ -96,7 +96,7 @@ export const FlowPanelControlComponent: React.FC }, true).then(result => { if ((result?.errors?.length ?? 0) <= 0) { setCompareFlow(oldFlowSnapshot) - addIslandSuccessNotification({message: "Updated flow"}) + toast({title: "Updated flow", color: "success"}) } }) }, [flowService, flowStore, namespaceId, projectId, flowId, setCompareFlow]) diff --git a/src/packages/ce/src/flow/components/panels/FlowPanelUpdateComponent.tsx b/src/packages/ce/src/flow/components/panels/FlowPanelUpdateComponent.tsx index 9f1f71aa..e72e5745 100644 --- a/src/packages/ce/src/flow/components/panels/FlowPanelUpdateComponent.tsx +++ b/src/packages/ce/src/flow/components/panels/FlowPanelUpdateComponent.tsx @@ -15,7 +15,7 @@ import { useStore } from "@code0-tech/pictor"; import {FlowService} from "@edition/flow/services/Flow.service"; -import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {toast} from "@code0-tech/pictor/dist/components/toast/Toast"; export interface FlowPanelUpdateComponentProps { flowId: Flow['id'] @@ -52,9 +52,7 @@ export const FlowPanelUpdateComponent: React.FC = flowId: flowId! }).then(payload => { if ((payload?.errors?.length ?? 0) <= 0) { - addIslandSuccessNotification({ - message: "Synced flow" - }) + toast({title: "Synced flow", color: "success"}) } }) diff --git a/src/packages/ce/src/flow/layout/FlowLayout.tsx b/src/packages/ce/src/flow/layout/FlowLayout.tsx deleted file mode 100644 index 9698c4ed..00000000 --- a/src/packages/ce/src/flow/layout/FlowLayout.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react"; -import {ResizableHandle, ResizablePanelGroup} from "@code0-tech/pictor/dist/components/resizable/Resizable"; -import {SidebarComponent} from "@core/components/SidebarComponent"; -import {FlowFolderView} from "@edition/flow/views/FlowFolderView"; -import {WorkerProvider} from "@edition/flow/components/FlowWorkerProvider"; -import {ReactFlowProvider} from "@xyflow/react"; - -interface FlowLayoutProps { - children: React.ReactNode -} - -export const FlowLayout: React.FC = ({children}) => { - - return - - - - - - - {children} - - - -} \ No newline at end of file diff --git a/src/packages/ce/src/flow/views/FlowFolderView.tsx b/src/packages/ce/src/flow/views/FlowFolderView.tsx index b8270c3c..8df6775d 100644 --- a/src/packages/ce/src/flow/views/FlowFolderView.tsx +++ b/src/packages/ce/src/flow/views/FlowFolderView.tsx @@ -4,6 +4,7 @@ import React from "react"; import { Button, Flex, + Spacing, Text, Tooltip, TooltipArrow, @@ -12,7 +13,7 @@ import { TooltipTrigger, useService } from "@code0-tech/pictor"; -import {IconArrowsMaximize, IconArrowsMinimize, IconCircleDot, IconPlus} from "@tabler/icons-react"; +import {IconApps, IconArrowsMaximize, IconArrowsMinimize, IconCircleDot, IconPlus} from "@tabler/icons-react"; import {useParams, useRouter} from "next/navigation"; import {Flow, FlowType} from "@code0-tech/sagittarius-graphql-types"; import {FlowService} from "@edition/flow/services/Flow.service"; @@ -24,8 +25,8 @@ import { FlowFolderContextMenuComponentGroupData, FlowFolderContextMenuComponentItemData } from "@edition/flow/components/folder/FlowFolderContextMenuComponent"; -import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout"; -import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {toast} from "@code0-tech/pictor/dist/components/toast/Toast"; +import Link from "next/link"; export const FlowFolderView: React.FC = () => { @@ -55,10 +56,8 @@ export const FlowFolderView: React.FC = () => { flowId: flow.id! }).then(payload => { if ((payload?.errors?.length ?? 0) <= 0) { - addIslandSuccessNotification({ - message: "Deleted flow" - }) - router.push(`/namespace/${namespaceIndex}/project/${projectIndex}/flow`) + toast({title: "Deleted flow", color: "success"}) + router.push(`/namespace/${namespaceIndex}/project/${projectIndex}`) } }) }, [flowService]) @@ -73,80 +72,88 @@ export const FlowFolderView: React.FC = () => { onOpenChange={(open) => setDeleteDialogOpen(open)} contextData={contextData} onDelete={deleteFlow}/> - - - - - - - - - - - - Open active flow - - - - - - - - - - - Close all - - - - - - - - - - - Open all - - - - - - - - }> - { - const number = flow.id?.match(/Flow\/(\d+)$/)?.[1] - router.push(`/namespace/${namespaceIndex}/project/${projectIndex}/flow/${number}`) - }} - onCreate={flowTypeId => { - setCreateDialogOpen(true) - setFlowTypeId(flowTypeId) - }} - onDelete={contextData => { - setDeleteDialogOpen(true) - setContextData(contextData) - }} - namespaceId={`gid://sagittarius/Namespace/${namespaceIndex}`} - projectId={`gid://sagittarius/NamespaceProject/${projectIndex}`}/> - + + + Explorer + + + + + + + + + Open active flow + + + + + + + + + + + Close all + + + + + + + + + + + Open all + + + + + + + + + + + + +
    + + { + const number = flow.id?.match(/Flow\/(\d+)$/)?.[1] + router.push(`/namespace/${namespaceIndex}/project/${projectIndex}/flow/${number}`) + }} + onCreate={flowTypeId => { + setCreateDialogOpen(true) + setFlowTypeId(flowTypeId) + }} + onDelete={contextData => { + setDeleteDialogOpen(true) + setContextData(contextData) + }} + namespaceId={`gid://sagittarius/Namespace/${namespaceIndex}`} + projectId={`gid://sagittarius/NamespaceProject/${projectIndex}`}/> } \ No newline at end of file diff --git a/src/packages/ce/src/flow/views/FlowRecentListView.tsx b/src/packages/ce/src/flow/views/FlowRecentListView.tsx new file mode 100644 index 00000000..e43f586e --- /dev/null +++ b/src/packages/ce/src/flow/views/FlowRecentListView.tsx @@ -0,0 +1,93 @@ +"use client" + +import React from "react"; +import {Badge, Button, Flex, hashToColor, Spacing, Text, useService, useStore} from "@code0-tech/pictor"; +import Link from "next/link"; +import {useParams} from "next/navigation"; +import {Flow, Namespace, NamespaceProject} from "@code0-tech/sagittarius-graphql-types"; +import {FlowService} from "@edition/flow/services/Flow.service"; +import {ProjectService} from "@edition/project/services/Project.service"; +import {FlowTypeService} from "@edition/flowtype/services/FlowType.service"; +import {icon, IconString} from "@core/util/icons"; + +const RECENT_FLOWS_LIMIT = 10 + +export const FlowRecentListView: React.FC = () => { + + const params = useParams() + const flowService = useService(FlowService) + const flowStore = useStore(FlowService) + const projectService = useService(ProjectService) + const projectStore = useStore(ProjectService) + + const namespaceIndex = params.namespaceId as any as number + const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` + + const projects = React.useMemo( + () => projectService.values({namespaceId}) + .filter((project): project is NamespaceProject => !!project), + [namespaceId, projectStore] + ) + + const flows = React.useMemo( + () => projects + .flatMap(project => project.id + ? flowService.values({namespaceId, projectId: project.id}) + .map(flow => ({flow, project})) + : []) + .filter((entry): entry is {flow: Flow, project: NamespaceProject} => !!entry.flow?.name) + .sort((a, b) => new Date(String(b.flow.updatedAt)).getTime() - new Date(String(a.flow.updatedAt)).getTime()) + .slice(0, RECENT_FLOWS_LIMIT), + [projects, flowStore] + ) + + return + + + Recent workflows + + + {flows.length} + + + + {flows.map(({flow, project}) => { + const projectIndex = project.id?.match(/NamespaceProject\/(\d+)$/)?.[1] + const flowIndex = flow.id?.match(/Flow\/(\d+)$/)?.[1] + const name = flow.name ?? "" + const displayName = name.split("/").filter(Boolean).pop() ?? name + return + + + })} + +} + +const FlowRecentListIcon: React.FC<{flow: Flow, project: NamespaceProject}> = ({flow, project}) => { + + const flowTypeService = useService(FlowTypeService) + const flowTypeStore = useStore(FlowTypeService) + + const flowType = React.useMemo(() => { + // Populate the flow types for this project's primary runtime, the same way + // the flow builder does, so getById can resolve the display icon. + flowTypeService.values({ + runtimeId: project.primaryRuntime?.id, + projectId: project.id, + namespaceId: project.namespace?.id, + }) + return flowTypeService.getById(flow.type?.id) + }, [flowTypeStore, flow, project]) + + const DisplayIcon = icon(flowType?.displayIcon as IconString) + + return +} diff --git a/src/packages/ce/src/function/components/files/FunctionFileDefaultComponent.tsx b/src/packages/ce/src/function/components/files/FunctionFileDefaultComponent.tsx index a93b2e96..709086c2 100644 --- a/src/packages/ce/src/function/components/files/FunctionFileDefaultComponent.tsx +++ b/src/packages/ce/src/function/components/files/FunctionFileDefaultComponent.tsx @@ -107,7 +107,9 @@ export const FunctionFileDefaultComponent: React.FC + return
    {definition?.names?.[0]?.content ?? FALLBACK_FUNCTION_NAME} {definition?.descriptions?.[0]?.content ?? FALLBACK_FUNCTION_DESCRIPTION} @@ -172,5 +174,5 @@ export const FunctionFileDefaultComponent: React.FC })()} - +
    } diff --git a/src/packages/ce/src/function/components/files/FunctionFileTriggerComponent.tsx b/src/packages/ce/src/function/components/files/FunctionFileTriggerComponent.tsx index b1ab42c7..2d399d7b 100644 --- a/src/packages/ce/src/function/components/files/FunctionFileTriggerComponent.tsx +++ b/src/packages/ce/src/function/components/files/FunctionFileTriggerComponent.tsx @@ -105,7 +105,9 @@ export const FunctionFileTriggerComponent: React.FC + return
    {definition?.names?.[0]?.content ?? FALLBACK_FLOW_TYPE_NAME} {definition?.descriptions?.[0]?.content ?? FALLBACK_FLOW_TYPE_DESCRIPTION} @@ -169,5 +171,5 @@ export const FunctionFileTriggerComponent: React.FC })()} - +
    }) diff --git a/src/packages/ce/src/function/components/files/FunctionFilesComponent.tsx b/src/packages/ce/src/function/components/files/FunctionFilesComponent.tsx index 7c31ecb4..da7e4baf 100644 --- a/src/packages/ce/src/function/components/files/FunctionFilesComponent.tsx +++ b/src/packages/ce/src/function/components/files/FunctionFilesComponent.tsx @@ -172,6 +172,7 @@ export const FunctionFilesComponent: React.FC = (pr } = (pr { nodes?.map(node => { return diff --git a/src/packages/ce/src/function/components/nodes/FunctionNodeDefaultComponent.tsx b/src/packages/ce/src/function/components/nodes/FunctionNodeDefaultComponent.tsx index b41e9455..fd788e33 100644 --- a/src/packages/ce/src/function/components/nodes/FunctionNodeDefaultComponent.tsx +++ b/src/packages/ce/src/function/components/nodes/FunctionNodeDefaultComponent.tsx @@ -4,7 +4,7 @@ import "./FunctionNodeComponent.style.scss"; import {FunctionNodeComponentProps} from "./FunctionNodeComponent"; import {Badge, Button, ButtonGroup, Card, Flex, Text, useService, useStore as usePictorStore} from "@code0-tech/pictor"; import {useFlowValidation} from "@edition/flow/hooks/Flow.validation.hook"; -import {IconBackspace, IconCircleX, IconEdit, IconSparkles, IconVariable} from "@tabler/icons-react"; +import {IconBackspace, IconPencil, IconSparkles, IconVariable} from "@tabler/icons-react"; import {FlowService} from "@edition/flow/services/Flow.service"; import {FunctionService} from "@edition/function/services/Function.service"; import {LiteralBadgeComponent} from "@edition/datatype/components/badges/LiteralBadgeComponent"; @@ -162,7 +162,7 @@ export const FunctionNodeDefaultComponent: React.FC - + + +} diff --git a/src/packages/ce/src/member/components/MemberDataTableFilterInputComponent.tsx b/src/packages/ce/src/member/components/MemberDataTableFilterInputComponent.tsx deleted file mode 100644 index 5a921b30..00000000 --- a/src/packages/ce/src/member/components/MemberDataTableFilterInputComponent.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React from "react"; -import { - DataTableFilterInput, - DataTableFilterSuggestionMenu, - MenuCheckboxItem, - useService, - useStore -} from "@code0-tech/pictor"; -import {IconCheck} from "@tabler/icons-react"; -import {Namespace} from "@code0-tech/sagittarius-graphql-types"; -import {DataTableFilterProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; -import {RoleService} from "@edition/role/services/Role.service"; -import {MemberService} from "@edition/member/services/Member.service"; - -export interface MemberDataTableFilterInputComponentProps { - namespaceId: Namespace['id'] - onChange: (filter: DataTableFilterProps) => void -} - -export const MemberDataTableFilterInputComponent: React.FC = (props) => { - - const {namespaceId, onChange} = props - - const memberService = useService(MemberService) - const memberStore = useStore(MemberService) - const roleService = useService(RoleService) - const roleStore = useStore(RoleService) - - const members = React.useMemo( - () => memberService.values({namespaceId: namespaceId}), - [memberStore] - ) - - const roles = React.useMemo( - () => { - const roleIds = members.map(m => m.roles?.nodes?.map(r => r?.id)).flat() - return roleIds.map(id => roleService.getById(id)).filter(Boolean) - }, - [roleStore] - ) - - return { - - const split = currentValue.split(",").map(s => s.trim()).filter(Boolean) - - return - {members.map(member => { - - const isChecked = split.includes(member?.user?.username!) - - return { - e.preventDefault(); - e.stopPropagation(); - const updated = isChecked - ? split.filter(name => name !== member?.user?.username) - : [...split, member?.user?.username]; - applySuggestion(updated.join(","), true); - }}> - {isChecked ? : } - @{member?.user?.username} - - })} - - } - }, - { - token: "Roles", - key: "roles.nodes.name", - operators: ["isOneOf"], - suggestion: (context, operator, currentValue, applySuggestion) => { - - const split = currentValue.split(",").map(s => s.trim()).filter(Boolean) - - return - {roles.map(role => { - - const isChecked = split.includes(role?.name!) - - return { - e.preventDefault(); - e.stopPropagation(); - const updated = isChecked - ? split.filter(name => name !== role?.name) - : [...split, role?.name]; - applySuggestion(updated.join(","), true); - }}> - {isChecked ? : } - {role?.name} - - })} - - } - } - ]}/> -} \ No newline at end of file diff --git a/src/packages/ce/src/member/components/MemberDataTableRowComponent.tsx b/src/packages/ce/src/member/components/MemberDataTableRowComponent.tsx index 1ce67a0f..b5a193b5 100644 --- a/src/packages/ce/src/member/components/MemberDataTableRowComponent.tsx +++ b/src/packages/ce/src/member/components/MemberDataTableRowComponent.tsx @@ -32,7 +32,7 @@ import {MemberService} from "@edition/member/services/Member.service"; import {RoleService} from "@edition/role/services/Role.service"; import {RolePermissionComponent} from "@edition/role/components/RolePermissionComponent"; import {IconDotsVertical, IconUserCog, IconUserOff, IconX} from "@tabler/icons-react"; -import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {toast} from "@code0-tech/pictor/dist/components/toast/Toast"; export interface MemberDataTableRowComponentProps { memberId: NamespaceMember['id'] @@ -83,9 +83,7 @@ export const MemberDataTableRowComponent: React.FC r?.id!) }).then(payload => { if ((payload?.errors?.length ?? 0) <= 0) { - addIslandSuccessNotification({ - message: "Updated roles on member" - }) + toast({title: "Updated roles on member", color: "success"}) } }) @@ -96,9 +94,7 @@ export const MemberDataTableRowComponent: React.FC { if ((payload?.errors?.length ?? 0) <= 0) { - addIslandSuccessNotification({ - message: "Removed member" - }) + toast({title: "Removed member", color: "success"}) } }) diff --git a/src/packages/ce/src/member/pages/MemberAddPage.tsx b/src/packages/ce/src/member/pages/MemberAddPage.tsx index 29c365ca..7cd93663 100644 --- a/src/packages/ce/src/member/pages/MemberAddPage.tsx +++ b/src/packages/ce/src/member/pages/MemberAddPage.tsx @@ -1,103 +1,32 @@ "use client" import React from "react"; -import {Button, Col, Flex, Spacing, Text, useForm, useService, useStore} from "@code0-tech/pictor"; -import {MemberService} from "@edition/member/services/Member.service"; import {useParams, useRouter} from "next/navigation"; -import {Namespace, User} from "@code0-tech/sagittarius-graphql-types"; -import Link from "next/link"; -import {UserService} from "@edition/user/services/User.service"; -import {UserInputComponent, UserSyntaxSegment} from "@edition/user/components/UserInputComponent"; -import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {Namespace} from "@code0-tech/sagittarius-graphql-types"; +import {MemberAddDialogComponent} from "@edition/member/components/MemberAddDialogComponent"; export const MemberAddPage: React.FC = () => { const params = useParams() - const memberService = useService(MemberService) - const memberStore = useStore(MemberService) - const userService = useService(UserService) - const userStore = useStore(UserService) const router = useRouter() - const [, startTransition] = React.useTransition() - const namespaceIndex = params?.namespaceId as string - const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex as unknown as number}` - - const members = React.useMemo(() => memberService.values({namespaceId: namespaceId}), [memberStore, userStore]) - const formInitialValues = React.useMemo(() => ({users: null}), []) - const filteredUsers = React.useMemo(() => { - return (user: User) => { - return !members.find(m => m.user?.id === user.id) - } - }, [members]) - - const [inputs, validate] = useForm<{ users: null | UserSyntaxSegment[] }>({ - useInitialValidation: false, - initialValues: formInitialValues, - validate: { - users: (value) => { - if (!value) return "Please select at least one user" - if (value.filter(segment => segment.type === "block").length <= 0) return "Please select at least one user" - return null - } - }, - onSubmit: (values) => { - startTransition(async () => { - - const payload = await memberService.memberBulkInvite({ - namespaceId: namespaceId!!, - userIds: values.users!! - .filter(segment => segment.type === "block") - .map(segment => segment.valueData!!.id!!) - }) - - if ((payload?.errors?.length ?? 0) <= 0) { - router.push(`/namespace/${namespaceIndex}/members`) - addIslandSuccessNotification({ - message: "Added members", - }) - } - }) - } - }) - - //TODO: user abilities for add user as member within namespace - - return
    - - - - Add new members - - - - Global runtimes are shared runtimes that can be used across multiple organizations. - - - {/*@ts-ignore*/} - - - - - - - - - - -
    -} \ No newline at end of file + const namespaceIndex = params.namespaceId as any as number + const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` + const settingsHref = `/namespace/${namespaceIndex}/settings` + + return { + if (open) return + + const nav = (window as unknown as { navigation?: { entries(): { url: string }[], currentEntry?: { index: number } } }).navigation + if (!nav?.entries) { + router.back() + return + } + + const index = nav.currentEntry?.index ?? 0 + if (index > 0) router.back() + else router.push(settingsHref) + }}/> +} diff --git a/src/packages/ce/src/member/pages/MembersPage.tsx b/src/packages/ce/src/member/pages/MembersPage.tsx deleted file mode 100644 index 950a4ae3..00000000 --- a/src/packages/ce/src/member/pages/MembersPage.tsx +++ /dev/null @@ -1,16 +0,0 @@ -"use client" - -import {MembersView} from "@edition/member/views/MembersView"; -import React from "react"; - -export const MembersPage: React.FC = () => { - return
    - -
    -} \ No newline at end of file diff --git a/src/packages/ce/src/member/views/MembersView.tsx b/src/packages/ce/src/member/views/MembersView.tsx index 41a4fa67..9bd258f3 100644 --- a/src/packages/ce/src/member/views/MembersView.tsx +++ b/src/packages/ce/src/member/views/MembersView.tsx @@ -2,109 +2,167 @@ import React from "react"; import {useParams} from "next/navigation"; -import {Button, DataTableFilterProps, DataTableSortProps, Flex, Spacing, Text, ButtonGroup, Menu, MenuTrigger, MenuPortal, MenuContent, MenuCheckboxItem} from "@code0-tech/pictor"; +import { + Badge, + Button, + ButtonGroup, + Flex, + Menu, + MenuCheckboxItem, + MenuContent, + MenuItem, + MenuLabel, + MenuPortal, + MenuTrigger, + Spacing, + Text, + useService, + useStore +} from "@code0-tech/pictor"; +import {DataTableSortProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; +import {IconAdjustmentsHorizontal, IconArrowsSort, IconCheck, IconPlus} from "@tabler/icons-react"; import Link from "next/link"; +import {Namespace, NamespaceMember} from "@code0-tech/sagittarius-graphql-types"; +import {MemberService} from "@edition/member/services/Member.service"; import {MemberDataTableComponent} from "@edition/member/components/MemberDataTableComponent"; -import {MemberDataTableFilterInputComponent} from "@edition/member/components/MemberDataTableFilterInputComponent"; -import {IconMinus, IconSortAscending, IconSortDescending} from "@tabler/icons-react"; +import {RoleService} from "@edition/role/services/Role.service"; + +const sortLabels = { + "user.username": "Username", + createdAt: "Created", + updatedAt: "Updated", +} as const + +const toggle = (value: string, setter: React.Dispatch>) => + setter(prev => prev.includes(value) ? prev.filter(v => v !== value) : [...prev, value]) //TODO: user abilities for add user as member within namespace export const MembersView: React.FC = () => { const params = useParams() - const namespaceId = params.namespaceId as any as number + const memberService = useService(MemberService) + const memberStore = useStore(MemberService) + const roleService = useService(RoleService) + const roleStore = useStore(RoleService) - const [filter, setFilter] = React.useState({}) - const [sort, setSort] = React.useState({}) + const namespaceIndex = params.namespaceId as any as number + const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` - return <> + const members = React.useMemo( + () => memberService.values({namespaceId: namespaceId}), + [memberStore, namespaceId] + ) + + const roles = React.useMemo( + () => roleService.values({namespaceId: namespaceId}), + [roleStore, namespaceId] + ) + + const usernames = React.useMemo( + () => members.map(m => m?.user?.username).filter(Boolean) as string[], + [members] + ) + + const [sort, setSort] = React.useState("user.username") + const [usernameFilter, setUsernameFilter] = React.useState([]) + const [roleFilter, setRoleFilter] = React.useState([]) - - - - Members - - - Manage members that belong to this namespace. You can add new members and manage their permissions. - + const preFilter = React.useCallback((member: NamespaceMember) => { + if (usernameFilter.length > 0 && !usernameFilter.includes(member?.user?.username!)) return false + if (roleFilter.length > 0 && !member?.roles?.nodes?.some(n => roleFilter.includes(n?.id!))) return false + return true + }, [usernameFilter, roleFilter]) + + const visibleCount = React.useMemo( + () => members.filter(preFilter).length, + [members, preFilter] + ) + + const hasFilter = usernameFilter.length > 0 || roleFilter.length > 0 + + const sortProps: DataTableSortProps = {[sort]: "asc"} + + return <> + + + Members + {visibleCount} - - - + {/* Filter */} - + - - { - if (sort["user.username"] === undefined) - setSort(prev => ({...prev, "user.username": "asc"})) - else if (sort["user.username"] === "asc") - setSort(prev => ({...prev, "user.username": "desc"})) - else - setSort(prev => ({...prev, "user.username": undefined})) - - event.preventDefault() - event.stopPropagation() - }}> - {sort["user.username"] === undefined ? : sort["user.username"] === "asc" ? - : } - Username - - { - if (sort["createdAt"] === undefined) - setSort(prev => ({...prev, createdAt: "asc"})) - else if (sort["createdAt"] === "asc") - setSort(prev => ({...prev, createdAt: "desc"})) - else - setSort(prev => ({...prev, createdAt: undefined})) - - event.preventDefault() - event.stopPropagation() - }}> - {sort["createdAt"] === undefined ? - : sort["createdAt"] === "asc" ? - : } - Created At - - { - if (sort["updatedAt"] === undefined) - setSort(prev => ({...prev, updatedAt: "asc"})) - else if (sort["updatedAt"] === "asc") - setSort(prev => ({...prev, updatedAt: "desc"})) - else - setSort(prev => ({...prev, updatedAt: undefined})) - - event.preventDefault() - event.stopPropagation() - }}> - {sort["updatedAt"] === undefined ? - : sort["updatedAt"] === "asc" ? - : } - Updated At - + + Username + {usernames.map(username => ( + { + e.preventDefault() + e.stopPropagation() + toggle(username, setUsernameFilter) + }}> + + @{username} + + ))} + Roles + {roles.map(role => ( + { + e.preventDefault() + e.stopPropagation() + toggle(role?.id!, setRoleFilter) + }}> + + {role?.name} + + ))} + + {/* Sort */} + + + + + + + Sort by + {(Object.keys(sortLabels) as (keyof typeof sortLabels)[]).map(k => ( + setSort(k)}> + + {sortLabels[k]} + + ))} + + + + + {/* Add */} + + + - -
    - setFilter(filter)}/> -
    - - - + + + Manage members that belong to this namespace. You can add new members and manage their permissions. + + + - -} \ No newline at end of file +} diff --git a/src/packages/ce/src/module/components/ModuleConfigurationDialogComponent.tsx b/src/packages/ce/src/module/components/ModuleConfigurationDialogComponent.tsx new file mode 100644 index 00000000..03cfdea7 --- /dev/null +++ b/src/packages/ce/src/module/components/ModuleConfigurationDialogComponent.tsx @@ -0,0 +1,286 @@ +"use client" + +import React from "react"; +import {useParams, useRouter} from "next/navigation"; +import { + Avatar, + Button, + Flex, + hashToColor, + Menu, + MenuContent, + MenuItem, + MenuPortal, + MenuTrigger, + SelectContent, + SelectItem, + SelectItemText, + SelectPortal, + SelectTrigger, + SelectValue, + SelectViewport, + Spacing, + Text, + useForm, + useService, + useStore +} from "@code0-tech/pictor"; +import {ModuleService} from "@ce-internal/module/services/Module.service"; +import { + ModuleConfigurationInput, + Namespace, + NamespaceProject, + NamespaceProjectRuntimeAssignment, + RuntimeModule +} from "@code0-tech/sagittarius-graphql-types"; +import {getTypeSchema} from "@code0-tech/triangulum"; +import {DatatypeService} from "@ce-internal/datatype/services/Datatype.service"; +import {DataTypeInputComponent} from "@ce-internal/datatype/components/inputs/DataTypeInputComponent"; +import {ButtonGroup} from "@code0-tech/pictor/dist/components/button-group/ButtonGroup"; +import {Select} from "@radix-ui/react-select"; +import {IconBadgeFilled, IconChevronDown} from "@tabler/icons-react"; +import {RuntimeService} from "@ce-internal/runtime/services/Runtime.service"; +import {ProjectService} from "@ce-internal/project/services/Project.service"; +import {toast} from "@code0-tech/pictor/dist/components/toast/Toast"; +import {InputDialog} from "@core/components/InputDialog"; + +export interface ModuleConfigurationDialogComponentProps { + open?: boolean + onOpenChange?: (open: boolean) => void +} + +export const ModuleConfigurationDialogComponent: React.FC = ({ + open, + onOpenChange + }) => { + + const params = useParams() + const moduleService = useService(ModuleService) + const moduleStore = useStore(ModuleService) + const dataTypeService = useService(DatatypeService) + const runtimeService = useService(RuntimeService) + const runtimeStore = useStore(RuntimeService) + const projectService = useService(ProjectService) + const projectStore = useStore(ProjectService) + const router = useRouter() + + const namespaceIndex = params.namespaceId as any as number + const projectIndex = params.projectId as any as number + const moduleIndex = params.moduleId as any as number + const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` + const projectId: NamespaceProject['id'] = `gid://sagittarius/NamespaceProject/${projectIndex}` + const moduleId: RuntimeModule['id'] = `gid://sagittarius/RuntimeModule/${moduleIndex}` + + const project = React.useMemo( + () => projectService.getById(projectId), + [projectStore, projectId] + ) + + const runtimes = React.useMemo( + () => runtimeService.values().filter(runtime => project?.runtimes?.nodes?.some(r => r?.id === runtime.id)), + [runtimeStore, project] + ) + + const runtime = React.useMemo( + () => runtimes.find((runtime) => runtime.modules?.nodes?.find(module => module?.id === moduleId)), + [runtimes] + ) + + const module = React.useMemo( + () => moduleService.getById(moduleId, { + namespaceId, + projectId, + runtimeId: runtime?.id + }), + [moduleId, moduleStore, namespaceId, projectId, runtime] + ) + + // Only runtimes that actually have this integration installed can be selected. + const selectableRuntimes = React.useMemo( + () => runtimes.filter(runtime => runtime.modules?.nodes?.some(m => m?.identifier === module?.identifier)), + [runtimes, module?.identifier] + ) + + const moduleConfigurationSchemas = module?.configurationDefinitions?.nodes?.map(moduleConfiguration => { + return getTypeSchema(moduleConfiguration?.type!, dataTypeService.values({ + namespaceId, + projectId, + runtimeId: `gid://sagittarius/Runtime/1` + })) + }) ?? [] + + const initialValues = React.useMemo( + () => { + const values: Record = {} + module?.configurationDefinitions?.nodes?.forEach((moduleConfiguration, index) => { + const value = project?.runtimeAssignments?.nodes?.find(rA => rA?.runtime?.id == runtime?.id)?.moduleConfigurations?.nodes?.find?.(config => config?.definition?.id === moduleConfiguration?.id)?.value + values[moduleConfiguration?.id!] = value ? {__typename: "LiteralValue", value: value} : null + }) + return values + }, + [module, project, runtime?.id] + ) + + const onSubmit = React.useCallback((values: Record) => { + const namespaceProjectRuntimeAssignmentId: NamespaceProjectRuntimeAssignment['id'] = project?.runtimeAssignments?.nodes?.find(rA => rA?.runtime?.id == runtime?.id)?.id + const moduleConfigurations: Array = Object.entries(values)?.map(([key, value]) => { + return { + moduleConfigurationDefinitionId: key, + value: value?.value ?? null + } as ModuleConfigurationInput + }) + + if (!namespaceProjectRuntimeAssignmentId) return + + projectService.projectModuleConfigurationUpdate({ + namespaceProjectRuntimeAssignmentId, + moduleConfigurations + }).then(payload => { + if ((payload?.errors?.length ?? 0) <= 0) { + toast({title: "Saved configuration", color: "success"}) + } + }) + + }, [projectService, project, runtime?.id]) + + const [inputs, validate] = useForm({ + useInitialValidation: true, + truthyValidationBeforeSubmit: false, + initialValues: initialValues, + onSubmit: onSubmit, + }) + + console.log(module) + + return onOpenChange?.(open)} + title={`${module?.names?.[0]?.content ?? "3-party"} Integration configuration`} + description={"Configure how this integration behaves within your project. Settings are applied per runtime and take effect for every flow that relies on it."}> + + + {module?.names?.[0]?.content ?? "Integration"} + + + + + + + + + + { + selectableRuntimes.map(pRuntime => { + return { + const lRuntime = runtimes.find(r => r.id === pRuntime.id) + const lModule = lRuntime?.modules?.nodes?.find(m => m?.identifier === module?.identifier) + + const lNamespaceProjectRuntimeAssignment1: NamespaceProjectRuntimeAssignment | undefined = project?.runtimeAssignments?.nodes?.find(rA => rA?.runtime?.id == runtime?.id)! + const lNamespaceProjectRuntimeAssignment2: NamespaceProjectRuntimeAssignment | undefined = project?.runtimeAssignments?.nodes?.find(rA => rA?.runtime?.id == lRuntime?.id)! + + const lModuleConfigurations: Array = lNamespaceProjectRuntimeAssignment2?.moduleConfigurations?.nodes?.map((config, index) => { + return { + moduleConfigurationDefinitionId: config?.definition?.id!, + value: lNamespaceProjectRuntimeAssignment1?.moduleConfigurations?.nodes?.[index]?.value + } + }) ?? [] + + + projectService.projectModuleConfigurationUpdate({ + namespaceProjectRuntimeAssignmentId: lNamespaceProjectRuntimeAssignment2?.id!, + moduleConfigurations: lModuleConfigurations + }).then(payload => { + if ((payload?.errors?.length ?? 0) <= 0) { + toast({title: "Copied and saved configuration", color: "success"}) + } + }) + + const number = lModule?.id?.match(/RuntimeModule\/(\d+)$/)?.[1] + router.push(`/namespace/${namespaceIndex}/project/${projectIndex}/module/${number}`) + }}> + + + + {pRuntime.name} + + {pRuntime.id === project?.primaryRuntime?.id && + } + + + }) + } + + + + + + + + + {module?.descriptions?.[0]?.content ?? "Configure how this integration behaves within your project. Settings are applied per runtime and take effect for every flow that relies on it."} + + +
    + { + module?.configurationDefinitions?.nodes?.map((moduleConfiguration, index) => { + + if (!moduleConfiguration) return null + if (!moduleConfigurationSchemas[index]) return null + + return
    + + +
    + }) + } +
    +
    +} diff --git a/src/packages/ce/src/module/pages/ModuleConfigurationPage.tsx b/src/packages/ce/src/module/pages/ModuleConfigurationPage.tsx index 9ab885d3..7dcb3c1c 100644 --- a/src/packages/ce/src/module/pages/ModuleConfigurationPage.tsx +++ b/src/packages/ce/src/module/pages/ModuleConfigurationPage.tsx @@ -2,289 +2,29 @@ import React from "react"; import {useParams, useRouter} from "next/navigation"; -import { - Avatar, - Badge, - Button, - Flex, - hashToColor, - Menu, - MenuContent, - MenuItem, - MenuPortal, - MenuTrigger, - ScrollArea, - ScrollAreaScrollbar, - ScrollAreaThumb, - ScrollAreaViewport, - SelectContent, - SelectItem, - SelectItemText, - SelectPortal, - SelectTrigger, - SelectValue, - SelectViewport, - Spacing, - Text, - useForm, - useService, - useStore -} from "@code0-tech/pictor"; -import {ModuleService} from "@ce-internal/module/services/Module.service"; -import { - ModuleConfigurationInput, - Namespace, - NamespaceProject, - NamespaceProjectRuntimeAssignment, - RuntimeModule -} from "@code0-tech/sagittarius-graphql-types"; -import {getTypeSchema} from "@code0-tech/triangulum"; -import {DatatypeService} from "@ce-internal/datatype/services/Datatype.service"; -import {DataTypeInputComponent} from "@ce-internal/datatype/components/inputs/DataTypeInputComponent"; -import {Panel} from "@xyflow/react"; -import {ButtonGroup} from "@code0-tech/pictor/dist/components/button-group/ButtonGroup"; -import '@xyflow/react/dist/style.css'; -import {Select} from "@radix-ui/react-select"; -import {IconChevronDown} from "@tabler/icons-react"; -import {RuntimeService} from "@ce-internal/runtime/services/Runtime.service"; -import {ProjectService} from "@ce-internal/project/services/Project.service"; -import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {ModuleConfigurationDialogComponent} from "@ce-internal/module/components/ModuleConfigurationDialogComponent"; export const ModuleConfigurationPage: React.FC = () => { const params = useParams() - const moduleService = useService(ModuleService) - const moduleStore = useStore(ModuleService) - const dataTypeService = useService(DatatypeService) - const runtimeService = useService(RuntimeService) - const runtimeStore = useStore(RuntimeService) - const projectService = useService(ProjectService) - const projectStore = useStore(ProjectService) const router = useRouter() const namespaceIndex = params.namespaceId as any as number const projectIndex = params.projectId as any as number - const moduleIndex = params.moduleId as any as number - const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` - const projectId: NamespaceProject['id'] = `gid://sagittarius/NamespaceProject/${projectIndex}` - const moduleId: RuntimeModule['id'] = `gid://sagittarius/RuntimeModule/${moduleIndex}` - - const project = React.useMemo( - () => projectService.getById(projectId), - [projectStore, projectId] - ) - - const runtimes = React.useMemo( - () => runtimeService.values().filter(runtime => project?.runtimes?.nodes?.some(r => r?.id === runtime.id)), - [runtimeStore, project] - ) - - const runtime = React.useMemo( - () => runtimes.find((runtime) => runtime.modules?.nodes?.find(module => module?.id === moduleId)), - [runtimes] - ) - - const module = React.useMemo( - () => moduleService.getById(moduleId, { - namespaceId, - projectId, - runtimeId: runtime?.id - }), - [moduleId, moduleStore, namespaceId, projectId, runtime] - ) - - const moduleConfigurationSchemas = module?.configurationDefinitions?.nodes?.map(moduleConfiguration => { - return getTypeSchema(moduleConfiguration?.type!, dataTypeService.values({ - namespaceId, - projectId, - runtimeId: `gid://sagittarius/Runtime/1` - })) - }) ?? [] - - const initialValues = React.useMemo( - () => { - const values: Record = {} - module?.configurationDefinitions?.nodes?.forEach((moduleConfiguration, index) => { - const value = project?.runtimeAssignments?.nodes?.find(rA => rA?.runtime?.id == runtime?.id)?.moduleConfigurations?.nodes?.find?.(config => config?.definition?.id === moduleConfiguration?.id)?.value - values[moduleConfiguration?.id!] = value ? {__typename: "LiteralValue", value: value} : null - }) - return values - }, - [module, project, runtime?.id] - ) - - const onSubmit = React.useCallback((values: Record) => { - const namespaceProjectRuntimeAssignmentId: NamespaceProjectRuntimeAssignment['id'] = project?.runtimeAssignments?.nodes?.find(rA => rA?.runtime?.id == runtime?.id)?.id - const moduleConfigurations: Array = Object.entries(values)?.map(([key, value]) => { - return { - moduleConfigurationDefinitionId: key, - value: value?.value ?? null - } as ModuleConfigurationInput - }) - - if (!namespaceProjectRuntimeAssignmentId) return - - projectService.projectModuleConfigurationUpdate({ - namespaceProjectRuntimeAssignmentId, - moduleConfigurations - }).then(payload => { - if ((payload?.errors?.length ?? 0) <= 0) { - addIslandSuccessNotification({ - message: "Saved configuration" - }) - } - }) - - }, [projectService, project, runtime?.id]) - - const [inputs, validate] = useForm({ - useInitialValidation: true, - truthyValidationBeforeSubmit: false, - initialValues: initialValues, - onSubmit: onSubmit, - }) - - return <> - - - - {module?.names?.[0].content} plugin configuration - - - - {module?.descriptions?.[0]?.content}
    - This plugin was developed by {" "} - - @{module?.author} - -
    - - { - module?.configurationDefinitions?.nodes?.map((moduleConfiguration, index) => { - - if (!moduleConfiguration) return null - if (!moduleConfigurationSchemas[index]) return null - - return
    - - -
    - }) - } -
    - - - -
    - - - - - - - - - - { - runtimes.map(pRuntime => { - return { - const lRuntime = runtimes.find(r => r.id === pRuntime.id) - const lModule = lRuntime?.modules?.nodes?.find(m => m?.identifier === module?.identifier) - - const lNamespaceProjectRuntimeAssignment1: NamespaceProjectRuntimeAssignment | undefined = project?.runtimeAssignments?.nodes?.find(rA => rA?.runtime?.id == runtime?.id)! - const lNamespaceProjectRuntimeAssignment2: NamespaceProjectRuntimeAssignment | undefined = project?.runtimeAssignments?.nodes?.find(rA => rA?.runtime?.id == lRuntime?.id)! - - const lModuleConfigurations: Array = lNamespaceProjectRuntimeAssignment2?.moduleConfigurations?.nodes?.map((config, index) => { - return { - moduleConfigurationDefinitionId: config?.definition?.id!, - value: lNamespaceProjectRuntimeAssignment1?.moduleConfigurations?.nodes?.[index]?.value - } - }) ?? [] - - - projectService.projectModuleConfigurationUpdate({ - namespaceProjectRuntimeAssignmentId: lNamespaceProjectRuntimeAssignment2?.id!, - moduleConfigurations: lModuleConfigurations - }).then(payload => { - if ((payload?.errors?.length ?? 0) <= 0) { - addIslandSuccessNotification({ - message: "Copied and saved configuration" - }) - } - }) - - const number = lModule?.id?.match(/RuntimeModule\/(\d+)$/)?.[1] - router.push(`/namespace/${namespaceIndex}/project/${projectIndex}/module/${number}`) - }}> - - - - {pRuntime.name} - - {pRuntime.id === project?.primaryRuntime?.id && - Primary - } - - - }) - } - - - - - - - -} \ No newline at end of file + const moduleHref = `/namespace/${namespaceIndex}/project/${projectIndex}/module` + + return { + if (open) return + + const nav = (window as unknown as { navigation?: { entries(): { url: string }[], currentEntry?: { index: number } } }).navigation + if (!nav?.entries) { + router.back() + return + } + + const index = nav.currentEntry?.index ?? 0 + if (index > 0) router.back() + else router.push(moduleHref) + }}/> +} diff --git a/src/packages/ce/src/module/pages/ModulesPage.tsx b/src/packages/ce/src/module/pages/ModulesPage.tsx index 78ddb000..0961d3cc 100644 --- a/src/packages/ce/src/module/pages/ModulesPage.tsx +++ b/src/packages/ce/src/module/pages/ModulesPage.tsx @@ -6,25 +6,57 @@ import {Namespace, NamespaceProject} from "@code0-tech/sagittarius-graphql-types import { Badge, Button, + ButtonGroup, Card, Col, Flex, hashToColor, + Menu, + MenuContent, + MenuItem, + MenuLabel, + MenuPortal, + MenuTrigger, Row, - ScrollArea, ScrollAreaScrollbar, ScrollAreaThumb, + ScrollArea, + ScrollAreaScrollbar, + ScrollAreaThumb, ScrollAreaViewport, Spacing, Text, + Tooltip, + TooltipContent, + TooltipPortal, + TooltipTrigger, useService, useStore } from "@code0-tech/pictor"; import {ModuleService} from "@ce-internal/module/services/Module.service"; +import {RuntimeService} from "@ce-internal/runtime/services/Runtime.service"; import {icon, IconString} from "@core/util/icons"; -import {IconCheck} from "@tabler/icons-react"; -import CardSection from "@code0-tech/pictor/dist/components/card/CardSection"; +import { + IconAdjustmentsHorizontal, + IconAlertTriangle, + IconArrowsSort, + IconCheck, + IconPlus, + IconSettings +} from "@tabler/icons-react"; import {ProjectService} from "@ce-internal/project/services/Project.service"; import Link from "next/link"; +const filterLabels = { + all: "All", + configurable: "Configurable", + functions: "With functions", +} as const + +const sortLabels = { + name: "Name", + flowTypes: "Events", + functions: "Functions", +} as const + export const ModulesPage: React.FC = () => { const router = useRouter() @@ -33,6 +65,8 @@ export const ModulesPage: React.FC = () => { const moduleStore = useStore(ModuleService) const projectService = useService(ProjectService) const projectStore = useStore(ProjectService) + const runtimeService = useService(RuntimeService) + const runtimeStore = useStore(RuntimeService) const namespaceIndex = params.namespaceId as any as number const projectIndex = params.projectId as any as number @@ -53,122 +87,256 @@ export const ModulesPage: React.FC = () => { [params, namespaceId, projectId, moduleStore, project] ) - return <> -
    - - - - - Installed plugins on {project?.name} + const [filter, setFilter] = React.useState("all") + const [sort, setSort] = React.useState("name") + + const visibleModules = React.useMemo(() => { + const filtered = modules.filter(module => { + if (filter === "configurable") return (module.configurationDefinitions?.count ?? 0) > 0 + if (filter === "functions") return (module.functionDefinitions?.count ?? 0) > 0 + return true + }) + return [...filtered].sort((a, b) => { + if (sort === "flowTypes") return (b.flowTypes?.count ?? 0) - (a.flowTypes?.count ?? 0) + if (sort === "functions") return (b.functionDefinitions?.count ?? 0) - (a.functionDefinitions?.count ?? 0) + return (a.names?.[0]?.content ?? a.identifier ?? "") + .localeCompare(b.names?.[0]?.content ?? b.identifier ?? "") + }) + }, [modules, filter, sort]) + + // Every runtime module (across all runtimes) so we can inspect an integration's + // required configuration definitions on each runtime, not just the primary one. + const allModules = React.useMemo( + () => moduleService.values(), + [moduleStore] + ) + + // For each integration, the runtimes it isn't fully configured on. A required + // (non-optional) configuration with an unset value (null/undefined) counts as + // unconfigured. Configurations are linked to a runtime, so we check every runtime + // assignment of this project. Severity is "error" when the primary runtime is + // affected, otherwise "warning" when only secondary runtimes are. + const integrationSetupStatus = React.useMemo(() => { + const result = new Map() + const assignments = project?.runtimeAssignments?.nodes ?? [] + const primaryRuntimeId = project?.primaryRuntime?.id + + modules.forEach(cardModule => { + const identifier = cardModule.identifier + if (!identifier) return + + const runtimeNames: string[] = [] + let primaryUnconfigured = false + + assignments.forEach(assignment => { + const runtimeId = assignment?.runtime?.id + if (!runtimeId) return + + const runtimeModule = allModules.find(m => m.runtime?.id === runtimeId && m.identifier === identifier) + const requiredDefinitions = runtimeModule?.configurationDefinitions?.nodes + ?.filter(definition => definition?.optional === false) ?? [] + + const unconfigured = requiredDefinitions.some(definition => { + const value = assignment?.moduleConfigurations?.nodes + ?.find(config => config?.definition?.id === definition?.id)?.value + return value === null || value === undefined + }) + + if (unconfigured) { + if (runtimeId === primaryRuntimeId) primaryUnconfigured = true + runtimeNames.push(runtimeService.getById(runtimeId)?.name ?? "Unknown runtime") + } + }) + + if (runtimeNames.length > 0) { + result.set(identifier, {severity: primaryUnconfigured ? "error" : "warning", runtimeNames}) + } + }) + + return result + }, [modules, allModules, project, runtimeStore]) + + return
    + + +
    + + {/* ── page header ── */} + + + Integrations + + + Connect {project?.name ?? "your project"} to the tools your workflows depend on. - + - - { - modules.map(module => { - - const DisplayIcon = icon(module.icon as IconString) - - return - - - - - - - - - - - {module.names?.[0].content} - - - {module.flowTypes?.count ? `${module.flowTypes.count} flow types` : null} - {module.flowTypes?.count && module.functionDefinitions?.count ? " and " : null} - {module.functionDefinitions?.count ? `${module.functionDefinitions.count} functions` : null} - - - - - - installed - + + {/* ── section header: count + filter / sort / request ── */} + + + Installed + {visibleModules.length} + + + + {/* Filter */} + + + + + + + Filter + {(Object.keys(filterLabels) as (keyof typeof filterLabels)[]).map(k => ( + setFilter(k)}> + + {filterLabels[k]} + + ))} + + + + + {/* Sort */} + + + + + + + Sort by + {(Object.keys(sortLabels) as (keyof typeof sortLabels)[]).map(k => ( + setSort(k)}> + + {sortLabels[k]} + + ))} + + + + + {/* Request */} + + + + + + + + + {/* ── the grid of installed integrations ── */} + + {visibleModules.map(module => { + + const DisplayIcon = icon(module.icon as IconString) + const name = module.names?.[0]?.content ?? module.identifier ?? "Integration" + const flowTypes = module.flowTypes?.count ?? 0 + const functions = module.functionDefinitions?.count ?? 0 + const configurable = (module.configurationDefinitions?.count ?? 0) > 0 + const setupStatus = integrationSetupStatus.get(module.identifier ?? "") + const moduleNumber = module?.id?.match(/RuntimeModule\/(\d+)$/)?.[1] + + return + router.push(`/namespace/${namespaceIndex}/project/${projectIndex}/module/${moduleNumber}`) + : undefined}> + + + {/* identity: icon, name + version + author */} + + + + + {name} + {module.version && + v{module.version}} - - {module.descriptions?.[0]?.content}
    - This plugin was developed by - - @{module.author} - -
    + {module.author && + by @{module.author}}
    -
    - - - - - - -
    - - }) - } - - - Are you missing a plugin? - - - Create a issue on our github repository or join our discord to request a plugin - - - - - - + {setupStatus && + + + + + {setupStatus.severity === "error" ? "Setup required" : "Setup incomplete"} + + + + + + Not fully configured on {setupStatus.runtimeNames.join(", ")} + + + + } + + + {/* metadata: one calm, labelled line */} + + {flowTypes > 0 && + + {flowTypes} Events + } + {functions > 0 && + + {functions} Functions + } + {configurable && + + + + Configurable + + } + + + + + })} + + {/* request-integration affordance, matching card footprint */} + + + +
    - - - - - -
    - - -} \ No newline at end of file +
    +
    + + + +
    +
    +} diff --git a/src/packages/ce/src/namespace/components/NamespaceDataTableComponent.tsx b/src/packages/ce/src/namespace/components/NamespaceDataTableComponent.tsx deleted file mode 100644 index f5637caa..00000000 --- a/src/packages/ce/src/namespace/components/NamespaceDataTableComponent.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import React from "react"; -import {DataTable, DataTableColumn, Text, useService, useStore} from "@code0-tech/pictor"; -import {DataTableFilterProps, DataTableSortProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; -import {Namespace} from "@code0-tech/sagittarius-graphql-types"; -import {NamespaceDataTableRowComponent} from "@edition/namespace/components/NamespaceDataTableRowComponent"; -import {NamespaceService} from "@edition/namespace/services/Namespace.service"; -import {OrganizationService} from "@edition/organization/services/Organization.service"; -import {MemberService} from "@edition/member/services/Member.service"; -import {UserService} from "@edition/user/services/User.service"; -import {useUserSession} from "@edition/user/hooks/User.session.hook"; -import {getNamespaceName} from "@edition/namespace/util/Namespace.name.util"; - -export type NamespaceRow = Namespace & { name?: string } - -export interface NamespaceDataTableComponentProps { - sort?: DataTableSortProps - filter?: DataTableFilterProps - preFilter?: (namespace: Namespace, index: number) => boolean - onSelect?: (item: Namespace | undefined) => void - minimized?: boolean -} - -export const NamespaceDataTableComponent: React.FC = (props) => { - - const {sort, filter, preFilter = () => true, onSelect, minimized} = props - - const namespaceService = useService(NamespaceService) - const namespaceStore = useStore(NamespaceService) - const organizationService = useService(OrganizationService) - const organizationStore = useStore(OrganizationService) - const memberService = useService(MemberService) - const memberStore = useStore(MemberService) - const userService = useService(UserService) - const userStore = useStore(UserService) - - const currentSession = useUserSession() - - const currentUser = React.useMemo( - () => userService.getById(currentSession?.user?.id), - [userStore, userService, currentSession?.user?.id] - ) - - const memberships = React.useMemo( - () => currentUser?.namespaceMemberships?.nodes ?? [], - [currentUser?.namespaceMemberships?.nodes?.length] - ) - - const data = React.useMemo( - () => memberships.map(membership => { - const namespace = namespaceService.getById(membership?.namespace?.id) - if (!namespace) return undefined - - const members = memberService.values({namespaceId: namespace.id}) - const row: NamespaceRow = { - ...namespace, - name: getNamespaceName(namespace, organizationService, userService), - members: { - ...namespace.members, - nodes: members, - } - } - return row - }).filter((namespace): namespace is NamespaceRow => !!namespace).filter(preFilter), - [memberships.length, namespaceStore, memberStore, organizationStore, userStore, preFilter] - ) - - return - - No workspace found. Create one to get started. - - } - onSelect={(item) => item && onSelect?.(item)} - data={data}> - {(namespace, index) => { - return - }} - - -} diff --git a/src/packages/ce/src/namespace/components/NamespaceDataTableFilterInputComponent.tsx b/src/packages/ce/src/namespace/components/NamespaceDataTableFilterInputComponent.tsx deleted file mode 100644 index b0f48bf9..00000000 --- a/src/packages/ce/src/namespace/components/NamespaceDataTableFilterInputComponent.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import React from "react"; -import { - DataTableFilterInput, - DataTableFilterSuggestionMenu, - MenuCheckboxItem, - useService, - useStore -} from "@code0-tech/pictor"; -import {IconCheck} from "@tabler/icons-react"; -import {DataTableFilterProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; -import {NamespaceService} from "@edition/namespace/services/Namespace.service"; -import {OrganizationService} from "@edition/organization/services/Organization.service"; -import {MemberService} from "@edition/member/services/Member.service"; -import {UserService} from "@edition/user/services/User.service"; -import {useUserSession} from "@edition/user/hooks/User.session.hook"; -import {getNamespaceName} from "@edition/namespace/util/Namespace.name.util"; - -export interface NamespaceDataTableFilterInputComponentProps { - onChange: (filter: DataTableFilterProps) => void -} - -export const NamespaceDataTableFilterInputComponent: React.FC = (props) => { - - const {onChange} = props - - const namespaceService = useService(NamespaceService) - const namespaceStore = useStore(NamespaceService) - const organizationService = useService(OrganizationService) - const organizationStore = useStore(OrganizationService) - const memberService = useService(MemberService) - const memberStore = useStore(MemberService) - const userService = useService(UserService) - const userStore = useStore(UserService) - - const currentSession = useUserSession() - - const currentUser = React.useMemo( - () => userService.getById(currentSession?.user?.id), - [userStore, currentSession] - ) - - const memberships = React.useMemo( - () => currentUser?.namespaceMemberships?.nodes ?? [], - [currentUser] - ) - - const namespaces = React.useMemo( - () => memberships.map( - membership => namespaceService.getById(membership?.namespace?.id) - ).filter(namespace => !!namespace), - [namespaceStore, memberships] - ) - - const members = React.useMemo( - () => { - // A user can be a member of several namespaces; keep one entry per username - // since the suggestions filter on the username. - const seen = new Set() - return namespaces.map( - namespace => memberService.values({namespaceId: namespace?.id}) - ).flat().filter(member => { - const username = member?.user?.username - if (!username || seen.has(username)) return false - seen.add(username) - return true - }) - }, - [namespaces, memberStore] - ) - - // The editor inside DataTableFilterInput builds its suggestion extension once on - // mount, so the suggestion callbacks would otherwise close over the empty arrays - // of the first render. Refs let them read the latest data at invocation time. - const namespacesRef = React.useRef(namespaces) - const membersRef = React.useRef(members) - - React.useEffect(() => { - namespacesRef.current = namespaces - membersRef.current = members - }, [namespaces, members]) - - return { - - const split = currentValue.split(",").map(s => s.trim()).filter(Boolean) - - return - {namespacesRef.current.map(namespace => { - - const name = getNamespaceName(namespace, organizationService, userService) - const isChecked = split.includes(name!) - - return { - e.preventDefault(); - e.stopPropagation(); - const updated = isChecked - ? split.filter(n => n !== name) - : [...split, name]; - applySuggestion(updated.join(","), true); - }}> - {isChecked ? : } - {name} - - })} - - } - }, { - token: "Member", - key: "members.nodes.user.username", - operators: ["isOneOf"], - suggestion: (context, operator, currentValue, applySuggestion) => { - - const split = currentValue.split(",").map(s => s.trim()).filter(Boolean) - - return - {membersRef.current.map(member => { - - const isChecked = split.includes(member?.user?.username!) - - return { - e.preventDefault(); - e.stopPropagation(); - const updated = isChecked - ? split.filter(name => name !== member?.user?.username) - : [...split, member?.user?.username]; - applySuggestion(updated.join(","), true); - }}> - {isChecked ? : } - @{member?.user?.username} - - })} - - } - } - ]}/> -} diff --git a/src/packages/ce/src/namespace/components/NamespaceDataTableRowComponent.tsx b/src/packages/ce/src/namespace/components/NamespaceDataTableRowComponent.tsx deleted file mode 100644 index 5b816af7..00000000 --- a/src/packages/ce/src/namespace/components/NamespaceDataTableRowComponent.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import React from "react"; -import {Namespace} from "@code0-tech/sagittarius-graphql-types"; -import {Avatar, Button, DataTableColumn, Flex, hashToColor, Text, useService, useStore} from "@code0-tech/pictor"; -import {IconLogout} from "@tabler/icons-react"; -import {NamespaceService} from "@edition/namespace/services/Namespace.service"; -import {OrganizationService} from "@edition/organization/services/Organization.service"; -import {MemberService} from "@edition/member/services/Member.service"; -import {UserService} from "@edition/user/services/User.service"; -import {formatDistanceToNow} from "date-fns"; -import {useUserSession} from "@edition/user/hooks/User.session.hook"; -import {getNamespaceName} from "@edition/namespace/util/Namespace.name.util"; - -export interface NamespaceDataTableRowComponentProps { - namespaceId: Namespace['id'] - onLeave?: (namespace: Namespace) => void - minimized?: boolean -} - -export const NamespaceDataTableRowComponent: React.FC = (props) => { - - const {namespaceId, onLeave, minimized} = props - - const namespaceService = useService(NamespaceService) - const namespaceStore = useStore(NamespaceService) - const organizationService = useService(OrganizationService) - const organizationStore = useStore(OrganizationService) - const memberService = useService(MemberService) - const memberStore = useStore(MemberService) - const userService = useService(UserService) - const userStore = useStore(UserService) - - const currentSession = useUserSession() - - const currentUser = React.useMemo( - () => userService.getById(currentSession?.user?.id), - [userStore, currentSession] - ) - - const namespace = React.useMemo( - () => namespaceService.getById(namespaceId), - [namespaceStore, namespaceId] - ) - - const name = React.useMemo( - () => getNamespaceName(namespace, organizationService, userService), - [namespace, organizationStore, userStore] - ) - - const namespaceMember = React.useMemo( - () => namespace && currentUser ? memberService.getByNamespaceIdAndUserId(namespace.id, currentUser.id) : null, - [memberStore, namespace, currentUser] - ) - - return <> - - - - - - - {name} - - - - {!minimized && ( - - Updated {formatDistanceToNow(namespace?.updatedAt!)} ago - - )} - - - - {!minimized && ( - <> - - - {namespace?.projects?.count ? ( - - {namespace?.projects?.count ?? 0} {" "} - projects - - ) : null} - - {namespace?.members?.count ? ( - - {namespace?.members?.count ?? 0}{" "} - members - - ) : null} - - {namespace?.runtimes?.count ? ( - - {namespace?.runtimes?.count ?? 0}{" "} - connected runtimes - - ) : null} - - - {onLeave && namespaceMember && namespaceMember.userAbilities?.deleteMember ? ( - - - - ) : null} - - )} - - -} diff --git a/src/packages/ce/src/namespace/components/NamespaceSettingsDialogComponent.tsx b/src/packages/ce/src/namespace/components/NamespaceSettingsDialogComponent.tsx new file mode 100644 index 00000000..d2f5f446 --- /dev/null +++ b/src/packages/ce/src/namespace/components/NamespaceSettingsDialogComponent.tsx @@ -0,0 +1,100 @@ +"use client" + +import React from "react"; +import {Button, Text, useService, useStore} from "@code0-tech/pictor"; +import {TabContent, TabList, TabTrigger} from "@code0-tech/pictor/dist/components/tab/Tab"; +import {IconServer, IconSettings2, IconShieldLock, IconTrash, IconUsers} from "@tabler/icons-react"; +import {useParams} from "next/navigation"; +import {Namespace} from "@code0-tech/sagittarius-graphql-types"; +import {NamespaceService} from "@edition/namespace/services/Namespace.service"; +import {MembersView} from "@edition/member/views/MembersView"; +import {RolesView} from "@edition/role/views/RolesView"; +import {NamespaceServersView} from "@edition/namespace/views/NamespaceServersView"; +import {NamespaceGeneralSettingsView} from "@edition/namespace/views/NamespaceGeneralSettingsView"; +import {NamespaceDeleteView} from "@edition/namespace/views/NamespaceDeleteView"; +import {NamespaceLicensesView} from "@edition/namespace/views/NamespaceLicensesView"; +import {NamespaceLicensesTabTriggerView} from "@edition/namespace/views/NamespaceLicensesTabTriggerView"; +import {SettingDialog} from "@core/components/SettingDialog"; + +export interface NamespaceSettingsDialogComponentProps { + open?: boolean + onOpenChange?: (open: boolean) => void +} + +export const NamespaceSettingsDialogComponent: React.FC = (props) => { + + const {open, onOpenChange} = props + + const params = useParams() + const namespaceService = useService(NamespaceService) + const namespaceStore = useStore(NamespaceService) + + const namespaceIndex = params.namespaceId as any as number + const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` + + const namespace = React.useMemo( + () => namespaceService.getById(namespaceId), + [namespaceStore, namespaceId] + ) + + const isOrganization = namespace?.parent?.__typename === "Organization" + + //TODO: add ability check for organization settings access for every settings tab + + return onOpenChange?.(open)} + title={"Workspace settings"} + description={"Members, roles and general settings for your workspace. These settings affect everyone who has access to this workspace."} + trigger={ + + + + + + + + + + {isOrganization && ( + <> + + + + + + + + )} + + }> + + + + + + + + + + + + + + + +} diff --git a/src/packages/ce/src/namespace/pages/NamespaceOverviewPage.tsx b/src/packages/ce/src/namespace/pages/NamespaceOverviewPage.tsx index c50a3bd4..5cd845c7 100644 --- a/src/packages/ce/src/namespace/pages/NamespaceOverviewPage.tsx +++ b/src/packages/ce/src/namespace/pages/NamespaceOverviewPage.tsx @@ -1,44 +1,46 @@ "use client" import React from "react"; -import {ProjectsView} from "@edition/project/views/ProjectsView"; -import {useService, useStore, ScrollArea, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport} from "@code0-tech/pictor"; -import {NamespaceOverviewPersonalLeftView} from "@edition/namespace/views/NamespaceOverviewPersonalLeftView"; +import { + AuroraBackground, + ScrollArea, + ScrollAreaScrollbar, + ScrollAreaThumb, + ScrollAreaViewport, + Spacing +} from "@code0-tech/pictor"; import {useParams} from "next/navigation"; -import {UserService} from "@edition/user/services/User.service"; -import {NamespaceOverviewOrganizationLeftView} from "@edition/namespace/views/NamespaceOverviewOrganizationLeftView"; -import {useUserSession} from "@edition/user/hooks/User.session.hook"; -import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout"; +import {Namespace} from "@code0-tech/sagittarius-graphql-types"; +import {NamespaceStatsView} from "@edition/namespace/views/NamespaceStatsView"; +import {ProjectTableView} from "@edition/project/views/ProjectTableView"; +import {ApplicationAttentionFlowsComponent} from "@edition/application/components/ApplicationAttentionFlowsComponent"; export const NamespaceOverviewPage: React.FC = () => { const params = useParams() - const userService = useService(UserService) - const userStore = useStore(UserService) - const currentSession = useUserSession() + const namespaceIndex = params.namespaceId as any as number + const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` - const currentUser = React.useMemo(() => userService.getById(currentSession?.user?.id), [userStore, currentSession]) - const namespaceIndexCurrentUser = currentUser?.namespace?.id?.match(/Namespace\/(\d+)$/)?.[1] - const namespaceId = params.namespaceId as any as string - - return : - }> -
    - - - - - - - - -
    -
    + return
    + + +
    + + + + + +
    +
    + + + +
    +
    } \ No newline at end of file diff --git a/src/packages/ce/src/namespace/pages/NamespaceProjectsCreatePage.tsx b/src/packages/ce/src/namespace/pages/NamespaceProjectsCreatePage.tsx index 03908fe0..9e187dce 100644 --- a/src/packages/ce/src/namespace/pages/NamespaceProjectsCreatePage.tsx +++ b/src/packages/ce/src/namespace/pages/NamespaceProjectsCreatePage.tsx @@ -1,108 +1,32 @@ "use client" import React from "react"; -import {Button, Col, Flex, Spacing, Text, TextInput, toast, useForm, useService} from "@code0-tech/pictor"; -import Link from "next/link"; import {useParams, useRouter} from "next/navigation"; -import {ProjectService} from "@edition/project/services/Project.service"; import {Namespace} from "@code0-tech/sagittarius-graphql-types"; +import {ProjectCreateDialogComponent} from "@edition/project/components/ProjectCreateDialogComponent"; export const NamespaceProjectsCreatePage: React.FC = () => { const params = useParams() - const projectService = useService(ProjectService) - const [, startTransition] = React.useTransition() const router = useRouter() - const namespaceIndex = params?.namespaceId as string - const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex as unknown as number}` + const namespaceIndex = params.namespaceId as any as number + const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` + const fallbackHref = `/namespace/${namespaceIndex}` - //TODO: user abilities for project creation within namespace + return { + if (open) return - const [inputs, validate] = useForm({ - useInitialValidation: false, - initialValues: { - name: null, - description: null, - }, - validate: { - name: (value) => { - if (!value) return "Name is required" - if ((value as string).length < 3) return "Name needs to be at least 3 characters" - if ((value as string).length > 50) return "Name needs to be less than 50 characters" - return null - }, - description: (value) => { - if (!value) return "Description is required" - if ((value as string).length > 50) return "Description needs to be less than 50 characters" - return null - }, - }, - onSubmit: (values) => { - startTransition(() => { - if (!namespaceId) { - toast({ - title: "The current user does not have a personal namespace.", - color: "error", - dismissible: true, - }) - return - } - projectService.projectCreate({ - name: values.name as unknown as string, - description: values.description as unknown as string, - namespaceId: namespaceId - }).then(payload => { - if ((payload?.errors?.length ?? 0) <= 0) { - router.push(`/namespace/${namespaceIndex}`) - } - }) - }) - } - }) + const nav = (window as unknown as { navigation?: { entries(): { url: string }[], currentEntry?: { index: number } } }).navigation + if (!nav?.entries) { + router.back() + return + } - return
    - - - - Create new project - - - - Global runtimes are shared runtimes that can be used across multiple organizations. - - - - - - - - - - - - - - -
    -} \ No newline at end of file + const index = nav.currentEntry?.index ?? 0 + if (index > 0) router.back() + else router.push(fallbackHref) + }}/> +} diff --git a/src/packages/ce/src/namespace/pages/NamespaceProjectsPage.tsx b/src/packages/ce/src/namespace/pages/NamespaceProjectsPage.tsx deleted file mode 100644 index 1b432954..00000000 --- a/src/packages/ce/src/namespace/pages/NamespaceProjectsPage.tsx +++ /dev/null @@ -1,16 +0,0 @@ -"use client" - -import {ProjectsView} from "@edition/project/views/ProjectsView"; -import React from "react"; - -export const NamespaceProjectsPage: React.FC = () => { - return
    - -
    -} \ No newline at end of file diff --git a/src/packages/ce/src/namespace/pages/NamespaceSettingsPage.tsx b/src/packages/ce/src/namespace/pages/NamespaceSettingsPage.tsx index 1a1235eb..081f9928 100644 --- a/src/packages/ce/src/namespace/pages/NamespaceSettingsPage.tsx +++ b/src/packages/ce/src/namespace/pages/NamespaceSettingsPage.tsx @@ -1,56 +1,29 @@ "use client" import React from "react"; -import {Tab, TabContent} from "@code0-tech/pictor/dist/components/tab/Tab"; -import {NamespaceDeleteView} from "@edition/namespace/views/NamespaceDeleteView"; -import {NamespaceGeneralSettingsView} from "@edition/namespace/views/NamespaceGeneralSettingsView"; -import { - ResizableHandle, - ResizablePanel, - ResizablePanelGroup -} from "@code0-tech/pictor/dist/components/resizable/Resizable"; -import {SidebarComponent} from "@core/components/SidebarComponent"; -import {NamespaceTabListView} from "@edition/namespace/views/NamespaceTabListView"; -import {useParams} from "next/navigation"; -import {useService, useStore} from "@code0-tech/pictor"; -import {NamespaceService} from "@edition/namespace/services/Namespace.service"; -import {Namespace} from "@code0-tech/sagittarius-graphql-types"; +import {useParams, useRouter} from "next/navigation"; +import {NamespaceSettingsDialogComponent} from "@edition/namespace/components/NamespaceSettingsDialogComponent"; export const NamespaceSettingsPage: React.FC = () => { const params = useParams() - const namespaceService = useService(NamespaceService) - const namespaceStore = useStore(NamespaceService) + const router = useRouter() const namespaceIndex = params.namespaceId as any as number - const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` + const namespaceHref = `/namespace/${namespaceIndex}` - const namespace = React.useMemo( - () => namespaceService.getById(namespaceId), - [namespaceStore, namespaceId] - ) + return { + if (open) return - //TODO: add ability check for organization settings access for every settings tab + const nav = (window as unknown as { navigation?: { entries(): { url: string }[], currentEntry?: { index: number } } }).navigation + if (!nav?.entries) { + router.back() + return + } - return - - - - - - - <> - - - - - - - - - - -} \ No newline at end of file + const index = nav.currentEntry?.index ?? 0 + if (index > 0) router.back() + else router.push(namespaceHref) + }}/> +} diff --git a/src/packages/ce/src/namespace/pages/NamespacesPage.tsx b/src/packages/ce/src/namespace/pages/NamespacesPage.tsx deleted file mode 100644 index 7edc513d..00000000 --- a/src/packages/ce/src/namespace/pages/NamespacesPage.tsx +++ /dev/null @@ -1,90 +0,0 @@ -"use client" - -import React from "react"; -import {NamespacesView} from "@edition/namespace/views/NamespacesView"; -import { - AuroraBackground, - Avatar, - Badge, - Button, - Flex, - ScrollArea, - ScrollAreaScrollbar, - ScrollAreaThumb, - ScrollAreaViewport, - Spacing, - Text, - useService, - useStore -} from "@code0-tech/pictor"; -import {IconMail, IconSparkles, IconUser} from "@tabler/icons-react"; -import {UserService} from "@edition/user/services/User.service"; -import {useUserSession} from "@edition/user/hooks/User.session.hook"; -import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout"; - -export const NamespacesPage = () => { - - - const userStore = useStore(UserService) - const userService = useService(UserService) - - const userSession = useUserSession() - const currentUser = React.useMemo(() => userService.getById(userSession?.user?.id), [userStore, userSession]) - - const leftContent = - - -
    - - - 👋 - -
    - - {currentUser?.firstname} {currentUser?.lastname} - - - - - @{currentUser?.username} - - - - {currentUser?.email} - - - - BASIC - - - - - - -
    -
    -
    - - return -
    - - - - - - - - -
    -
    - -} diff --git a/src/packages/ce/src/namespace/views/NamespaceDeleteView.tsx b/src/packages/ce/src/namespace/views/NamespaceDeleteView.tsx index e16ab4ae..dc4c058d 100644 --- a/src/packages/ce/src/namespace/views/NamespaceDeleteView.tsx +++ b/src/packages/ce/src/namespace/views/NamespaceDeleteView.tsx @@ -1,12 +1,12 @@ "use client" import React from "react"; -import {Button, Card, Flex, Spacing, Text, useService, useStore} from "@code0-tech/pictor"; +import {Button, DialogClose, Spacing, Text, useService, useStore} from "@code0-tech/pictor"; import {useParams, useRouter} from "next/navigation"; import {NamespaceService} from "@edition/namespace/services/Namespace.service"; import {OrganizationService} from "@edition/organization/services/Organization.service"; import {Namespace} from "@code0-tech/sagittarius-graphql-types"; -import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {toast} from "@code0-tech/pictor/dist/components/toast/Toast"; export const NamespaceDeleteView: React.FC = () => { @@ -32,9 +32,7 @@ export const NamespaceDeleteView: React.FC = () => { organizationId: parentOrganization?.id!! }).then(payload => { if ((payload?.errors?.length ?? 0) <= 0) { - addIslandSuccessNotification({ - message: "Deleted organization" - }) + toast({title: "Deleted organization", color: "success"}) } router.push("/") }) @@ -42,21 +40,14 @@ export const NamespaceDeleteView: React.FC = () => { }, [parentOrganization]) return <> - - Delete organization - - - - - - - This will delete the organization and cannot be undone. - - - - - + Delete workspace + + + Permanently delete this workspace and everything in it. This action cannot be undone. + + + + + } \ No newline at end of file diff --git a/src/packages/ce/src/namespace/views/NamespaceGeneralSettingsView.tsx b/src/packages/ce/src/namespace/views/NamespaceGeneralSettingsView.tsx index b8e7464f..e4706fb0 100644 --- a/src/packages/ce/src/namespace/views/NamespaceGeneralSettingsView.tsx +++ b/src/packages/ce/src/namespace/views/NamespaceGeneralSettingsView.tsx @@ -1,13 +1,12 @@ "use client" import React from "react"; -import CardSection from "@code0-tech/pictor/dist/components/card/CardSection"; -import {Button, Card, Flex, Spacing, Text, TextInput, useForm, useService, useStore} from "@code0-tech/pictor"; +import {Button, Flex, Spacing, Text, TextInput, useForm, useService, useStore} from "@code0-tech/pictor"; import {useParams} from "next/navigation"; import {NamespaceService} from "@edition/namespace/services/Namespace.service"; import {OrganizationService} from "@edition/organization/services/Organization.service"; import {Namespace} from "@code0-tech/sagittarius-graphql-types"; -import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {toast} from "@code0-tech/pictor/dist/components/toast/Toast"; export const NamespaceGeneralSettingsView: React.FC = () => { @@ -45,9 +44,7 @@ export const NamespaceGeneralSettingsView: React.FC = () => { organizationId: parentOrganization?.id!! }).then(payload => { if ((payload?.errors?.length ?? 0) <= 0) { - addIslandSuccessNotification({ - message: "Updated organization" - }) + toast({title: "Updated organization", color: "success"}) } }) }) @@ -56,23 +53,17 @@ export const NamespaceGeneralSettingsView: React.FC = () => { return <> - - General - - + General + - - - - - - Name - - - - - + + + The name and general settings for this workspace, visible to everyone who has access to it. + + + } \ No newline at end of file diff --git a/src/packages/ce/src/namespace/views/NamespaceLicensesTabTriggerView.tsx b/src/packages/ce/src/namespace/views/NamespaceLicensesTabTriggerView.tsx new file mode 100644 index 00000000..aa10b62e --- /dev/null +++ b/src/packages/ce/src/namespace/views/NamespaceLicensesTabTriggerView.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +export const NamespaceLicensesTabTriggerView: React.FC = () => { + return null +} diff --git a/src/packages/ce/src/namespace/views/NamespaceLicensesView.tsx b/src/packages/ce/src/namespace/views/NamespaceLicensesView.tsx new file mode 100644 index 00000000..f64b8133 --- /dev/null +++ b/src/packages/ce/src/namespace/views/NamespaceLicensesView.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +export const NamespaceLicensesView: React.FC = () => { + return null +} diff --git a/src/packages/ce/src/namespace/views/NamespaceListView.tsx b/src/packages/ce/src/namespace/views/NamespaceListView.tsx new file mode 100644 index 00000000..1a035d97 --- /dev/null +++ b/src/packages/ce/src/namespace/views/NamespaceListView.tsx @@ -0,0 +1,79 @@ +"use client" + +import React from "react"; +import {Avatar, Badge, Button, Flex, hashToColor, Spacing, Text, useService, useStore} from "@code0-tech/pictor"; +import {IconPlus} from "@tabler/icons-react"; +import Link from "next/link"; +import {Namespace} from "@code0-tech/sagittarius-graphql-types"; +import {NamespaceService} from "@edition/namespace/services/Namespace.service"; +import {OrganizationService} from "@edition/organization/services/Organization.service"; +import {UserService} from "@edition/user/services/User.service"; +import {useUserSession} from "@edition/user/hooks/User.session.hook"; +import {getNamespaceName} from "@edition/namespace/util/Namespace.name.util"; + +export const NamespaceListView: React.FC = () => { + + const namespaceService = useService(NamespaceService) + const namespaceStore = useStore(NamespaceService) + const organizationService = useService(OrganizationService) + const organizationStore = useStore(OrganizationService) + const userService = useService(UserService) + const userStore = useStore(UserService) + + const currentSession = useUserSession() + + const currentUser = React.useMemo( + () => userService.getById(currentSession?.user?.id), + [userStore, userService, currentSession?.user?.id] + ) + + const memberships = React.useMemo( + () => currentUser?.namespaceMemberships?.nodes ?? [], + [currentUser?.namespaceMemberships?.nodes?.length] + ) + + const namespaces = React.useMemo( + () => memberships + .map(membership => namespaceService.getById(membership?.namespace?.id)) + .filter((namespace): namespace is Namespace => !!namespace), + [memberships.length, namespaceStore] + ) + + return + + + Workspaces + + + {namespaces.length} + + + + {namespaces.map(namespace => { + const number = namespace.id?.match(/Namespace\/(\d+)$/)?.[1] + const name = getNamespaceName(namespace, organizationService, userService) ?? "" + const isPersonal = namespace.parent?.__typename === "User" + const user = namespace.parent?.__typename === "User" + ? userService.getById(namespace.parent.id) : undefined + return + + + })} + + + + +} diff --git a/src/packages/ce/src/namespace/views/NamespaceMenuView.tsx b/src/packages/ce/src/namespace/views/NamespaceMenuView.tsx new file mode 100644 index 00000000..bb45fa40 --- /dev/null +++ b/src/packages/ce/src/namespace/views/NamespaceMenuView.tsx @@ -0,0 +1,176 @@ +"use client" + +import React from "react"; +import { + Avatar, + Button, + Flex, + hashToColor, + Menu, + MenuContent, + MenuItem, + MenuLabel, + MenuPortal, + MenuSeparator, + MenuSub, + MenuSubContent, + MenuSubTrigger, + MenuTrigger, + Text, + useService, + useStore +} from "@code0-tech/pictor"; +import { + IconCheck, + IconChevronDown, + IconChevronRight, + IconServer, + IconSettings, + IconShieldLock, + IconSwitch, + IconUsers +} from "@tabler/icons-react"; +import Link from "next/link"; +import {useParams} from "next/navigation"; +import {Namespace} from "@code0-tech/sagittarius-graphql-types"; +import {NamespaceService} from "@edition/namespace/services/Namespace.service"; +import {OrganizationService} from "@edition/organization/services/Organization.service"; +import {UserService} from "@edition/user/services/User.service"; +import {useUserSession} from "@edition/user/hooks/User.session.hook"; +import {getNamespaceName} from "@edition/namespace/util/Namespace.name.util"; + +export const NamespaceMenuView: React.FC = () => { + + const params = useParams() + const namespaceService = useService(NamespaceService) + const namespaceStore = useStore(NamespaceService) + const organizationService = useService(OrganizationService) + const organizationStore = useStore(OrganizationService) + const userService = useService(UserService) + const userStore = useStore(UserService) + + const currentSession = useUserSession() + + const namespaceIndex = params.namespaceId as any as number + const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` + + const namespace = React.useMemo( + () => namespaceService.getById(namespaceId), + [namespaceStore, namespaceId] + ) + + const isPersonal = namespace?.parent?.__typename === "User" + const user = isPersonal && namespace?.parent?.__typename === "User" + ? userService.getById(namespace.parent.id) : undefined + + const name = React.useMemo( + () => getNamespaceName(namespace, organizationService, userService) ?? "", + [namespace, organizationStore, userStore] + ) + + const description = isPersonal ? "Personal workspace" : "Organization workspace" + + const currentUser = React.useMemo( + () => userService.getById(currentSession?.user?.id), + [userStore, userService, currentSession?.user?.id] + ) + + const memberships = React.useMemo( + () => currentUser?.namespaceMemberships?.nodes ?? [], + [currentUser?.namespaceMemberships?.nodes?.length] + ) + + const namespaces = React.useMemo( + () => memberships + .map(membership => namespaceService.getById(membership?.namespace?.id)) + .filter((it): it is Namespace => !!it), + [memberships.length, namespaceStore] + ) + + return + + + + + + + Workspace + + + + + Settings + + + + + + Members + + + + + + Roles + + + + + + Servers + + + + + + + + Switch Workspace + + + + + {namespaces.map(entry => { + const number = entry.id?.match(/Namespace\/(\d+)$/)?.[1] + const entryPersonal = entry.parent?.__typename === "User" + const entryUser = entry.parent?.__typename === "User" + ? userService.getById(entry.parent.id) : undefined + const entryName = getNamespaceName(entry, organizationService, userService) ?? "" + return + + {entryPersonal + ? + : } + + {entryName} + + + + + })} + + + + + +} diff --git a/src/packages/ce/src/namespace/views/NamespaceOverviewOrganizationLeftView.tsx b/src/packages/ce/src/namespace/views/NamespaceOverviewOrganizationLeftView.tsx deleted file mode 100644 index 2a643c17..00000000 --- a/src/packages/ce/src/namespace/views/NamespaceOverviewOrganizationLeftView.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import React from "react"; -import { - AuroraBackground, - Avatar, - Button, - Flex, - hashToColor, - ScrollArea, - ScrollAreaScrollbar, - ScrollAreaThumb, - ScrollAreaViewport, - Spacing, - Text, - Tooltip, - TooltipContent, - TooltipPortal, - TooltipTrigger, - useService, - useStore -} from "@code0-tech/pictor"; -import {UserService} from "@edition/user/services/User.service"; -import {OrganizationService} from "@edition/organization/services/Organization.service"; -import Link from "next/link"; -import {useParams} from "next/navigation"; -import {MemberService} from "@edition/member/services/Member.service"; -import {NamespaceService} from "@edition/namespace/services/Namespace.service"; -import {RuntimeDataTableComponent} from "@edition/runtime/components/RuntimeDataTableComponent"; - -export const NamespaceOverviewOrganizationLeftView: React.FC = () => { - - const params = useParams() - const userService = useService(UserService) - const userStore = useStore(UserService) - const organizationService = useService(OrganizationService) - const organizationStore = useStore(OrganizationService) - const memberService = useService(MemberService) - const memberStore = useStore(MemberService) - const namespaceService = useService(NamespaceService) - const namespaceStore = useStore(NamespaceService) - - const namespaceIndex = params.namespaceId as any as number - - const namespace = React.useMemo( - () => namespaceService.getById(`gid://sagittarius/Namespace/${namespaceIndex}`), - [namespaceStore, namespaceIndex] - ) - const parentOrganization = React.useMemo( - () => namespace?.parent?.__typename === "Organization" ? organizationService.getById(namespace?.parent?.id) : null, - [organizationStore, namespace] - ) - - const members = React.useMemo( - () => memberService.values({namespaceId: `gid://sagittarius/Namespace/${namespaceIndex}`}), - [memberStore, userStore] - ) - - return - - - - - {parentOrganization?.name} - - - - - - - -
    - - Members - - - {members.map(member => { - const user = userService.getById(member.user?.id!!) - return - - - - - - - - @{user?.username} - - - {user?.email} - - - - - - })} - {members.length <= 0 && - - - } - - -
    - - Runtimes - - -
    -
    - - - -
    - -} \ No newline at end of file diff --git a/src/packages/ce/src/namespace/views/NamespaceOverviewPersonalLeftView.tsx b/src/packages/ce/src/namespace/views/NamespaceOverviewPersonalLeftView.tsx deleted file mode 100644 index 573e0ea6..00000000 --- a/src/packages/ce/src/namespace/views/NamespaceOverviewPersonalLeftView.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import React from "react"; -import { - AuroraBackground, - Avatar, - Badge, - Button, - Flex, - hashToColor, - ScrollArea, - ScrollAreaScrollbar, - ScrollAreaThumb, - ScrollAreaViewport, - Spacing, - Text, - Tooltip, - TooltipContent, - TooltipPortal, - TooltipTrigger, - useService, - useStore -} from "@code0-tech/pictor"; -import {UserService} from "@edition/user/services/User.service"; -import {IconMail, IconSparkles, IconUser, IconUserCog} from "@tabler/icons-react"; -import {OrganizationService} from "@edition/organization/services/Organization.service"; -import Link from "next/link"; -import {useParams} from "next/navigation"; -import {MemberService} from "@edition/member/services/Member.service"; -import {useUserSession} from "@edition/user/hooks/User.session.hook"; - -export const NamespaceOverviewPersonalLeftView: React.FC = () => { - - const params = useParams() - const userService = useService(UserService) - const userStore = useStore(UserService) - const organizationService = useService(OrganizationService) - const organizationStore = useStore(OrganizationService) - const memberService = useService(MemberService) - const memberStore = useStore(MemberService) - - const namespaceIndex = params.namespaceId as any as number - - const currentSession = useUserSession() - const currentUser = React.useMemo( - () => userService.getById(currentSession?.user?.id), - [userStore, currentSession] - ) - const organizations = React.useMemo( - () => organizationService.values(), - [organizationStore] - ) - - const members = React.useMemo( - () => memberService.values({namespaceId: `gid://sagittarius/Namespace/${namespaceIndex}`}), - [memberStore, userStore] - ) - - return - - -
    - - - 👋 - -
    - - {currentUser?.firstname} {currentUser?.lastname} - - - - - @{currentUser?.username} - - - - {currentUser?.admin ? Admin : - User} - - - - {currentUser?.email} - - - - BASIC - - - - - - - - - -
    - - Organizations - - - {organizations.map(organization => { - return - })} - {organizations.length <= 0 && You aren't a member of any organization} - - -
    - - Members - - - {members.map(member => { - const user = userService.getById(member.user?.id!!) - return - - - - - - - - @{user?.username} - - - {user?.email} - - - - - - })} - {members.length <= 0 && - - - } - - -
    -
    - - - -
    - -} \ No newline at end of file diff --git a/src/packages/ce/src/namespace/views/NamespaceOverviewRightView.tsx b/src/packages/ce/src/namespace/views/NamespaceOverviewRightView.tsx deleted file mode 100644 index cf65ee05..00000000 --- a/src/packages/ce/src/namespace/views/NamespaceOverviewRightView.tsx +++ /dev/null @@ -1,82 +0,0 @@ -"use client" - -import React from "react"; -import { - AuroraBackground, - Avatar, - Badge, - Button, - Flex, - Spacing, - Text, - Tooltip, - TooltipContent, - TooltipPortal, - TooltipTrigger, - useService, - useStore -} from "@code0-tech/pictor"; -import {MemberService} from "@edition/member/services/Member.service"; -import {useParams} from "next/navigation"; -import {NamespaceService} from "@edition/namespace/services/Namespace.service"; -import {UserService} from "@edition/user/services/User.service"; -import Link from "next/link"; -import {RuntimeDataTableComponent} from "@edition/runtime/components/RuntimeDataTableComponent"; - -export const NamespaceOverviewRightView: React.FC = () => { - - const params = useParams() - const memberService = useService(MemberService) - const memberStore = useStore(MemberService) - const namespaceService = useService(NamespaceService) - const namespaceStore = useStore(NamespaceService) - const userService = useService(UserService) - const userStore = useStore(UserService) - - const namespaceId = params.namespaceId as any as number - const namespace = React.useMemo(() => namespaceService.getById(`gid://sagittarius/Namespace/${namespaceId}`), [namespaceStore, namespaceId]) - const members = React.useMemo(() => memberService.values({namespaceId: `gid://sagittarius/Namespace/${namespaceId}`}), [memberStore, userStore]) - - return - - - This is your personal space where you can manage your projects, roles, runtimes and invite outside - users. - - Members - - - {members.map(member => { - const user = userService.getById(member.user?.id!!) - return - - - - - - - - @{user?.username} - - - {user?.email} - - - - - - })} - - - - - - Runtimes - - - - -} \ No newline at end of file diff --git a/src/packages/ce/src/namespace/views/NamespaceRowView.tsx b/src/packages/ce/src/namespace/views/NamespaceRowView.tsx new file mode 100644 index 00000000..8d797d9c --- /dev/null +++ b/src/packages/ce/src/namespace/views/NamespaceRowView.tsx @@ -0,0 +1,218 @@ +"use client" + +import React from "react"; +import { + Avatar, + Badge, + Button, + ButtonGroup, Card, + Col, + Flex, hashToColor, + Menu, + MenuContent, + MenuItem, + MenuLabel, + MenuPortal, + MenuTrigger, + Row, Spacing, + Text, + useService, + useStore +} from "@code0-tech/pictor"; +import { + IconAdjustmentsHorizontal, + IconArrowsSort, + IconCheck, + IconFolders, + IconPlus, + IconUsers +} from "@tabler/icons-react"; +import Link from "next/link"; +import {Namespace} from "@code0-tech/sagittarius-graphql-types"; +import {NamespaceService} from "@edition/namespace/services/Namespace.service"; +import {OrganizationService} from "@edition/organization/services/Organization.service"; +import {UserService} from "@edition/user/services/User.service"; +import {useUserSession} from "@edition/user/hooks/User.session.hook"; +import {getNamespaceName} from "@edition/namespace/util/Namespace.name.util"; + +const filterLabels = { + all: "All", + personal: "Personal", + organization: "Organizations", +} as const + +const sortLabels = { + name: "Name", + projects: "Projects", + members: "Members", +} as const + +export const NamespaceRowView: React.FC = () => { + + const namespaceService = useService(NamespaceService) + const namespaceStore = useStore(NamespaceService) + const organizationService = useService(OrganizationService) + const organizationStore = useStore(OrganizationService) + const userService = useService(UserService) + const userStore = useStore(UserService) + + const currentSession = useUserSession() + + const currentUser = React.useMemo( + () => userService.getById(currentSession?.user?.id), + [userStore, userService, currentSession?.user?.id] + ) + + const memberships = React.useMemo( + () => currentUser?.namespaceMemberships?.nodes ?? [], + [currentUser?.namespaceMemberships?.nodes?.length] + ) + + const namespaces = React.useMemo( + () => memberships + .map(membership => namespaceService.getById(membership?.namespace?.id)) + .filter((namespace): namespace is Namespace => !!namespace), + [memberships.length, namespaceStore] + ) + + const [filter, setFilter] = React.useState("all") + const [sort, setSort] = React.useState("name") + + const visibleNamespaces = React.useMemo(() => { + const filtered = namespaces.filter(namespace => { + if (filter === "all") return true + const personal = namespace.parent?.__typename === "User" + return filter === "personal" ? personal : !personal + }) + return [...filtered].sort((a, b) => { + if (sort === "projects") return (b.projects?.count ?? 0) - (a.projects?.count ?? 0) + if (sort === "members") return (b.members?.count ?? 0) - (a.members?.count ?? 0) + return (getNamespaceName(a, organizationService, userService) ?? "") + .localeCompare(getNamespaceName(b, organizationService, userService) ?? "") + }) + }, [namespaces, filter, sort, organizationStore, userStore]) + + return <> + + + Workspaces + {visibleNamespaces.length} + + + + {/* Filter */} + + + + + + + Filter + {(Object.keys(filterLabels) as (keyof typeof filterLabels)[]).map(k => ( + setFilter(k)}> + + {filterLabels[k]} + + ))} + + + + + {/* Sort */} + + + + + + + Sort by + {(Object.keys(sortLabels) as (keyof typeof sortLabels)[]).map(k => ( + setSort(k)}> + + {sortLabels[k]} + + ))} + + + + + {/* Create */} + + + + + + + + {/* ── The grid users choose from; create sits in the same grid ── */} + + {visibleNamespaces.map(namespace => { + const number = namespace.id?.match(/Namespace\/(\d+)$/)?.[1] + const name = getNamespaceName(namespace, organizationService, userService) ?? "" + const isPersonal = namespace.parent?.__typename === "User" + const user = namespace.parent?.__typename === "User" + ? userService.getById(namespace.parent.id) : undefined + return + + + + {/* identity: avatar (user avatar for personal, identicon for org), name and personal marker */} + + {isPersonal + ? + : } + + {name} + {isPersonal && Personal} + + + {/* metadata: one calm, labelled line */} + + + + + {namespace.projects?.count ?? 0} projects + + + + + + {namespace.members?.count ?? 0} members + + + + + + + + })} + + {/* create-workspace affordance, matching card footprint */} + + + + + + + +} diff --git a/src/packages/ce/src/namespace/views/NamespaceServersView.tsx b/src/packages/ce/src/namespace/views/NamespaceServersView.tsx new file mode 100644 index 00000000..a3fcda7e --- /dev/null +++ b/src/packages/ce/src/namespace/views/NamespaceServersView.tsx @@ -0,0 +1,135 @@ +"use client" + +import React from "react"; +import { + Badge, + Button, + ButtonGroup, + Flex, + Menu, + MenuContent, + MenuItem, + MenuLabel, + MenuPortal, + MenuTrigger, + Spacing, + Text, + useService, + useStore +} from "@code0-tech/pictor"; +import {TabContent} from "@code0-tech/pictor/dist/components/tab/Tab"; +import {DataTableSortProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; +import {IconAdjustmentsHorizontal, IconArrowsSort, IconCheck, IconPlus} from "@tabler/icons-react"; +import Link from "next/link"; +import {useParams, useRouter} from "next/navigation"; +import {Namespace, Runtime} from "@code0-tech/sagittarius-graphql-types"; +import {RuntimeService} from "@edition/runtime/services/Runtime.service"; +import {RuntimeDataTableComponent} from "@edition/runtime/components/RuntimeDataTableComponent"; + +const filterLabels = { + all: "All", + connected: "Connected", + disconnected: "Disconnected", +} as const + +const sortLabels = { + name: "Name", + createdAt: "Created", + updatedAt: "Updated", +} as const + +export const NamespaceServersView: React.FC = () => { + + const params = useParams() + const router = useRouter() + const runtimeService = useService(RuntimeService) + const runtimeStore = useStore(RuntimeService) + + const namespaceIndex = params.namespaceId as any as number + const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` + + const runtimes = React.useMemo(() => runtimeService.values(), [runtimeStore]) + + const [filter, setFilter] = React.useState("all") + const [sort, setSort] = React.useState("name") + + const preFilter = React.useCallback((runtime: Runtime) => { + if (runtime?.namespace?.id !== namespaceId) return false + if (filter === "connected") return runtime.status === "CONNECTED" + if (filter === "disconnected") return runtime.status !== "CONNECTED" + return true + }, [filter, namespaceId]) + + const visibleCount = React.useMemo(() => runtimes.filter(preFilter).length, [runtimes, preFilter]) + + const sortProps: DataTableSortProps = {[sort]: "asc"} + + return + + + Servers + {visibleCount} + + + {/* Filter */} + + + + + + + Filter + {(Object.keys(filterLabels) as (keyof typeof filterLabels)[]).map(k => ( + setFilter(k)}> + + {filterLabels[k]} + + ))} + + + + + {/* Sort */} + + + + + + + Sort by + {(Object.keys(sortLabels) as (keyof typeof sortLabels)[]).map(k => ( + setSort(k)}> + + {sortLabels[k]} + + ))} + + + + + {/* Create */} + + + + + + + + Runtimes are the environments where the applications of this namespace run. + + + { + const number = runtime?.id?.match(/Runtime\/(\d+)$/)?.[1] + if (number) router.push(`/namespace/${namespaceIndex}/runtimes/${number}/settings`) + }}/> + +} diff --git a/src/packages/ce/src/namespace/views/NamespaceStatsView.tsx b/src/packages/ce/src/namespace/views/NamespaceStatsView.tsx new file mode 100644 index 00000000..b1a23140 --- /dev/null +++ b/src/packages/ce/src/namespace/views/NamespaceStatsView.tsx @@ -0,0 +1,108 @@ +"use client" + +import React from "react"; +import {Badge, Card, Flex, Text, useService, useStore} from "@code0-tech/pictor"; +import {useParams} from "next/navigation"; +import {NamespaceService} from "@edition/namespace/services/Namespace.service"; +import {ProjectService} from "@edition/project/services/Project.service"; +import {FlowService} from "@edition/flow/services/Flow.service"; +import {Namespace} from "@code0-tech/sagittarius-graphql-types"; +import {IconAlertTriangle, IconTrendingUp} from "@tabler/icons-react"; + +// Each node run is assumed to save this many seconds of manual work. +const SAVED_SECONDS_PER_NODE_RUN = 60 + +const withinDays = (time: string | null | undefined, days: number): boolean => + !!time && Date.now() - new Date(time).getTime() < days * 24 * 60 * 60 * 1000 + +export const NamespaceStatsView: React.FC = () => { + + const params = useParams() + const namespaceService = useService(NamespaceService) + const namespaceStore = useStore(NamespaceService) + const projectService = useService(ProjectService) + const projectStore = useStore(ProjectService) + const flowService = useService(FlowService) + const flowStore = useStore(FlowService) + + const namespaceIndex = params.namespaceId as any as number + const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` + + const namespace = React.useMemo(() => namespaceService.getById(namespaceId), [namespaceStore, namespaceId]) + + const projects = React.useMemo( + () => projectService.values({namespaceId}), + [projectStore, namespaceId] + ) + + const flows = React.useMemo( + () => projects.flatMap(project => flowService.values({ + namespaceId: project.namespace?.id, + projectId: project.id, + })), + [projects, flowStore] + ) + + const projectsCount = namespace?.projects?.count ?? 0 + const membersCount = namespace?.members?.count ?? 0 + + // Saved work = executed nodes across a set of flows, pressed into whole minutes. + const savedMinutesOf = (flowSet: typeof flows) => Math.floor(flowSet.reduce((sum, flow) => + sum + (flow.nodes?.count ?? 0) * (flow.executionResults?.count ?? 0), 0) + * SAVED_SECONDS_PER_NODE_RUN / 60) + + const savedMinutes = React.useMemo(() => savedMinutesOf(flows), [flows]) + + const invalidFlows = React.useMemo( + () => flows.filter(flow => flow.validationStatus === "INVALID"), + [flows] + ) + + // Each badge shows the amount added within the last 30 days ("new this month"). + const trends = React.useMemo(() => { + const recentFlows = flows.filter(flow => withinDays(flow.createdAt, 30)) + return { + projects: projects.filter(project => withinDays(project.createdAt, 30)).length, + savedMinutes: savedMinutesOf(recentFlows), + invalidFlows: recentFlows.filter(flow => flow.validationStatus === "INVALID").length, + } + }, [projects, flows]) + + const stats: { label: string, value: number, trend: number, tone: "success" | "warning" }[] = [ + {label: "Projects", value: projectsCount, trend: trends.projects, tone: "success"}, + {label: "Members", value: membersCount, trend: 0, tone: "success"}, + {label: "Saved minutes", value: savedMinutes, trend: trends.savedMinutes, tone: "success"}, + {label: "Invalid flows", value: invalidFlows.length, trend: trends.invalidFlows, tone: "warning"}, + ] + + return + + {stats.map((stat, index) => ( + + + + {stat.label} + {stat.value !== 0 && stat.trend !== 0 && ( + + {stat.tone === "warning" + ? + : } + {stat.trend} + + )} + + {stat.value} + + {index < stats.length - 1 && ( +
    + )} + + ))} + + +} diff --git a/src/packages/ce/src/namespace/views/NamespaceTabListView.tsx b/src/packages/ce/src/namespace/views/NamespaceTabListView.tsx deleted file mode 100644 index c704757a..00000000 --- a/src/packages/ce/src/namespace/views/NamespaceTabListView.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from "react"; -import {TabList, TabTrigger} from "@code0-tech/pictor/dist/components/tab/Tab"; -import {Button, Text, useService, useStore} from "@code0-tech/pictor"; -import {useParams} from "next/navigation"; -import {NamespaceService} from "@edition/namespace/services/Namespace.service"; -import {Namespace} from "@code0-tech/sagittarius-graphql-types"; - -export const NamespaceTabListView: React.FC = () => { - - const params = useParams() - const namespaceService = useService(NamespaceService) - const namespaceStore = useStore(NamespaceService) - - const namespaceIndex = params.namespaceId as any as number - const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` - - const namespace = React.useMemo( - () => namespaceService.getById(namespaceId), - [namespaceStore, namespaceId] - ) - - return - {namespace?.parent?.__typename === "Organization" && ( - <> - - - - - - - - )} - -} \ No newline at end of file diff --git a/src/packages/ce/src/namespace/views/NamespacesTopView.tsx b/src/packages/ce/src/namespace/views/NamespacesTopView.tsx deleted file mode 100644 index e6b84f14..00000000 --- a/src/packages/ce/src/namespace/views/NamespacesTopView.tsx +++ /dev/null @@ -1,20 +0,0 @@ -"use client" - -import React from "react"; -import {useRouter} from "next/navigation"; -import {NamespaceDataTableComponent} from "@edition/namespace/components/NamespaceDataTableComponent"; - -export const NamespacesTopView = () => { - - const router = useRouter() - - return <> - index < 5} onSelect={(namespace) => { - const number = namespace?.id?.match(/Namespace\/(\d+)$/)?.[1] - router.push(`/namespace/${number}`) - }}/> - - -} diff --git a/src/packages/ce/src/namespace/views/NamespacesView.tsx b/src/packages/ce/src/namespace/views/NamespacesView.tsx deleted file mode 100644 index c39b3329..00000000 --- a/src/packages/ce/src/namespace/views/NamespacesView.tsx +++ /dev/null @@ -1,124 +0,0 @@ -"use client" - -import { - Button, - Flex, - Menu, - MenuCheckboxItem, - MenuContent, - MenuPortal, - MenuTrigger, - Spacing, - Text -} from "@code0-tech/pictor"; -import Link from "next/link"; -import React from "react"; -import {useRouter} from "next/navigation"; -import {NamespaceDataTableComponent} from "@edition/namespace/components/NamespaceDataTableComponent"; -import { - NamespaceDataTableFilterInputComponent -} from "@edition/namespace/components/NamespaceDataTableFilterInputComponent"; -import {DataTableFilterProps, DataTableSortProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; -import {ButtonGroup} from "@code0-tech/pictor/dist/components/button-group/ButtonGroup"; -import {IconMinus, IconSortAscending, IconSortDescending} from "@tabler/icons-react"; - -export const NamespacesView = () => { - - const router = useRouter() - - const [filter, setFilter] = React.useState({}) - const [sort, setSort] = React.useState({}) - - return <> - -
    - - Workspaces - - - - Workspaces bring your organizations, projects and flows together in one shared place
    - so you and your team can create, organize and manage everything from a single central - view -
    -
    - - - - - - - - - - - { - if (sort["name"] === undefined) - setSort(prev => ({...prev, name: "asc"})) - else if (sort["name"] === "asc") - setSort(prev => ({...prev, name: "desc"})) - else - setSort(prev => ({...prev, name: undefined})) - - event.preventDefault() - event.stopPropagation() - }}> - {sort["name"] === undefined ? : sort["name"] === "asc" ? - : } - Name - - { - if (sort["createdAt"] === undefined) - setSort(prev => ({...prev, createdAt: "asc"})) - else if (sort["createdAt"] === "asc") - setSort(prev => ({...prev, createdAt: "desc"})) - else - setSort(prev => ({...prev, createdAt: undefined})) - - event.preventDefault() - event.stopPropagation() - }}> - {sort["createdAt"] === undefined ? - : sort["createdAt"] === "asc" ? - : } - Created At - - { - if (sort["updatedAt"] === undefined) - setSort(prev => ({...prev, updatedAt: "asc"})) - else if (sort["updatedAt"] === "asc") - setSort(prev => ({...prev, updatedAt: "desc"})) - else - setSort(prev => ({...prev, updatedAt: undefined})) - - event.preventDefault() - event.stopPropagation() - }}> - {sort["updatedAt"] === undefined ? - : sort["updatedAt"] === "asc" ? - : } - Updated At - - - - - -
    - -
    - setFilter(filter)}/> -
    - - { - const number = namespace?.id?.match(/Namespace\/(\d+)$/)?.[1] - router.push(`/namespace/${number}`) - }}/> - - -} diff --git a/src/packages/ce/src/organization/components/OrganizationCreateDialogComponent.tsx b/src/packages/ce/src/organization/components/OrganizationCreateDialogComponent.tsx new file mode 100644 index 00000000..187604b6 --- /dev/null +++ b/src/packages/ce/src/organization/components/OrganizationCreateDialogComponent.tsx @@ -0,0 +1,64 @@ +"use client" + +import React, {startTransition} from "react"; +import {Button, Flex, Spacing, TextInput, useForm, useService} from "@code0-tech/pictor"; +import {OrganizationService} from "@edition/organization/services/Organization.service"; +import {UserService} from "@edition/user/services/User.service"; +import {InputDialog} from "@core/components/InputDialog"; + +export interface OrganizationCreateDialogComponentProps { + open?: boolean + onOpenChange?: (open: boolean) => void +} + +export const OrganizationCreateDialogComponent: React.FC = ({open, onOpenChange}) => { + + const organizationService = useService(OrganizationService) + const userService = useService(UserService) + + const initialValues = React.useMemo(() => ({ + name: "" + }), []) + + const [inputs, validate] = useForm({ + useInitialValidation: false, + initialValues, + validate: { + name: (value) => { + if (!value) return "Name is required" + return null + } + }, + onSubmit: (values) => { + startTransition(() => { + organizationService.organizationCreate({ + name: values.name as unknown as string + }).then(payload => { + if ((payload?.errors?.length ?? 0) <= 0) { + userService.refetchCurrentUser().then(() => onOpenChange?.(false)) + } + }) + }) + } + }) + + return onOpenChange?.(open)}> + + + + + + +} diff --git a/src/packages/ce/src/organization/components/OrganizationDataTableComponent.tsx b/src/packages/ce/src/organization/components/OrganizationDataTableComponent.tsx deleted file mode 100644 index baff3903..00000000 --- a/src/packages/ce/src/organization/components/OrganizationDataTableComponent.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React from "react"; -import {DataTable, DataTableColumn, Text, useService, useStore} from "@code0-tech/pictor"; -import {DataTableFilterProps, DataTableSortProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; -import {Organization} from "@code0-tech/sagittarius-graphql-types"; -import {OrganizationDataTableRowComponent} from "@edition/organization/components/OrganizationDataTableRowComponent"; -import {OrganizationService} from "@edition/organization/services/Organization.service"; -import {MemberService} from "@edition/member/services/Member.service"; - -export interface OrganizationDataTableComponentProps { - sort?: DataTableSortProps - filter?: DataTableFilterProps - preFilter?: (project: Organization, index: number) => boolean - onSelect?: (item: Organization | undefined) => void - minimized?: boolean -} - -export const OrganizationDataTableComponent: React.FC = (props) => { - - const {sort, filter, preFilter = () => true, onSelect, minimized} = props - - const organizationService = useService(OrganizationService) - const organizationStore = useStore(OrganizationService) - const memberService = useService(MemberService) - const memberStore = useStore(MemberService) - - const organizations = React.useMemo( - () => organizationService.values(), - [organizationStore] - ) - - const data = React.useMemo( - () => organizations.map( - o => o.json() - ).filter(preFilter).map(o => { - const members = memberService.values({namespaceId: o.namespace?.id}) - const organization: Organization = { - ...o, - namespace: { - ...o.namespace, - members: { - nodes: members, - } - } - } - return organization - }), - [organizations, memberStore, preFilter] - ) - - return - - No organization found. Create one to get started. - - } - onSelect={(item) => item && onSelect?.(item)} - data={data}> - {(organization, index) => { - return - }} - - -} \ No newline at end of file diff --git a/src/packages/ce/src/organization/components/OrganizationDataTableFilterInputComponent.tsx b/src/packages/ce/src/organization/components/OrganizationDataTableFilterInputComponent.tsx deleted file mode 100644 index 4abd5813..00000000 --- a/src/packages/ce/src/organization/components/OrganizationDataTableFilterInputComponent.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import React from "react"; -import { - DataTableFilterInput, - DataTableFilterSuggestionMenu, - MenuCheckboxItem, - useService, - useStore -} from "@code0-tech/pictor"; -import {IconCheck} from "@tabler/icons-react"; -import {DataTableFilterProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; -import {OrganizationService} from "@edition/organization/services/Organization.service"; -import {NamespaceService} from "@edition/namespace/services/Namespace.service"; -import {MemberService} from "@edition/member/services/Member.service"; - -export interface OrganizationDataTableFilterInputComponentProps { - onChange: (filter: DataTableFilterProps) => void -} - -export const OrganizationDataTableFilterInputComponent: React.FC = (props) => { - - const {onChange} = props - - const organizationService = useService(OrganizationService) - const organizationStore = useStore(OrganizationService) - const namespaceStore = useStore(NamespaceService) - const namespaceService = useService(NamespaceService) - const memberService = useService(MemberService) - const memberStore = useStore(MemberService) - - const organizations = React.useMemo( - () => organizationService.values(), - [organizationStore] - ) - - const namespaces = React.useMemo( - () => organizations.map( - organization => namespaceService.getById(organization?.namespace?.id) - ), - [namespaceStore, organizations] - ) - - const members = React.useMemo( - () => namespaces.map( - namespace => memberService.values({namespaceId: namespace?.id}) - ).flat(), - [namespaces, memberStore, organizations] - ) - - return { - - const split = currentValue.split(",").map(s => s.trim()).filter(Boolean) - - return - {organizations.map(organization => { - - const isChecked = split.includes(organization?.name!) - - return { - e.preventDefault(); - e.stopPropagation(); - const updated = isChecked - ? split.filter(name => name !== organization?.name) - : [...split, organization?.name]; - applySuggestion(updated.join(","), true); - }}> - {isChecked ? : } - {organization?.name} - - })} - - } - }, { - token: "Member", - key: "namespace.members.nodes.user.username", - operators: ["isOneOf"], - suggestion: (context, operator, currentValue, applySuggestion) => { - - const split = currentValue.split(",").map(s => s.trim()).filter(Boolean) - - return - {members.map(member => { - - const isChecked = split.includes(member?.user?.username!) - - return { - e.preventDefault(); - e.stopPropagation(); - const updated = isChecked - ? split.filter(name => name !== member?.user?.username) - : [...split, member?.user?.username]; - applySuggestion(updated.join(","), true); - }}> - {isChecked ? : } - {member?.user?.username} - - })} - - } - } - ]}/> -} \ No newline at end of file diff --git a/src/packages/ce/src/organization/components/OrganizationDataTableRowComponent.tsx b/src/packages/ce/src/organization/components/OrganizationDataTableRowComponent.tsx deleted file mode 100644 index 3311d315..00000000 --- a/src/packages/ce/src/organization/components/OrganizationDataTableRowComponent.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import React from "react"; -import {Organization} from "@code0-tech/sagittarius-graphql-types"; -import {Avatar, Button, DataTableColumn, Flex, hashToColor, Text, useService, useStore} from "@code0-tech/pictor"; -import {IconLogout} from "@tabler/icons-react"; -import {OrganizationService} from "@edition/organization/services/Organization.service"; -import {NamespaceService} from "@edition/namespace/services/Namespace.service"; -import {MemberService} from "@edition/member/services/Member.service"; -import {UserService} from "@edition/user/services/User.service"; -import {formatDistanceToNow} from "date-fns"; -import {useUserSession} from "@edition/user/hooks/User.session.hook"; -import {OrganizationView} from "@edition/organization/services/Organization.view"; - -export interface OrganizationDataTableRowComponentProps { - organizationId: Organization['id'] - onLeave?: (organization: OrganizationView) => void - minimized?: boolean -} - -export const OrganizationDataTableRowComponent: React.FC = (props) => { - - const {organizationId, onLeave, minimized} = props - - const organizationService = useService(OrganizationService) - const organizationStore = useStore(OrganizationService) - const namespaceStore = useStore(NamespaceService) - const namespaceService = useService(NamespaceService) - const memberService = useService(MemberService) - const memberStore = useStore(MemberService) - const userService = useService(UserService) - const userStore = useStore(UserService) - - const currentSession = useUserSession() - - const currentUser = React.useMemo( - () => userService.getById(currentSession?.user?.id), - [userStore, currentSession] - ) - - const organization = React.useMemo( - () => organizationService.getById(organizationId), - [organizationStore, organizationId] - ) - - const namespace = React.useMemo( - () => organization ? namespaceService.getById(organization.namespace?.id) : null, - [namespaceStore, organization] - ) - - const namespaceMember = React.useMemo( - () => namespace && currentUser ? memberService.getByNamespaceIdAndUserId(namespace.id, currentUser.id) : null, - [memberStore, namespace, currentUser] - ) - - const namespaceMembers = React.useMemo( - () => memberService.values({namespaceId: namespace?.id}), - [memberStore, userStore, namespace] - ) - - return <> - - - - - - - {organization?.name} - - - - {!minimized && ( - - Updated {formatDistanceToNow(organization?.updatedAt!)} ago - - )} - - - - {!minimized && ( - <> - - - {namespace?.projects?.count ? ( - - {namespace?.projects?.count ?? 0} {" "} - projects - - ) : null} - - {namespace?.members?.count ? ( - - {namespace?.members?.count ?? 0}{" "} - members - - ) : null} - - {namespace?.runtimes?.count ? ( - - {namespace?.runtimes?.count ?? 0}{" "} - connected runtimes - - ) : null} - - - {onLeave && namespaceMember && namespaceMember.userAbilities?.deleteMember ? ( - - - - ) : null} - - )} - - -} \ No newline at end of file diff --git a/src/packages/ce/src/organization/pages/OrganizationCreatePage.tsx b/src/packages/ce/src/organization/pages/OrganizationCreatePage.tsx index 9f11a124..dcc15625 100644 --- a/src/packages/ce/src/organization/pages/OrganizationCreatePage.tsx +++ b/src/packages/ce/src/organization/pages/OrganizationCreatePage.tsx @@ -1,72 +1,25 @@ "use client" -import {Button, Col, Flex, Spacing, Text, TextInput, useForm, useService} from "@code0-tech/pictor"; -import Link from "next/link"; -import {OrganizationService} from "@edition/organization/services/Organization.service"; -import {UserService} from "@edition/user/services/User.service"; import React from "react"; import {useRouter} from "next/navigation"; +import {OrganizationCreateDialogComponent} from "@edition/organization/components/OrganizationCreateDialogComponent"; -export const OrganizationCreatePage = () => { +export const OrganizationCreatePage: React.FC = () => { - const organizationService = useService(OrganizationService) - const userService = useService(UserService) - const [, startTransition] = React.useTransition() const router = useRouter() - const [inputs, validate] = useForm({ - useInitialValidation: false, - initialValues: { - name: null, - }, - validate: { - name: (value) => { - if (!value) return "Name is required" - return null - } - }, - onSubmit: (values) => { - startTransition(() => { - organizationService.organizationCreate({ - name: values.name as unknown as string - }).then(payload => { - if ((payload?.errors?.length ?? 0) <= 0) { - userService.refetchCurrentUser().then(() => router.push("/workspaces")) - } - }) - }) - } - }) + return { + if (open) return - return
    - - - - Create new organization - - - - Organizations are helpfully if managing a group of users and plenty of projects. - - - - - - - - - - - - -
    -} \ No newline at end of file + const nav = (window as unknown as { navigation?: { entries(): { url: string }[], currentEntry?: { index: number } } }).navigation + if (!nav?.entries) { + router.back() + return + } + + const index = nav.currentEntry?.index ?? 0 + if (index > 0) router.back() + else router.push("/workspaces") + }}/> +} diff --git a/src/packages/ce/src/organization/pages/OrganizationsPage.tsx b/src/packages/ce/src/organization/pages/OrganizationsPage.tsx deleted file mode 100644 index ee3ab751..00000000 --- a/src/packages/ce/src/organization/pages/OrganizationsPage.tsx +++ /dev/null @@ -1,17 +0,0 @@ -"use client" - -import React from "react"; -import {OrganizationsView} from "@edition/organization/views/OrganizationsView"; - -export const OrganizationsPage = () => { - return
    - -
    - -} \ No newline at end of file diff --git a/src/packages/ce/src/organization/views/OrganizationsTopView.tsx b/src/packages/ce/src/organization/views/OrganizationsTopView.tsx deleted file mode 100644 index bab3740c..00000000 --- a/src/packages/ce/src/organization/views/OrganizationsTopView.tsx +++ /dev/null @@ -1,20 +0,0 @@ -"use client" - -import React from "react"; -import {useRouter} from "next/navigation"; -import {OrganizationDataTableComponent} from "@edition/organization/components/OrganizationDataTableComponent"; - -export const OrganizationsTopView = () => { - - const router = useRouter() - - return <> - index < 5} onSelect={(organization) => { - const number = organization?.namespace?.id?.match(/Namespace\/(\d+)$/)?.[1] - router.push(`/namespace/${number}`) - }}/> - - -} \ No newline at end of file diff --git a/src/packages/ce/src/organization/views/OrganizationsView.tsx b/src/packages/ce/src/organization/views/OrganizationsView.tsx deleted file mode 100644 index 3f7cb0d1..00000000 --- a/src/packages/ce/src/organization/views/OrganizationsView.tsx +++ /dev/null @@ -1,122 +0,0 @@ -"use client" - -import { - Button, - Flex, - Menu, - MenuCheckboxItem, - MenuContent, - MenuPortal, - MenuTrigger, - Spacing, - Text -} from "@code0-tech/pictor"; -import Link from "next/link"; -import React from "react"; -import {useRouter} from "next/navigation"; -import {OrganizationDataTableComponent} from "@edition/organization/components/OrganizationDataTableComponent"; -import { - OrganizationDataTableFilterInputComponent -} from "@edition/organization/components/OrganizationDataTableFilterInputComponent"; -import {DataTableFilterProps, DataTableSortProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; -import {ButtonGroup} from "@code0-tech/pictor/dist/components/button-group/ButtonGroup"; -import {IconMinus, IconSortAscending, IconSortDescending} from "@tabler/icons-react"; - -export const OrganizationsView = () => { - - const router = useRouter() - - const [filter, setFilter] = React.useState({}) - const [sort, setSort] = React.useState({}) - - return <> - - - - Organizations - - - Manage organizations that you belong to. You can create new organizations and switch between them. - - - - - - - - - - - - - { - if (sort["name"] === undefined) - setSort(prev => ({...prev, name: "asc"})) - else if (sort["name"] === "asc") - setSort(prev => ({...prev, name: "desc"})) - else - setSort(prev => ({...prev, name: undefined})) - - event.preventDefault() - event.stopPropagation() - }}> - {sort["name"] === undefined ? : sort["name"] === "asc" ? - : } - Name - - { - if (sort["createdAt"] === undefined) - setSort(prev => ({...prev, createdAt: "asc"})) - else if (sort["createdAt"] === "asc") - setSort(prev => ({...prev, createdAt: "desc"})) - else - setSort(prev => ({...prev, createdAt: undefined})) - - event.preventDefault() - event.stopPropagation() - }}> - {sort["createdAt"] === undefined ? - : sort["createdAt"] === "asc" ? - : } - Created At - - { - if (sort["updatedAt"] === undefined) - setSort(prev => ({...prev, updatedAt: "asc"})) - else if (sort["updatedAt"] === "asc") - setSort(prev => ({...prev, updatedAt: "desc"})) - else - setSort(prev => ({...prev, updatedAt: undefined})) - - event.preventDefault() - event.stopPropagation() - }}> - {sort["updatedAt"] === undefined ? - : sort["updatedAt"] === "asc" ? - : } - Updated At - - - - - - - -
    - setFilter(filter)}/> -
    - - {/**TODO: use namespaceId*/} - { - const number = organization?.namespace?.id?.match(/Namespace\/(\d+)$/)?.[1] - router.push(`/namespace/${number}`) - }}/> - - -} \ No newline at end of file diff --git a/src/packages/ce/src/project/components/ProjectCreateDialogComponent.tsx b/src/packages/ce/src/project/components/ProjectCreateDialogComponent.tsx new file mode 100644 index 00000000..89f138bc --- /dev/null +++ b/src/packages/ce/src/project/components/ProjectCreateDialogComponent.tsx @@ -0,0 +1,85 @@ +"use client" + +import React, {startTransition} from "react"; +import {Button, Spacing, TextInput, toast, useForm, useService} from "@code0-tech/pictor"; +import {Namespace} from "@code0-tech/sagittarius-graphql-types"; +import {ProjectService} from "@edition/project/services/Project.service"; +import {InputDialog} from "@core/components/InputDialog"; + +export interface ProjectCreateDialogComponentProps { + open?: boolean + onOpenChange?: (open: boolean) => void + namespaceId?: Namespace['id'] +} + +export const ProjectCreateDialogComponent: React.FC = ({open, onOpenChange, namespaceId}) => { + + //TODO: user abilities for project creation within namespace + + const projectService = useService(ProjectService) + + const initialValues = React.useMemo(() => ({name: null, description: null}), []) + + const [inputs, validate] = useForm<{ name: string | null, description: string | null }>({ + useInitialValidation: false, + initialValues: initialValues, + validate: { + name: (value) => { + if (!value) return "Name is required" + if (value.length < 3) return "Name needs to be at least 3 characters" + if (value.length > 50) return "Name needs to be less than 50 characters" + return null + }, + description: (value) => { + if (!value) return "Description is required" + if (value.length > 50) return "Description needs to be less than 50 characters" + return null + }, + }, + onSubmit: (values) => { + startTransition(() => { + if (!namespaceId) { + toast({ + title: "The current user does not have a personal namespace.", + color: "error", + }) + return + } + projectService.projectCreate({ + name: values.name!!, + description: values.description!!, + namespaceId: namespaceId + }).then(payload => { + if ((payload?.errors?.length ?? 0) <= 0) { + onOpenChange?.(false) + } + }) + }) + } + }) + + return onOpenChange?.(open)}> + + + + + + +} diff --git a/src/packages/ce/src/project/components/ProjectDataTableFilterInputComponent.tsx b/src/packages/ce/src/project/components/ProjectDataTableFilterInputComponent.tsx deleted file mode 100644 index ea640a80..00000000 --- a/src/packages/ce/src/project/components/ProjectDataTableFilterInputComponent.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import React from "react"; -import { - DataTableFilterInput, - DataTableFilterSuggestionMenu, - MenuCheckboxItem, - useService, - useStore -} from "@code0-tech/pictor"; -import {IconCheck} from "@tabler/icons-react"; -import {Namespace} from "@code0-tech/sagittarius-graphql-types"; -import {DataTableFilterProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; -import {ProjectService} from "@edition/project/services/Project.service"; -import {RuntimeService} from "@edition/runtime/services/Runtime.service"; - -export interface ProjectDataTableFilterInputComponentProps { - namespaceId: Namespace['id'] - onChange: (filter: DataTableFilterProps) => void -} - -export const ProjectDataTableFilterInputComponent: React.FC = (props) => { - - const {namespaceId, onChange} = props - - const projectService = useService(ProjectService) - const projectStore = useStore(ProjectService) - const runtimeService = useService(RuntimeService) - const runtimeStore = useStore(RuntimeService) - - const projects = React.useMemo( - () => projectService.values({namespaceId: namespaceId}), - [projectStore, namespaceId] - ) - - const allPrimaryRuntimes = React.useMemo( - () => { - const runtimeIds = projects.map(p => p.primaryRuntime?.id) - return runtimeIds.map(id => runtimeService.getById(id)).filter(Boolean) - }, - [projects, runtimeStore] - ) - - return { - - const split = currentValue.split(",").map(s => s.trim()).filter(Boolean) - - return - {projects.map(project => { - - const isChecked = split.includes(project?.name!) - - return { - e.preventDefault(); - e.stopPropagation(); - const updated = isChecked - ? split.filter(name => name !== project?.name) - : [...split, project?.name]; - applySuggestion(updated.join(","), true); - }}> - {isChecked ? : } - {project?.name} - - })} - - } - }, { - token: "Primary Runtime", - key: "primaryRuntime.name", - operators: ["isOneOf"], - suggestion: (context, operator, currentValue, applySuggestion) => { - - const split = currentValue.split(",").map(s => s.trim()).filter(Boolean) - - return - {allPrimaryRuntimes.map(runtime => { - - const isChecked = split.includes(runtime?.name!) - - return { - e.preventDefault(); - e.stopPropagation(); - const updated = isChecked - ? split.filter(name => name !== runtime?.name) - : [...split, runtime?.name]; - applySuggestion(updated.join(","), true); - }}> - {isChecked ? : } - {runtime?.name} - - })} - - } - } - ]}/> -} \ No newline at end of file diff --git a/src/packages/ce/src/project/components/ProjectSettingsDialogComponent.tsx b/src/packages/ce/src/project/components/ProjectSettingsDialogComponent.tsx new file mode 100644 index 00000000..469f0070 --- /dev/null +++ b/src/packages/ce/src/project/components/ProjectSettingsDialogComponent.tsx @@ -0,0 +1,49 @@ +"use client" + +import React from "react"; +import {Button, Text} from "@code0-tech/pictor"; +import {TabList, TabTrigger} from "@code0-tech/pictor/dist/components/tab/Tab"; +import {IconServer, IconSettings2, IconTrash} from "@tabler/icons-react"; +import {SettingDialog} from "@core/components/SettingDialog"; +import {ProjectSettingsGeneralView} from "@edition/project/views/ProjectSettingsGeneralView"; +import {ProjectSettingsAssignedServersView} from "@edition/project/views/ProjectSettingsAssignedServersView"; +import {ProjectSettingsDeleteView} from "@edition/project/views/ProjectSettingsDeleteView"; + +export interface ProjectSettingsDialogComponentProps { + open?: boolean + onOpenChange?: (open: boolean) => void +} + +export const ProjectSettingsDialogComponent: React.FC = (props) => { + + const {open, onOpenChange} = props + + return onOpenChange?.(open)} + title={"Project settings"} + description={"General settings, assigned servers and restrictions for this project. These settings affect everyone who has access to this project."} + trigger={ + + + + + + + + + + }> + + + + +} diff --git a/src/packages/ce/src/project/layout/ProjectLayout.tsx b/src/packages/ce/src/project/layout/ProjectLayout.tsx new file mode 100644 index 00000000..d5bd630b --- /dev/null +++ b/src/packages/ce/src/project/layout/ProjectLayout.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import {ResizablePanelGroup} from "@code0-tech/pictor/dist/components/resizable/Resizable"; +import {WorkerProvider} from "@edition/flow/components/FlowWorkerProvider"; +import {ReactFlowProvider} from "@xyflow/react"; + +interface ProjectLayoutProps { + children: React.ReactNode +} + +export const ProjectLayout: React.FC = ({children}) => { + + return + + + {children} + + + +} \ No newline at end of file diff --git a/src/packages/ce/src/flow/pages/FlowOverviewPage.tsx b/src/packages/ce/src/project/pages/ProjectOverviewPage.tsx similarity index 94% rename from src/packages/ce/src/flow/pages/FlowOverviewPage.tsx rename to src/packages/ce/src/project/pages/ProjectOverviewPage.tsx index 355930a7..7722e56f 100644 --- a/src/packages/ce/src/flow/pages/FlowOverviewPage.tsx +++ b/src/packages/ce/src/project/pages/ProjectOverviewPage.tsx @@ -25,7 +25,7 @@ import { } from "@code0-tech/sagittarius-graphql-types"; import {FlowService} from "@edition/flow/services/Flow.service"; import {mapAiGenerationFlowToFlowInput} from "@edition/ai/util/AI.flow.mapper"; -import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {toast} from "@code0-tech/pictor/dist/components/toast/Toast"; const flowTemplates = [ { @@ -46,7 +46,7 @@ const flowTemplates = [ }, ] -export const FlowOverviewPage: React.FC = () => { +export const ProjectOverviewPage: React.FC = () => { const params = useParams() const [prompt, setPrompt] = React.useState("") @@ -75,14 +75,13 @@ export const FlowOverviewPage: React.FC = () => { projectId: projectId, }).then(result => { if ((result?.errors?.length ?? 0) <= 0) { - addIslandSuccessNotification({message: "Created flow"}) + toast({title: "Created flow", color: "success"}) } }) }, [flowService, flowStore, namespaceId, projectId]) return - + style={{borderRadius: "1rem"}}> diff --git a/src/packages/ce/src/project/pages/ProjectSettingsPage.tsx b/src/packages/ce/src/project/pages/ProjectSettingsPage.tsx index 91abc8cd..d58f8d2f 100644 --- a/src/packages/ce/src/project/pages/ProjectSettingsPage.tsx +++ b/src/packages/ce/src/project/pages/ProjectSettingsPage.tsx @@ -1,44 +1,30 @@ "use client" -import {Tab, TabList, TabTrigger} from "@code0-tech/pictor/dist/components/tab/Tab"; import React from "react"; -import {Button, Text} from "@code0-tech/pictor"; -import {ProjectSettingsGeneralView} from "@edition/project/views/ProjectSettingsGeneralView"; -import {ProjectSettingsDeleteView} from "@edition/project/views/ProjectSettingsDeleteView"; -import { - ResizableHandle, - ResizablePanel, - ResizablePanelGroup -} from "@code0-tech/pictor/dist/components/resizable/Resizable"; -import {SidebarComponent} from "@core/components/SidebarComponent"; +import {useParams, useRouter} from "next/navigation"; +import {ProjectSettingsDialogComponent} from "@edition/project/components/ProjectSettingsDialogComponent"; export const ProjectSettingsPage: React.FC = () => { - return - - - - - - - - - - - - - - <> - - - - - - -} \ No newline at end of file + + const params = useParams() + const router = useRouter() + + const namespaceIndex = params.namespaceId as any as number + const projectIndex = params.projectId as any as number + const projectHref = `/namespace/${namespaceIndex}/project/${projectIndex}` + + return { + if (open) return + + const nav = (window as unknown as { navigation?: { entries(): { url: string }[], currentEntry?: { index: number } } }).navigation + if (!nav?.entries) { + router.back() + return + } + + const index = nav.currentEntry?.index ?? 0 + if (index > 0) router.back() + else router.push(projectHref) + }}/> +} diff --git a/src/packages/ce/src/project/views/PersonalProjectsView.tsx b/src/packages/ce/src/project/views/PersonalProjectsView.tsx deleted file mode 100644 index 013a4c7f..00000000 --- a/src/packages/ce/src/project/views/PersonalProjectsView.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from "react"; -import { - Button, - Flex, - Menu, - MenuCheckboxItem, - MenuContent, - MenuPortal, - MenuTrigger, - Spacing, - Text, - useService, - useStore -} from "@code0-tech/pictor"; -import {UserService} from "@edition/user/services/User.service"; -import Link from "next/link"; -import {useRouter} from "next/navigation"; -import {ProjectDataTableComponent} from "@edition/project/components/ProjectDataTableComponent"; -import {DataTableFilterProps, DataTableSortProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; -import {ProjectDataTableFilterInputComponent} from "@edition/project/components/ProjectDataTableFilterInputComponent"; -import {ButtonGroup} from "@code0-tech/pictor/dist/components/button-group/ButtonGroup"; -import {IconMinus, IconSortAscending, IconSortDescending} from "@tabler/icons-react"; -import {useUserSession} from "@edition/user/hooks/User.session.hook"; - -export const PersonalProjectsView: React.FC = () => { - - const router = useRouter() - const userSession = useUserSession() - const userStore = useStore(UserService) - const userService = useService(UserService) - - const currentUser = React.useMemo(() => userService.getById(userSession?.user?.id), [userStore, userSession]) - const namespaceId = currentUser?.namespace?.id - const namespaceIndex = namespaceId?.match(/Namespace\/(\d+)$/)?.[1] - - const [filter, setFilter] = React.useState({}) - const [sort, setSort] = React.useState({}) - - const projectsList = React.useMemo(() => { - if (!currentUser || !currentUser.namespace) return null - return { - const number = project?.id?.match(/NamespaceProject\/(\d+)$/)?.[1] - router.push(`/namespace/${namespaceIndex}/project/${number}/flow`) - }} namespaceId={currentUser.namespace.id}/> - - }, [currentUser, filter, sort]) - - return <> - {projectsList} - - - -} \ No newline at end of file diff --git a/src/packages/ce/src/project/views/ProjectListView.tsx b/src/packages/ce/src/project/views/ProjectListView.tsx new file mode 100644 index 00000000..19eb9f7e --- /dev/null +++ b/src/packages/ce/src/project/views/ProjectListView.tsx @@ -0,0 +1,59 @@ +"use client" + +import React from "react"; +import {Avatar, Badge, Button, Flex, hashToColor, Spacing, Text, useService, useStore} from "@code0-tech/pictor"; +import {IconPlus} from "@tabler/icons-react"; +import Link from "next/link"; +import {useParams} from "next/navigation"; +import {Namespace, NamespaceProject} from "@code0-tech/sagittarius-graphql-types"; +import {ProjectService} from "@edition/project/services/Project.service"; + +export const ProjectListView: React.FC = () => { + + const params = useParams() + const projectService = useService(ProjectService) + const projectStore = useStore(ProjectService) + + const namespaceIndex = params.namespaceId as any as number + const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` + + const projects = React.useMemo( + () => projectService.values({namespaceId}) + .filter((project): project is NamespaceProject => !!project), + [namespaceId, projectStore] + ) + + return + + + Projects + + + {projects.length} + + + + {projects.map(project => { + const projectIndex = project.id?.match(/NamespaceProject\/(\d+)$/)?.[1] + return + + + })} + + + + +} diff --git a/src/packages/ce/src/project/views/ProjectMenuView.tsx b/src/packages/ce/src/project/views/ProjectMenuView.tsx new file mode 100644 index 00000000..d763508d --- /dev/null +++ b/src/packages/ce/src/project/views/ProjectMenuView.tsx @@ -0,0 +1,140 @@ +"use client" + +import React from "react"; +import { + Avatar, + Button, + Flex, + hashToColor, + Menu, + MenuContent, + MenuItem, + MenuLabel, + MenuPortal, + MenuSeparator, + MenuSub, + MenuSubContent, + MenuSubTrigger, + MenuTrigger, Spacing, + Text, + useService, + useStore +} from "@code0-tech/pictor"; +import { + IconArrowLeft, + IconCheck, + IconChevronDown, + IconChevronRight, + IconSettings, + IconSwitch +} from "@tabler/icons-react"; +import Link from "next/link"; +import {useParams} from "next/navigation"; +import {Namespace, NamespaceProject} from "@code0-tech/sagittarius-graphql-types"; +import {ProjectService} from "@edition/project/services/Project.service"; + +export const ProjectMenuView: React.FC = () => { + + const params = useParams() + const projectService = useService(ProjectService) + const projectStore = useStore(ProjectService) + + const namespaceIndex = params.namespaceId as any as number + const projectIndex = params.projectId as any as number + const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` + const projectId: NamespaceProject['id'] = `gid://sagittarius/NamespaceProject/${projectIndex}` + + const project = React.useMemo( + () => projectService.getById(projectId, {namespaceId}), + [projectStore, projectId, namespaceId] + ) + + const projects = React.useMemo( + () => projectService.values({namespaceId}) + .filter((entry): entry is NamespaceProject => !!entry), + [projectStore, namespaceId] + ) + + const name = project?.name ?? "" + + return <> + + + + + + + + + + + + Project + + + + + Settings + + + + + + Servers + + + + + + + + Switch Project + + + + + {projects.map(entry => { + const number = entry.id?.match(/NamespaceProject\/(\d+)$/)?.[1] + const entryName = entry.name ?? "" + return + + + + {entryName} + + + + + })} + + + + + + +} diff --git a/src/packages/ce/src/project/views/ProjectRecentListView.tsx b/src/packages/ce/src/project/views/ProjectRecentListView.tsx new file mode 100644 index 00000000..09d1d487 --- /dev/null +++ b/src/packages/ce/src/project/views/ProjectRecentListView.tsx @@ -0,0 +1,69 @@ +"use client" + +import React from "react"; +import {Avatar, Badge, Button, Flex, hashToColor, Spacing, Text, useService, useStore} from "@code0-tech/pictor"; +import Link from "next/link"; +import {NamespaceProject} from "@code0-tech/sagittarius-graphql-types"; +import {ProjectService} from "@edition/project/services/Project.service"; +import {UserService} from "@edition/user/services/User.service"; +import {useUserSession} from "@edition/user/hooks/User.session.hook"; + +const RECENT_PROJECTS_LIMIT = 10 + +export const ProjectRecentListView: React.FC = () => { + + const projectService = useService(ProjectService) + const projectStore = useStore(ProjectService) + const userService = useService(UserService) + const userStore = useStore(UserService) + + const currentSession = useUserSession() + + const currentUser = React.useMemo( + () => userService.getById(currentSession?.user?.id), + [userStore, userService, currentSession?.user?.id] + ) + + const memberships = React.useMemo( + () => currentUser?.namespaceMemberships?.nodes ?? [], + [currentUser?.namespaceMemberships?.nodes?.length] + ) + + const projects = React.useMemo( + () => memberships + .flatMap(membership => membership?.namespace?.id + ? projectService.values({namespaceId: membership.namespace.id}) + : []) + .filter((project): project is NamespaceProject => !!project) + .sort((a, b) => new Date(String(b.updatedAt)).getTime() - new Date(String(a.updatedAt)).getTime()) + .slice(0, RECENT_PROJECTS_LIMIT), + [memberships.length, projectStore] + ) + + return + + + Recent projects + + + {projects.length} + + + + {projects.map(project => { + const namespaceIndex = project.namespace?.id?.match(/Namespace\/(\d+)$/)?.[1] + const projectIndex = project.id?.match(/NamespaceProject\/(\d+)$/)?.[1] + return + + + })} + +} diff --git a/src/packages/ce/src/project/views/ProjectSettingsAssignedServersView.tsx b/src/packages/ce/src/project/views/ProjectSettingsAssignedServersView.tsx new file mode 100644 index 00000000..17b03b00 --- /dev/null +++ b/src/packages/ce/src/project/views/ProjectSettingsAssignedServersView.tsx @@ -0,0 +1,125 @@ +"use client" + +import React, {startTransition} from "react"; +import { + Avatar, + Badge, + Button, + ButtonGroup, + Flex, + hashToColor, + Menu, + MenuContent, + MenuItem, + MenuPortal, + MenuSeparator, + MenuTrigger, + Spacing, + Text, + useService, + useStore +} from "@code0-tech/pictor"; +import {TabContent} from "@code0-tech/pictor/dist/components/tab/Tab"; +import {IconPlus} from "@tabler/icons-react"; +import {useParams, useRouter} from "next/navigation"; +import {Namespace, NamespaceProject, Runtime} from "@code0-tech/sagittarius-graphql-types"; +import {ProjectService} from "@edition/project/services/Project.service"; +import {RuntimeService} from "@edition/runtime/services/Runtime.service"; +import {RuntimeProjectDataTableComponent} from "@edition/runtime/components/RuntimeProjectDataTableComponent"; + +export const ProjectSettingsAssignedServersView: React.FC = () => { + + const params = useParams() + const router = useRouter() + const projectService = useService(ProjectService) + const projectStore = useStore(ProjectService) + const runtimeService = useService(RuntimeService) + const runtimeStore = useStore(RuntimeService) + + const namespaceIndex = params.namespaceId as any as number + const projectIndex = params.projectId as any as number + const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` + const projectId: NamespaceProject['id'] = `gid://sagittarius/NamespaceProject/${projectIndex}` + + const project = React.useMemo( + () => projectService.getById(projectId, {namespaceId}), + [projectStore, projectId, namespaceId] + ) + + const availableRuntimes = React.useMemo(() => { + const globalRuntimes = runtimeService.values().filter(runtime => !runtime?.namespace?.id) + const namespaceRuntimes = runtimeService.values({namespaceId}) + return [...globalRuntimes, ...namespaceRuntimes] + }, [runtimeStore, namespaceId]) + + const assignedCount = React.useMemo( + () => project?.runtimes?.nodes?.length ?? 0, + [project] + ) + + const assignRuntime = React.useCallback((runtimeId: Runtime['id']) => { + const runtimeIds = project?.runtimes?.nodes?.map(r => r?.id!) ?? [] + + if (!runtimeId) return + if (runtimeIds.includes(runtimeId)) return + + startTransition(() => { + projectService.projectAssignRuntimes({ + runtimeIds: [...runtimeIds, runtimeId], + namespaceProjectId: projectId + }) + }) + }, [projectService, projectId, project]) + + return + + + Assigned servers + {assignedCount} + + + + + + + + + {availableRuntimes.map((runtime, index) => ( + + assignRuntime(runtime.id)}> + + + + {runtime?.name} + + {runtime?.status} + + + + + {index < availableRuntimes.length - 1 && } + + ))} + + + + + + + + The runtimes assigned to this project. Assigned runtimes are the environments where this project's flows run. + + + { + const number = runtime?.id?.match(/Runtime\/(\d+)$/)?.[1] + if (number) router.push(`/namespace/${namespaceIndex}/runtimes/${number}/settings`) + }}/> + +} diff --git a/src/packages/ce/src/project/views/ProjectSettingsDeleteView.tsx b/src/packages/ce/src/project/views/ProjectSettingsDeleteView.tsx index 1dac2bc1..93d30976 100644 --- a/src/packages/ce/src/project/views/ProjectSettingsDeleteView.tsx +++ b/src/packages/ce/src/project/views/ProjectSettingsDeleteView.tsx @@ -1,11 +1,11 @@ "use client" import React, {startTransition} from "react"; -import {Button, Card, Flex, Spacing, Text, useService} from "@code0-tech/pictor"; +import {Button, DialogClose, Spacing, Text, useService} from "@code0-tech/pictor"; import {TabContent} from "@code0-tech/pictor/dist/components/tab/Tab"; import {useParams, useRouter} from "next/navigation"; import {ProjectService} from "@edition/project/services/Project.service"; -import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {toast} from "@code0-tech/pictor/dist/components/toast/Toast"; export const ProjectSettingsDeleteView: React.FC = () => { @@ -22,9 +22,7 @@ export const ProjectSettingsDeleteView: React.FC = () => { namespaceProjectId: `gid://sagittarius/NamespaceProject/${projectId}` }).then(payload => { if ((payload?.errors?.length ?? 0) <= 0) { - addIslandSuccessNotification({ - message: "Deleted project" - }) + toast({title: "Deleted project", color: "success"}) } router.push(`/namespace/${namespaceId}`) }) @@ -32,22 +30,16 @@ export const ProjectSettingsDeleteView: React.FC = () => { }, [projectId]) - return - - Delete - - - - - - - This will delete the project and cannot be undone. - - - - - + return + Delete project + + + Permanently delete this project and everything in it. This action cannot be undone. + + + + + + } \ No newline at end of file diff --git a/src/packages/ce/src/project/views/ProjectSettingsGeneralView.tsx b/src/packages/ce/src/project/views/ProjectSettingsGeneralView.tsx index 6e71424b..7ba0806a 100644 --- a/src/packages/ce/src/project/views/ProjectSettingsGeneralView.tsx +++ b/src/packages/ce/src/project/views/ProjectSettingsGeneralView.tsx @@ -17,7 +17,7 @@ import CardSection from "@code0-tech/pictor/dist/components/card/CardSection"; import {useParams} from "next/navigation"; import {ProjectService} from "@edition/project/services/Project.service"; import {Namespace, NamespaceProject} from "@code0-tech/sagittarius-graphql-types"; -import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {toast} from "@code0-tech/pictor/dist/components/toast/Toast"; export const ProjectSettingsGeneralView: React.FC = () => { @@ -77,23 +77,25 @@ export const ProjectSettingsGeneralView: React.FC = () => { namespaceProjectId: projectId }).then(payload => { if ((payload?.errors?.length ?? 0) <= 0) { - addIslandSuccessNotification({ - message: "Updated project" - }) + toast({title: "Updated project", color: "success"}) } }) }) } }) - return - - General - - + + + The name, description and general settings for this project. + + diff --git a/src/packages/ce/src/project/views/ProjectTableView.tsx b/src/packages/ce/src/project/views/ProjectTableView.tsx new file mode 100644 index 00000000..c3f45cb2 --- /dev/null +++ b/src/packages/ce/src/project/views/ProjectTableView.tsx @@ -0,0 +1,206 @@ +"use client" + +import React from "react"; +import { + Avatar, + Badge, + Button, + ButtonGroup, + Card, + DataTable, + DataTableColumn, + DataTableHeader, + DataTableHeaderColumn, + DataTableMaxPaginationValue, + DataTablePagination, + DataTablePaginationBackwardsTrigger, + DataTablePaginationForwardTrigger, + DataTablePaginationValue, + Flex, + hashToColor, + Menu, + MenuContent, + MenuItem, + MenuLabel, + MenuPortal, + MenuTrigger, + Spacing, + Text, + useService, + useStore +} from "@code0-tech/pictor"; +import {DataTableSortProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; +import { + IconAdjustmentsHorizontal, + IconArrowsSort, + IconCheck, + IconChevronLeft, + IconChevronRight, + IconPlus +} from "@tabler/icons-react"; +import Link from "next/link"; +import {useParams, useRouter} from "next/navigation"; +import {Namespace} from "@code0-tech/sagittarius-graphql-types"; +import {ProjectService} from "@edition/project/services/Project.service"; + +const filterLabels = { + all: "All", + withFlows: "With flows", + empty: "Empty", +} as const + +const sortLabels = { + name: "Name", + flows: "Flows", + createdAt: "Created", + updatedAt: "Updated", +} as const + +export const ProjectTableView: React.FC = () => { + + const params = useParams() + const router = useRouter() + const projectService = useService(ProjectService) + const projectStore = useStore(ProjectService) + + const namespaceIndex = params.namespaceId as any as number + const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` + + const [filter, setFilter] = React.useState("all") + const [sort, setSort] = React.useState("name") + + const projects = React.useMemo( + () => projectService.values({namespaceId}), + [projectStore, namespaceId] + ) + + const decoratedProjects = React.useMemo(() => projects.map(project => ({ + id: project.id, + name: project.name ?? "", + flows: project.flows?.count ?? 0, + createdAt: project.createdAt, + updatedAt: project.updatedAt, + })), [projects]) + + const preFilter = React.useCallback((project: (typeof decoratedProjects)[number]) => { + if (filter === "withFlows") return project.flows > 0 + if (filter === "empty") return project.flows === 0 + return true + }, [filter]) + + const visibleProjects = React.useMemo( + () => decoratedProjects.filter(preFilter), + [decoratedProjects, preFilter] + ) + + const sortProps: DataTableSortProps = {[sort]: "asc"} + + return <> + + + Projects + {visibleProjects.length} + + + + {/* Filter */} + + + + + + + Filter + {(Object.keys(filterLabels) as (keyof typeof filterLabels)[]).map((k) => ( + setFilter(k)}> + + {filterLabels[k]} + + ))} + + + + + {/* Sort */} + + + + + + + Sort by + {(Object.keys(sortLabels) as (keyof typeof sortLabels)[]).map((k) => ( + setSort(k)}> + + {sortLabels[k]} + + ))} + + + + + {/* Create */} + + + + + + + + + No projects found. + } + onSelect={(project) => { + const number = project?.id?.match(/NamespaceProject\/(\d+)$/)?.[1] + if (number) router.push(`/namespace/${namespaceIndex}/project/${number}`) + }}> + + + Project + + + Flows + + + {(project) => <> + + + + {project.name} + + + + {project.flows} flows + + } + + + Page / + + + + + + + + + + + + + +} diff --git a/src/packages/ce/src/project/views/ProjectsView.tsx b/src/packages/ce/src/project/views/ProjectsView.tsx deleted file mode 100644 index be1aca32..00000000 --- a/src/packages/ce/src/project/views/ProjectsView.tsx +++ /dev/null @@ -1,133 +0,0 @@ -"use client" - - -import React from "react"; -import { - Button, - Flex, - Menu, - MenuCheckboxItem, - MenuContent, - MenuPortal, - MenuTrigger, - Spacing, - Text -} from "@code0-tech/pictor"; -import Link from "next/link"; -import {useParams, useRouter} from "next/navigation"; -import {ProjectDataTableComponent} from "@edition/project/components/ProjectDataTableComponent"; -import {DataTableFilterProps, DataTableSortProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; -import {ButtonGroup} from "@code0-tech/pictor/dist/components/button-group/ButtonGroup"; -import {IconMinus, IconSortAscending, IconSortDescending} from "@tabler/icons-react"; -import {ProjectDataTableFilterInputComponent} from "@edition/project/components/ProjectDataTableFilterInputComponent"; - -//TODO: user abilities for project creation within namespace -export const ProjectsView: React.FC = () => { - - const params = useParams() - const router = useRouter() - const namespaceId = params.namespaceId as any as number - - const [filter, setFilter] = React.useState({}) - const [sort, setSort] = React.useState({}) - - const projectsList = React.useMemo(() => { - return { - const number = project?.id?.match(/NamespaceProject\/(\d+)$/)?.[1] - router.push(`/namespace/${namespaceId}/project/${number}/flow`) - }}/> - }, [namespaceId, filter, sort]) - - return <> - - -
    - - Projects - - - - Manage projects that you belong to.
    You can create new projects and switch between them. -
    -
    - - - - - - - - - - - { - if (sort["name"] === undefined) - setSort(prev => ({...prev, name: "asc"})) - else if (sort["name"] === "asc") - setSort(prev => ({...prev, name: "desc"})) - else - setSort(prev => ({...prev, name: undefined})) - - event.preventDefault() - event.stopPropagation() - }}> - {sort["name"] === undefined ? : sort["name"] === "asc" ? - : } - Name - - { - if (sort["createdAt"] === undefined) - setSort(prev => ({...prev, createdAt: "asc"})) - else if (sort["createdAt"] === "asc") - setSort(prev => ({...prev, createdAt: "desc"})) - else - setSort(prev => ({...prev, createdAt: undefined})) - - event.preventDefault() - event.stopPropagation() - }}> - {sort["createdAt"] === undefined ? - : sort["createdAt"] === "asc" ? - : } - Created At - - { - if (sort["updatedAt"] === undefined) - setSort(prev => ({...prev, updatedAt: "asc"})) - else if (sort["updatedAt"] === "asc") - setSort(prev => ({...prev, updatedAt: "desc"})) - else - setSort(prev => ({...prev, updatedAt: undefined})) - - event.preventDefault() - event.stopPropagation() - }}> - {sort["updatedAt"] === undefined ? - : sort["updatedAt"] === "asc" ? - : } - Updated At - - - - - -
    - -
    - setFilter(filter)}/> -
    - - {projectsList} - - -} \ No newline at end of file diff --git a/src/packages/ce/src/role/components/RoleCreateDialogComponent.tsx b/src/packages/ce/src/role/components/RoleCreateDialogComponent.tsx new file mode 100644 index 00000000..79d7088b --- /dev/null +++ b/src/packages/ce/src/role/components/RoleCreateDialogComponent.tsx @@ -0,0 +1,70 @@ +"use client" + +import React, {startTransition} from "react"; +import {Button, Spacing, TextInput, toast, useForm, useService} from "@code0-tech/pictor"; +import {Namespace} from "@code0-tech/sagittarius-graphql-types"; +import {RoleService} from "@edition/role/services/Role.service"; +import {InputDialog} from "@core/components/InputDialog"; + +export interface RoleCreateDialogComponentProps { + open?: boolean + onOpenChange?: (open: boolean) => void + namespaceId?: Namespace['id'] +} + +export const RoleCreateDialogComponent: React.FC = ({open, onOpenChange, namespaceId}) => { + + //TODO: user abilities for add role within namespace + + const roleService = useService(RoleService) + + const initialValues = React.useMemo(() => ({name: ""}), []) + + const [inputs, validate] = useForm({ + useInitialValidation: false, + initialValues, + validate: { + name: (value) => { + if (!value) return "Name is required" + if (value.length < 3) return "Name needs to be at least 3 characters" + if (value.length > 50) return "Name needs to be less than 50 characters" + return null + }, + }, + onSubmit: (values) => { + startTransition(() => { + if (!namespaceId) { + toast({ + title: "The current user does not have a personal namespace.", + color: "error", + }) + return + } + roleService.roleCreate({ + name: values.name as unknown as string, + namespaceId: namespaceId + }).then(payload => { + if ((payload?.errors?.length ?? 0) <= 0) { + toast({title: "Created role", color: "success"}) + onOpenChange?.(false) + } + }) + }) + } + }) + + return onOpenChange?.(open)}> + + + + +} diff --git a/src/packages/ce/src/role/components/RoleDataTableFilterInputComponent.tsx b/src/packages/ce/src/role/components/RoleDataTableFilterInputComponent.tsx deleted file mode 100644 index 353a20c8..00000000 --- a/src/packages/ce/src/role/components/RoleDataTableFilterInputComponent.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React from "react"; -import { - DataTableFilterInput, - DataTableFilterSuggestionMenu, - MenuCheckboxItem, - useService, - useStore -} from "@code0-tech/pictor"; -import {IconCheck} from "@tabler/icons-react"; -import {Namespace} from "@code0-tech/sagittarius-graphql-types"; -import {DataTableFilterProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; -import {RoleService} from "@edition/role/services/Role.service"; - -export interface RoleDataTableFilterInputComponentProps { - namespaceId: Namespace['id'] - onChange: (filter: DataTableFilterProps) => void -} - -export const RoleDataTableFilterInputComponent: React.FC = (props) => { - - const {namespaceId, onChange} = props - - const roleService = useService(RoleService) - const roleStore = useStore(RoleService) - - const roles = React.useMemo( - () => roleService.values({namespaceId: namespaceId}), - [roleStore, namespaceId] - ) - - const abilities = React.useMemo(() => { - const all = new Set() - roles.forEach(role => role.abilities?.forEach(ability => all.add(ability))) - return Array.from(all) - }, [roles]) - - return { - - const split = currentValue.split(",").map(s => s.trim()).filter(Boolean) - - return - {roles.map(role => { - - const isChecked = split.includes(role?.name!) - - return { - e.preventDefault(); - e.stopPropagation(); - const updated = isChecked - ? split.filter(name => name !== role?.name) - : [...split, role?.name]; - applySuggestion(updated.join(","), true); - }}> - {isChecked ? : } - {role?.name} - - })} - - } - }, { - token: "Permissions", - key: "abilities", - operators: ["isOneOf"], - suggestion: (context, operator, currentValue, applySuggestion) => { - - const split = currentValue.split(",").map(s => s.trim()).filter(Boolean) - - return - {abilities.map(ability => { - - const isChecked = split.includes(ability) - - return { - e.preventDefault(); - e.stopPropagation(); - const updated = isChecked - ? split.filter(name => name !== ability) - : [...split, ability]; - applySuggestion(updated.join(","), true); - }}> - {isChecked ? : } - {ability} - - })} - - } - } - ]}/> -} \ No newline at end of file diff --git a/src/packages/ce/src/role/components/RoleSettingsDialogComponent.tsx b/src/packages/ce/src/role/components/RoleSettingsDialogComponent.tsx new file mode 100644 index 00000000..555e595f --- /dev/null +++ b/src/packages/ce/src/role/components/RoleSettingsDialogComponent.tsx @@ -0,0 +1,59 @@ +"use client" + +import React from "react"; +import {Button, Text} from "@code0-tech/pictor"; +import {TabList, TabTrigger} from "@code0-tech/pictor/dist/components/tab/Tab"; +import {IconFolder, IconSettings2, IconShieldLock, IconTrash} from "@tabler/icons-react"; +import {RoleGeneralAdjustmentView} from "@edition/role/views/RoleGeneralAdjustmentView"; +import {RolePermissionView} from "@edition/role/views/RolePermissionView"; +import {RoleProjectView} from "@edition/role/views/RoleProjectView"; +import {RoleDeleteView} from "@edition/role/views/RoleDeleteView"; +import {SettingDialog} from "@core/components/SettingDialog"; + +export interface RoleSettingsDialogComponentProps { + open?: boolean + onOpenChange?: (open: boolean) => void +} + +export const RoleSettingsDialogComponent: React.FC = (props) => { + + const {open, onOpenChange} = props + + //TODO: limit tabs based on user abilities for roles + + return onOpenChange?.(open)} + title={"Role settings"} + description={"General settings and permissions for this role. These settings affect every member the role is assigned to."} + trigger={ + + + + + + + + + + + + + }> + + + + + +} diff --git a/src/packages/ce/src/role/pages/RoleCreatePage.tsx b/src/packages/ce/src/role/pages/RoleCreatePage.tsx index 8031713a..b2757529 100644 --- a/src/packages/ce/src/role/pages/RoleCreatePage.tsx +++ b/src/packages/ce/src/role/pages/RoleCreatePage.tsx @@ -1,95 +1,32 @@ "use client" import React from "react"; -import {Button, Col, Flex, Spacing, Text, TextInput, toast, useForm, useService} from "@code0-tech/pictor"; -import Link from "next/link"; import {useParams, useRouter} from "next/navigation"; import {Namespace} from "@code0-tech/sagittarius-graphql-types"; -import {RoleService} from "@edition/role/services/Role.service"; +import {RoleCreateDialogComponent} from "@edition/role/components/RoleCreateDialogComponent"; export const RoleCreatePage: React.FC = () => { - //TODO: user abilities for add role within namespace - const params = useParams() - const roleService = useService(RoleService) - const [, startTransition] = React.useTransition() const router = useRouter() - const namespaceIndex = params?.namespaceId as string - const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex as unknown as number}` - - //TODO: user abilities for project creation within namespace - - const [inputs, validate] = useForm({ - useInitialValidation: false, - initialValues: { - name: "" - }, - validate: { - name: (value) => { - if (!value) return "Name is required" - if (value.length < 3) return "Name needs to be at least 3 characters" - if (value.length > 50) return "Name needs to be less than 50 characters" - return null - }, - }, - onSubmit: (values) => { - startTransition(() => { - if (!namespaceId) { - toast({ - title: "The current user does not have a personal namespace.", - color: "error", - dismissible: true, - }) - return - } - roleService.roleCreate({ - name: values.name as unknown as string, - namespaceId: namespaceId - }).then(payload => { - if ((payload?.errors?.length ?? 0) <= 0) { - router.push(`/namespace/${namespaceIndex}/roles`) - } - }) - }) - } - }) - - return
    - - - - Create new role - - - - Global runtimes are shared runtimes that can be used across multiple organizations. - - - - - - - - - - - - -
    -} \ No newline at end of file + const namespaceIndex = params.namespaceId as any as number + const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` + const settingsHref = `/namespace/${namespaceIndex}/settings` + + return { + if (open) return + + const nav = (window as unknown as { navigation?: { entries(): { url: string }[], currentEntry?: { index: number } } }).navigation + if (!nav?.entries) { + router.back() + return + } + + const index = nav.currentEntry?.index ?? 0 + if (index > 0) router.back() + else router.push(settingsHref) + }}/> +} diff --git a/src/packages/ce/src/role/pages/RoleSettingsPage.tsx b/src/packages/ce/src/role/pages/RoleSettingsPage.tsx index 8a66870b..3c96e149 100644 --- a/src/packages/ce/src/role/pages/RoleSettingsPage.tsx +++ b/src/packages/ce/src/role/pages/RoleSettingsPage.tsx @@ -1,68 +1,29 @@ "use client" import React from "react"; -import {Button, ScrollArea, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport, Text} from "@code0-tech/pictor"; -import {Tab, TabList, TabTrigger} from "@code0-tech/pictor/dist/components/tab/Tab"; -import {RoleProjectView} from "@edition/role/views/RoleProjectView"; -import {RolePermissionView} from "@edition/role/views/RolePermissionView"; -import {RoleGeneralAdjustmentView} from "@edition/role/views/RoleGeneralAdjustmentView"; -import {RoleDeleteView} from "@edition/role/views/RoleDeleteView"; -import { - ResizableHandle, - ResizablePanel, - ResizablePanelGroup -} from "@code0-tech/pictor/dist/components/resizable/Resizable"; -import {SidebarComponent} from "@core/components/SidebarComponent"; +import {useParams, useRouter} from "next/navigation"; +import {RoleSettingsDialogComponent} from "@edition/role/components/RoleSettingsDialogComponent"; export const RoleSettingsPage: React.FC = () => { - //TODO: limit tabs based on user abilities for roles + const params = useParams() + const router = useRouter() - return <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -} \ No newline at end of file + const namespaceIndex = params.namespaceId as any as number + const settingsHref = `/namespace/${namespaceIndex}/settings` + + return { + if (open) return + + const nav = (window as unknown as { navigation?: { entries(): { url: string }[], currentEntry?: { index: number } } }).navigation + if (!nav?.entries) { + router.back() + return + } + + const index = nav.currentEntry?.index ?? 0 + if (index > 0) router.back() + else router.push(settingsHref) + }}/> +} diff --git a/src/packages/ce/src/role/pages/RolesPage.tsx b/src/packages/ce/src/role/pages/RolesPage.tsx deleted file mode 100644 index 4e110b45..00000000 --- a/src/packages/ce/src/role/pages/RolesPage.tsx +++ /dev/null @@ -1,8 +0,0 @@ -"use client" - -import React from "react"; -import {RolesView} from "@edition/role/views/RolesView"; - -export const RolesPage: React.FC = () => { - return -} \ No newline at end of file diff --git a/src/packages/ce/src/role/views/RoleDeleteView.tsx b/src/packages/ce/src/role/views/RoleDeleteView.tsx index 85d8db88..d6fdece6 100644 --- a/src/packages/ce/src/role/views/RoleDeleteView.tsx +++ b/src/packages/ce/src/role/views/RoleDeleteView.tsx @@ -2,12 +2,12 @@ import React from "react"; import {useParams, useRouter} from "next/navigation"; -import {Button, Card, Flex, Spacing, Text, useService} from "@code0-tech/pictor"; +import {Button, DialogClose, Spacing, Text, useService} from "@code0-tech/pictor"; import {RoleService} from "@edition/role/services/Role.service"; import {MemberService} from "@edition/member/services/Member.service"; import type {NamespaceRole} from "@code0-tech/sagittarius-graphql-types"; import {TabContent} from "@code0-tech/pictor/dist/components/tab/Tab"; -import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {toast} from "@code0-tech/pictor/dist/components/toast/Toast"; export const RoleDeleteView: React.FC = () => { @@ -28,32 +28,23 @@ export const RoleDeleteView: React.FC = () => { }).then(payload => { if ((payload?.errors?.length ?? 0) <= 0) { memberService.removeRoleFromMembers(roleId) - addIslandSuccessNotification({ - message: "Deleted role" - }) + toast({title: "Deleted role", color: "success"}) } - router.push(`/namespace/${namespaceIndex}/roles`) + router.push(`/namespace/${namespaceIndex}/settings`) }) }) }, []) return - - Delete role + Delete role + + + Permanently delete this role and unassign it from all members. This action cannot be undone. - - - - - - This will delete the role and cannot be undone. - - - - - + + + + } \ No newline at end of file diff --git a/src/packages/ce/src/role/views/RoleGeneralAdjustmentView.tsx b/src/packages/ce/src/role/views/RoleGeneralAdjustmentView.tsx index 6f2d6065..8b2b6162 100644 --- a/src/packages/ce/src/role/views/RoleGeneralAdjustmentView.tsx +++ b/src/packages/ce/src/role/views/RoleGeneralAdjustmentView.tsx @@ -1,13 +1,12 @@ "use client" import React from "react"; -import {Button, Card, Flex, Spacing, Text, TextInput, useForm, useService, useStore} from "@code0-tech/pictor"; -import CardSection from "@code0-tech/pictor/dist/components/card/CardSection"; +import {Button, Flex, Spacing, Text, TextInput, useForm, useService, useStore} from "@code0-tech/pictor"; import {TabContent} from "@code0-tech/pictor/dist/components/tab/Tab"; import {RoleService} from "@edition/role/services/Role.service"; import type {Namespace, NamespaceRole} from "@code0-tech/sagittarius-graphql-types"; import {useParams} from "next/navigation"; -import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {toast} from "@code0-tech/pictor/dist/components/toast/Toast"; export const RoleGeneralAdjustmentView: React.FC = () => { @@ -50,9 +49,7 @@ export const RoleGeneralAdjustmentView: React.FC = () => { name: values.name! }).then(payload => { if ((payload?.errors?.length ?? 0) <= 0) { - addIslandSuccessNotification({ - message: "Updated role name" - }) + toast({title: "Updated role name", color: "success"}) } }) }) @@ -61,21 +58,17 @@ export const RoleGeneralAdjustmentView: React.FC = () => { return - - General - - + General + - - - - - Name - - - - + + + The name and general settings for this role, visible to every member it is assigned to. + + + } \ No newline at end of file diff --git a/src/packages/ce/src/role/views/RolePermissionView.tsx b/src/packages/ce/src/role/views/RolePermissionView.tsx index e6bceef4..24426f71 100644 --- a/src/packages/ce/src/role/views/RolePermissionView.tsx +++ b/src/packages/ce/src/role/views/RolePermissionView.tsx @@ -21,7 +21,8 @@ import {TabContent} from "@code0-tech/pictor/dist/components/tab/Tab"; import {useParams} from "next/navigation"; import {RoleService} from "@edition/role/services/Role.service"; import {RolePermissionComponent} from "@edition/role/components/RolePermissionComponent"; -import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {toast} from "@code0-tech/pictor/dist/components/toast/Toast"; +import BorderBeam from "border-beam"; type Permission = { label: string @@ -219,9 +220,7 @@ export const RolePermissionView: React.FC = () => { abilities: updatedAbilities }).then(payload => { if ((payload?.errors?.length ?? 0) <= 0) { - addIslandSuccessNotification({ - message: "Updated role permissions" - }) + toast({title: "Updated role permissions", color: "success"}) } }) }) @@ -230,14 +229,16 @@ export const RolePermissionView: React.FC = () => { return - - Select from role templates - - + Permissions + - + + + Assign permissions to this role. Members with this role inherit every permission you enable here. + + + Role templates + {React.useMemo(() => { return {permissionTemplates.map(permissionTemplate => { @@ -253,32 +254,30 @@ export const RolePermissionView: React.FC = () => { const isSelected = templateAbilities.length === roleAbilities.length && templateAbilities.every(a => new Set(roleAbilities).has(a)); + const card = + + {permissionTemplate.name} + + + enabled) + .map(([ability, _]) => ability as NamespaceRoleAbility)}/> + + + - return - - - {permissionTemplate.name} - - - enabled) - .map(([ability, _]) => ability as NamespaceRoleAbility)}/> - - - {isSelected ? : null} - + return + {isSelected ? {card} : card} })} }, [initialValues, role])} - - Current stored permissions - + Current stored permissions diff --git a/src/packages/ce/src/role/views/RoleProjectView.tsx b/src/packages/ce/src/role/views/RoleProjectView.tsx index 6fe05ea1..cdcf2d8d 100644 --- a/src/packages/ce/src/role/views/RoleProjectView.tsx +++ b/src/packages/ce/src/role/views/RoleProjectView.tsx @@ -1,21 +1,52 @@ "use client" -import {Alert, Button, DataTableColumn, Flex, Spacing, Text, useService, useStore} from "@code0-tech/pictor"; +import { + Alert, + Badge, + Button, + ButtonGroup, + DataTableColumn, + Flex, + Menu, + MenuCheckboxItem, + MenuContent, + MenuItem, + MenuLabel, + MenuPortal, + MenuTrigger, + Spacing, + Text, + useService, + useStore +} from "@code0-tech/pictor"; +import {DataTableSortProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; import {TabContent} from "@code0-tech/pictor/dist/components/tab/Tab"; import React from "react"; import {useParams} from "next/navigation"; import {RoleService} from "@edition/role/services/Role.service"; +import {ProjectService} from "@edition/project/services/Project.service"; import type {Namespace, NamespaceProject, NamespaceRole, Scalars} from "@code0-tech/sagittarius-graphql-types"; import {ProjectDataTableComponent} from "@edition/project/components/ProjectDataTableComponent"; import {ProjectMenuComponent} from "@edition/project/components/ProjectMenuComponent"; -import {IconTrash} from "@tabler/icons-react"; -import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {IconAdjustmentsHorizontal, IconArrowsSort, IconCheck, IconPlus, IconTrash} from "@tabler/icons-react"; +import {toast} from "@code0-tech/pictor/dist/components/toast/Toast"; + +const sortLabels = { + name: "Name", + createdAt: "Created", + updatedAt: "Updated", +} as const + +const toggle = (value: string, setter: React.Dispatch>) => + setter(prev => prev.includes(value) ? prev.filter(v => v !== value) : [...prev, value]) export const RoleProjectView: React.FC = () => { const params = useParams() const roleService = useService(RoleService) const roleStore = useStore(RoleService) + const projectService = useService(ProjectService) + const projectStore = useStore(ProjectService) const [, startTransition] = React.useTransition() const namespaceIndex = params.namespaceId as any as number @@ -31,6 +62,18 @@ export const RoleProjectView: React.FC = () => { setAssignedProjectIds(roleAssignedProjects) }, [role]) + const projects = React.useMemo(() => projectService.values({namespaceId: namespaceId}), [projectStore, namespaceId]) + const assignedProjects = React.useMemo( + () => projects.filter(p => assignedProjectIds.find(id => id == p?.id)), + [projects, assignedProjectIds] + ) + + const [sort, setSort] = React.useState("name") + const [nameFilter, setNameFilter] = React.useState([]) + + const sortProps: DataTableSortProps = {[sort]: "asc"} + const hasFilter = nameFilter.length > 0 + const assignProjects = () => { startTransition(() => { roleService.roleAssignProject({ @@ -38,9 +81,7 @@ export const RoleProjectView: React.FC = () => { projectIds: assignedProjectIds as Scalars['NamespaceProjectID']['output'][] }).then(payload => { if ((payload?.errors?.length ?? 0) <= 0) { - addIslandSuccessNotification({ - message: "Assigned project" - }) + toast({title: "Assigned project", color: "success"}) } }) }) @@ -63,26 +104,89 @@ export const RoleProjectView: React.FC = () => { }, [assignedProjectIds]) const filterAssignedProjects = React.useCallback((project: NamespaceProject) => { - return !!assignedProjectIds.find(projectId => projectId == project.id!!) - }, [assignedProjectIds]) + if (!assignedProjectIds.find(projectId => projectId == project.id!!)) return false + if (nameFilter.length > 0 && !nameFilter.includes(project?.name!)) return false + return true + }, [assignedProjectIds, nameFilter]) + + const visibleCount = React.useMemo( + () => assignedProjects.filter(p => nameFilter.length === 0 || nameFilter.includes(p?.name!)).length, + [assignedProjects, nameFilter] + ) return - - Projects that members can access - - - + + Assigned projects + {visibleCount} + + + {/* Filter */} + + + + + + + Name + {assignedProjects.map(project => ( + { + e.preventDefault() + e.stopPropagation() + toggle(project?.name!, setNameFilter) + }}> + + {project?.name} + + ))} + + + + + {/* Sort */} + + + + + + + Sort by + {(Object.keys(sortLabels) as (keyof typeof sortLabels)[]).map(k => ( + setSort(k)}> + + {sortLabels[k]} + + ))} + + + + + {/* Add */} addAssignedProject(project.id!!)}> - + - - - + {/* Save */} + + +
    + + + Limit the projects that members with this role can access. + + {(assignedProjectIds.length ?? 0) <= 0 ? ( @@ -91,18 +195,17 @@ export const RoleProjectView: React.FC = () => { ) : ( - <> - { - return [ - - - - ] - }} namespaceId={namespaceId} preFilter={filterAssignedProjects}/> - + { + return [ + + + + ] + }} namespaceId={namespaceId} preFilter={filterAssignedProjects}/> )}
    -} \ No newline at end of file +} diff --git a/src/packages/ce/src/role/views/RolesView.tsx b/src/packages/ce/src/role/views/RolesView.tsx index d06b0738..d54041a7 100644 --- a/src/packages/ce/src/role/views/RolesView.tsx +++ b/src/packages/ce/src/role/views/RolesView.tsx @@ -3,127 +3,164 @@ import React from "react"; import {useParams, useRouter} from "next/navigation"; import { + Badge, Button, + ButtonGroup, Flex, Menu, MenuCheckboxItem, MenuContent, + MenuItem, + MenuLabel, MenuPortal, MenuTrigger, Spacing, - Text + Text, + useService, + useStore } from "@code0-tech/pictor"; -import {IconMinus, IconSortAscending, IconSortDescending} from "@tabler/icons-react"; +import {DataTableSortProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; +import {IconAdjustmentsHorizontal, IconArrowsSort, IconCheck, IconPlus} from "@tabler/icons-react"; import Link from "next/link"; +import {Namespace, NamespaceRole} from "@code0-tech/sagittarius-graphql-types"; +import {RoleService} from "@edition/role/services/Role.service"; import {RoleDataTableComponent} from "@edition/role/components/RoleDataTableComponent"; -import {ButtonGroup} from "@code0-tech/pictor/dist/components/button-group/ButtonGroup"; -import {DataTableFilterProps, DataTableSortProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; -import {RoleDataTableFilterInputComponent} from "@edition/role/components/RoleDataTableFilterInputComponent"; + +const sortLabels = { + name: "Name", + createdAt: "Created", + updatedAt: "Updated", +} as const + +const toggle = (value: string, setter: React.Dispatch>) => + setter(prev => prev.includes(value) ? prev.filter(v => v !== value) : [...prev, value]) //TODO: user abilities for add role within namespace export const RolesView: React.FC = () => { const params = useParams() const router = useRouter() - const namespaceId = params.namespaceId as any as number + const roleService = useService(RoleService) + const roleStore = useStore(RoleService) - const [filter, setFilter] = React.useState({}) - const [sort, setSort] = React.useState({}) + const namespaceIndex = params.namespaceId as any as number + const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` - const rolesList = React.useMemo(() => { - return { - const roleIndex = role?.id?.match(/NamespaceRole\/(\d+)$/)?.[1] - router.push(`/namespace/${namespaceId}/roles/${roleIndex}/settings`) - }} namespaceId={`gid://sagittarius/Namespace/${namespaceId}`}/> - }, [namespaceId, filter, sort]) + const roles = React.useMemo( + () => roleService.values({namespaceId: namespaceId}), + [roleStore, namespaceId] + ) - return <> + const abilities = React.useMemo(() => { + const all = new Set() + roles.forEach(role => role.abilities?.forEach(ability => all.add(ability))) + return Array.from(all) + }, [roles]) + + const [sort, setSort] = React.useState("name") + const [nameFilter, setNameFilter] = React.useState([]) + const [abilityFilter, setAbilityFilter] = React.useState([]) + + const preFilter = React.useCallback((role: NamespaceRole) => { + if (nameFilter.length > 0 && !nameFilter.includes(role?.name!)) return false + if (abilityFilter.length > 0 && !role?.abilities?.some(a => abilityFilter.includes(a))) return false + return true + }, [nameFilter, abilityFilter]) + + const visibleCount = React.useMemo( + () => roles.map(r => r.json()).filter(preFilter).length, + [roles, preFilter] + ) - - - - Roles - - - Manage roles that you belong to. You can create new roles and switch between them. - + const hasFilter = nameFilter.length > 0 || abilityFilter.length > 0 + + const sortProps: DataTableSortProps = {[sort]: "asc"} + + return <> + + + Roles + {visibleCount} - - - + {/* Filter */} + + + + + + + Name + {roles.map(role => ( + { + e.preventDefault() + e.stopPropagation() + toggle(role?.name!, setNameFilter) + }}> + + {role?.name} + + ))} + Permissions + {abilities.map(ability => ( + { + e.preventDefault() + e.stopPropagation() + toggle(ability, setAbilityFilter) + }}> + + {ability} + + ))} + + + + + {/* Sort */} - + - - { - if (sort["name"] === undefined) - setSort(prev => ({...prev, name: "asc"})) - else if (sort["name"] === "asc") - setSort(prev => ({...prev, name: "desc"})) - else - setSort(prev => ({...prev, name: undefined})) - - event.preventDefault() - event.stopPropagation() - }}> - {sort["name"] === undefined ? : sort["name"] === "asc" ? - : } - Name - - { - if (sort["createdAt"] === undefined) - setSort(prev => ({...prev, createdAt: "asc"})) - else if (sort["createdAt"] === "asc") - setSort(prev => ({...prev, createdAt: "desc"})) - else - setSort(prev => ({...prev, createdAt: undefined})) - - event.preventDefault() - event.stopPropagation() - }}> - {sort["createdAt"] === undefined ? - : sort["createdAt"] === "asc" ? - : } - Created At - - { - if (sort["updatedAt"] === undefined) - setSort(prev => ({...prev, updatedAt: "asc"})) - else if (sort["updatedAt"] === "asc") - setSort(prev => ({...prev, updatedAt: "desc"})) - else - setSort(prev => ({...prev, updatedAt: undefined})) - - event.preventDefault() - event.stopPropagation() - }}> - {sort["updatedAt"] === undefined ? - : sort["updatedAt"] === "asc" ? - : } - Updated At - + + Sort by + {(Object.keys(sortLabels) as (keyof typeof sortLabels)[]).map(k => ( + setSort(k)}> + + {sortLabels[k]} + + ))} + + {/* Create */} + + + - -
    - setFilter(filter)}/> -
    - - {rolesList} - + + + Manage roles that you belong to. You can create new roles and switch between them. + + + { + const roleIndex = role?.id?.match(/NamespaceRole\/(\d+)$/)?.[1] + router.push(`/namespace/${namespaceIndex}/roles/${roleIndex}/settings`) + }}/> - -} \ No newline at end of file +} diff --git a/src/packages/ce/src/runtime/components/RuntimeCreateDialogComponent.tsx b/src/packages/ce/src/runtime/components/RuntimeCreateDialogComponent.tsx new file mode 100644 index 00000000..b8dc34d5 --- /dev/null +++ b/src/packages/ce/src/runtime/components/RuntimeCreateDialogComponent.tsx @@ -0,0 +1,112 @@ +"use client" + +import React, {startTransition} from "react"; +import { + Button, + Spacing, + TextInput, + toast, + useForm, + useService +} from "@code0-tech/pictor"; +import {RuntimeService} from "@edition/runtime/services/Runtime.service"; +import {Namespace} from "@code0-tech/sagittarius-graphql-types"; +import {InputDialog} from "@core/components/InputDialog"; + +export interface RuntimeCreateDialogComponentProps { + open?: boolean + onOpenChange?: (open: boolean) => void + namespaceId?: Namespace['id'] +} + +export const RuntimeCreateDialogComponent: React.FC = ({open, onOpenChange, namespaceId}) => { + + const runtimeService = useService(RuntimeService) + const [token, setToken] = React.useState(undefined) + + const initialValues = React.useMemo( + () => ({ + name: "", + description: "", + }), + [] + ) + + const [inputs, validate] = useForm({ + useInitialValidation: false, + initialValues, + validate: { + name: (value) => { + if (!value) return "Name is required" + if (value.length < 3) return "Name needs to be at least 3 characters" + if (value.length > 50) return "Name needs to be less than 50 characters" + return null + }, + description: (value) => { + if (!value) return "Description is required" + if ((value as string).length > 50) return "Description needs to be less than 50 characters" + return null + } + }, + onSubmit: (values) => { + startTransition(() => { + runtimeService.runtimeCreate({ + name: values.name as unknown as string, + description: values.description as unknown as string, + ...(namespaceId ? {namespaceId} : {}) + }).then(payload => { + if ((payload?.errors?.length ?? 0) <= 0) { + if (payload?.runtime?.token) { + setToken(payload.runtime.token) + toast({title: "Created runtime", color: "success"}) + } else { + toast({ + title: "The runtime was created but no token was provided.", + color: "error", + }) + onOpenChange?.(false) + } + } + }) + }) + } + }) + + return onOpenChange?.(open)}> + {!token ? ( + <> + + + + + ) : ( + + )} + + {!token ? ( + + ) : ( + + )} + +} diff --git a/src/packages/ce/src/runtime/components/RuntimeDataTableFilterInputComponent.tsx b/src/packages/ce/src/runtime/components/RuntimeDataTableFilterInputComponent.tsx deleted file mode 100644 index e41cdbda..00000000 --- a/src/packages/ce/src/runtime/components/RuntimeDataTableFilterInputComponent.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React from "react"; -import { - DataTableFilterInput, - DataTableFilterSuggestionMenu, - MenuCheckboxItem, - useService, - useStore -} from "@code0-tech/pictor"; -import {IconCheck} from "@tabler/icons-react"; -import {Namespace, Runtime} from "@code0-tech/sagittarius-graphql-types"; -import {DataTableFilterProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; -import {RuntimeService} from "@edition/runtime/services/Runtime.service"; - -export interface RoleDataTableFilterInputComponentProps { - namespaceId?: Namespace['id'] - onChange: (filter: DataTableFilterProps) => void - preFilter?: (project: Runtime, index: number) => boolean -} - -export const RuntimeDataTableFilterInputComponent: React.FC = (props) => { - - const {namespaceId, onChange, preFilter = () => true} = props - - const runtimeService = useService(RuntimeService) - const runtimeStore = useStore(RuntimeService) - - const runtimes = React.useMemo( - () => runtimeService.values({namespaceId}).filter(preFilter), - [runtimeStore, namespaceId] - ) - - return { - - const split = currentValue.split(",").map(s => s.trim()).filter(Boolean) - - return - {runtimes.map(runtime => { - - const isChecked = split.includes(runtime?.name!) - - return { - e.preventDefault(); - e.stopPropagation(); - const updated = isChecked - ? split.filter(name => name !== runtime?.name) - : [...split, runtime?.name]; - applySuggestion(updated.join(","), true); - }}> - {isChecked ? : } - {runtime?.name} - - })} - - } - }, { - token: "Status", - key: "status", - operators: ["isOneOf"], - suggestion: (context, operator, currentValue, applySuggestion) => { - - const split = currentValue.split(",").map(s => s.trim()).filter(Boolean) - - return - {["CONNECTED", "DISCONNECTED"].map(status => { - - const isChecked = split.includes(status) - - return { - e.preventDefault(); - e.stopPropagation(); - const updated = isChecked - ? split.filter(name => name !== status) - : [...split, status]; - applySuggestion(updated.join(","), true); - }}> - {isChecked ? : } - {status} - - })} - - } - } - ]}/> -} \ No newline at end of file diff --git a/src/packages/ce/src/runtime/components/RuntimeSettingsDialogComponent.tsx b/src/packages/ce/src/runtime/components/RuntimeSettingsDialogComponent.tsx new file mode 100644 index 00000000..c3cd04a7 --- /dev/null +++ b/src/packages/ce/src/runtime/components/RuntimeSettingsDialogComponent.tsx @@ -0,0 +1,175 @@ +"use client" + +import React from "react"; +import {Button, DialogClose, Flex, Spacing, Text, TextInput, useForm, useService, useStore} from "@code0-tech/pictor"; +import {TabContent, TabList, TabTrigger} from "@code0-tech/pictor/dist/components/tab/Tab"; +import {Runtime} from "@code0-tech/sagittarius-graphql-types"; +import {RuntimeService} from "@edition/runtime/services/Runtime.service"; +import {useRouter} from "next/navigation"; +import {toast} from "@code0-tech/pictor/dist/components/toast/Toast"; +import {IconBackground, IconPlugConnected, IconTrash} from "@tabler/icons-react"; +import {SettingDialog} from "@core/components/SettingDialog"; + +export interface RuntimeSettingsDialogComponentProps { + runtimeId?: Runtime['id'] + namespaceId?: number + open?: boolean + onOpenChange?: (open: boolean) => void +} + +export const RuntimeSettingsDialogComponent: React.FC = (props) => { + + const {runtimeId, namespaceId, open, onOpenChange} = props + + const runtimeService = useService(RuntimeService) + const runtimeStore = useStore(RuntimeService) + const [, startTransition] = React.useTransition() + const [token, setToken] = React.useState(undefined) + const router = useRouter() + + const runtime = React.useMemo( + () => runtimeService.getById(runtimeId), + [runtimeStore, runtimeId] + ) + + const initialValues = React.useMemo(() => ({ + name: runtime?.name, + description: runtime?.description, + }), [runtime]) + + const [inputs, validate] = useForm({ + useInitialValidation: false, + initialValues: initialValues, + validate: { + name: (value) => { + if (!value) return "Name is required" + return null + }, + description: (value) => { + if (!value) return "Description is required" + return null + } + }, + onSubmit: (values) => { + if (!runtime) return + startTransition(() => { + runtimeService.runtimeUpdate({ + name: values.name as unknown as string, + description: values.description as unknown as string, + runtimeId: runtime.id! + }).then(payload => { + if ((payload?.errors?.length ?? 0) <= 0) { + toast({title: "Updated runtime", color: "success"}) + } + }) + }) + } + }) + + const deleteRuntime = React.useCallback(() => { + if (!runtime) return + startTransition(() => { + runtimeService.runtimeDelete({ + runtimeId: runtime.id! + }).then(payload => { + if ((payload?.errors?.length ?? 0) <= 0) { + toast({title: "Deleted runtime", color: "success"}) + } + onOpenChange?.(false) + router.push(namespaceId ? `/namespace/${namespaceId}/runtimes` : "/runtimes") + }) + }) + }, [runtime, namespaceId, onOpenChange, router]) + + const rotateToken = React.useCallback(() => { + if (!runtime) return + startTransition(() => { + runtimeService.runtimeRotateToken({ + runtimeId: runtime.id! + }).then(payload => { + if ((payload?.errors?.length ?? 0) <= 0) { + if (payload?.runtime?.token) { + setToken(payload.runtime.token) + toast({title: "Rotated runtime token", color: "success"}) + } + } + }) + }) + }, [runtime]) + + return onOpenChange?.(open)} + title={`Settings of ${runtime?.name ?? ""}`} + description={"General settings and restrictions for your runtime. These settings affect how the runtime connects to and behaves within your Sculptor application."} + trigger={ + + + + + + + + + + }> + + + General + + + + + General configuration for your runtime, including the name and description used to recognise it. + + + + + + + + How to connect + + + Connect your runtime to Sculptor using an access token. Keep it secret and rotate it when exposed. + + + {token ? ( + + ) : ( + + )} + + + Delete runtime + + + Permanently remove this runtime from your application. This action cannot be undone. + + + + + + + +} diff --git a/src/packages/ce/src/runtime/pages/RuntimeAssignPage.tsx b/src/packages/ce/src/runtime/pages/RuntimeAssignPage.tsx deleted file mode 100644 index 884e4c1c..00000000 --- a/src/packages/ce/src/runtime/pages/RuntimeAssignPage.tsx +++ /dev/null @@ -1,117 +0,0 @@ -"use client" - -import React, {startTransition} from "react"; -import {useParams} from "next/navigation"; -import { - Avatar, Badge, - Button, - Flex, hashToColor, - Menu, - MenuContent, - MenuItem, - MenuPortal, MenuSeparator, - MenuTrigger, Spacing, - Text, - useService, - useStore -} from "@code0-tech/pictor"; -import {RuntimeService} from "@edition/runtime/services/Runtime.service"; -import {ProjectService} from "@edition/project/services/Project.service"; -import {Runtime} from "@code0-tech/sagittarius-graphql-types"; -import {formatDistanceToNow} from "date-fns"; -import {RuntimeProjectDataTableComponent} from "@edition/runtime/components/RuntimeProjectDataTableComponent"; - -export const RuntimeAssignPage: React.FC = () => { - - const params = useParams() - const runtimeService = useService(RuntimeService) - const runtimeStore = useStore(RuntimeService) - const projectService = useService(ProjectService) - const projectStore = useStore(ProjectService) - - const projectId = params.projectId as any as number - const namespaceId = params.namespaceId as any as number - - const availableRuntimes = React.useMemo( - () => { - const globalRuntimes = runtimeService.values().filter(runtime => !runtime?.namespace?.id) - const namespaceRuntimes = runtimeService.values({namespaceId: `gid://sagittarius/Namespace/${namespaceId}`}) - return [...globalRuntimes, ...namespaceRuntimes] - }, - [runtimeStore, projectId] - ) - - const project = React.useMemo( - () => projectService.getById(`gid://sagittarius/NamespaceProject/${projectId}`), - [projectStore, projectId] - ) - - const assignRuntime = React.useCallback((runtimeId: Runtime['id']) => { - const runtimeIds = project?.runtimes?.nodes?.map(r => r?.id!) ?? [] - - if (!runtimeId) return - if (runtimeIds.includes(runtimeId)) return - - startTransition(() => { - projectService.projectAssignRuntimes({ - runtimeIds: [...runtimeIds, runtimeId], - namespaceProjectId: `gid://sagittarius/NamespaceProject/${projectId}` - }) - }) - }, [projectService, projectId, project]) - - return
    - - Assigned runtimes - - - - - - - {availableRuntimes.map((runtime, index) => { - return <> - assignRuntime(runtime.id)}> - - - - - - {runtime?.name} - - - - {runtime?.status} - - - - - - - Updated {formatDistanceToNow(runtime?.updatedAt!)} ago - - - - {index < availableRuntimes.length - 1 ? - : null} - - })} - - - - - - -
    -} \ No newline at end of file diff --git a/src/packages/ce/src/runtime/pages/RuntimeCreatePage.tsx b/src/packages/ce/src/runtime/pages/RuntimeCreatePage.tsx index 1e32e128..e91ea522 100644 --- a/src/packages/ce/src/runtime/pages/RuntimeCreatePage.tsx +++ b/src/packages/ce/src/runtime/pages/RuntimeCreatePage.tsx @@ -1,144 +1,45 @@ "use client" import React from "react"; -import { - Button, - Col, - Flex, - Spacing, - Text, - TextInput, - toast, - useForm, - useService, - useStore -} from "@code0-tech/pictor"; -import Link from "next/link"; +import {useService, useStore} from "@code0-tech/pictor"; import {notFound, useParams, useRouter} from "next/navigation"; -import {RuntimeService} from "@edition/runtime/services/Runtime.service"; +import {RuntimeCreateDialogComponent} from "@edition/runtime/components/RuntimeCreateDialogComponent"; import {UserService} from "@edition/user/services/User.service"; -import {NamespaceService} from "@edition/namespace/services/Namespace.service"; import {useUserSession} from "@edition/user/hooks/User.session.hook"; export const RuntimeCreatePage: React.FC = () => { const params = useParams() + const router = useRouter() const namespaceIndex = params.namespaceId as any as number - const runtimeService = useService(RuntimeService) - const [, startTransition] = React.useTransition() - const [token, setToken] = React.useState(undefined) - const router = useRouter() const currentSession = useUserSession() - const userStore = useStore(UserService) const userService = useService(UserService) - const namespaceService = useService(NamespaceService) - const namespaceStore = useStore(NamespaceService) - - const currentUser = React.useMemo(() => userService.getById(currentSession?.user?.id), [userStore, currentSession]) - const namespace = React.useMemo(() => namespaceService.getById(`gid://sagittarius/Namespace/${namespaceIndex}`), [namespaceStore, namespaceIndex]) + const userStore = useStore(UserService) + const currentUser = React.useMemo( + () => userService.getById(currentSession?.user?.id), + [userStore, currentSession] + ) if (!namespaceIndex && currentUser && !currentUser.admin) { notFound() } - //TODO: user abilities for runtime creation within namespace - // if (namespace.) + const runtimesHref = namespaceIndex ? `/namespace/${namespaceIndex}/runtimes` : "/runtimes" + + return { + if (open) return - const [inputs, validate] = useForm({ - useInitialValidation: false, - initialValues: { - name: "", - description: "", - }, - validate: { - name: (value) => { - if (!value) return "Name is required" - if (value.length < 3) return "Name needs to be at least 3 characters" - if (value.length > 50) return "Name needs to be less than 50 characters" - return null - }, - description: (value) => { - if (!value) return "Description is required" - if ((value as string).length > 50) return "Description needs to be less than 50 characters" - return null - } - }, - onSubmit: (values) => { - startTransition(() => { - runtimeService.runtimeCreate({ - name: values.name as unknown as string, - description: values.description as unknown as string, - ...(namespaceIndex ? {namespaceId: `gid://sagittarius/Namespace/${namespaceIndex}`} : {}) - }).then(payload => { - if ((payload?.errors?.length ?? 0) <= 0) { - if (payload?.runtime?.token) { - if (!token) setToken(payload.runtime.token) - } else { - toast({ - title: "The runtime was created but no token was provided.", - color: "error", - dismissible: true, - }) - router.push(namespaceIndex ? `/namespace/${namespaceIndex}/runtimes` : "/runtimes") - } - } - }) - }) - } - }) + const nav = (window as unknown as { navigation?: { entries(): { url: string }[], currentEntry?: { index: number } } }).navigation + if (!nav?.entries) { + router.back() + return + } - return
    - - - - {!token ? "Create new runtime" : "Runtime created successfully"} - - - - Runtimes are shared runtimes that can be used across multiple organizations. - - - {!token ? ( - <> - - - - - - ) : ( - <> - - - - )} - - - - - {!token ? ( - - ) : null} - - - -
    -} \ No newline at end of file + const index = nav.currentEntry?.index ?? 0 + if (index > 0) router.back() + else router.push(runtimesHref) + }}/> +} diff --git a/src/packages/ce/src/runtime/pages/RuntimeSettingsPage.tsx b/src/packages/ce/src/runtime/pages/RuntimeSettingsPage.tsx index ea3195b3..ffcb1533 100644 --- a/src/packages/ce/src/runtime/pages/RuntimeSettingsPage.tsx +++ b/src/packages/ce/src/runtime/pages/RuntimeSettingsPage.tsx @@ -1,202 +1,48 @@ "use client" import React from "react"; -import {Button, Card, Flex, Spacing, Text, TextInput, useForm, useService, useStore} from "@code0-tech/pictor"; -import {RuntimeService} from "@edition/runtime/services/Runtime.service"; +import {useService, useStore} from "@code0-tech/pictor"; import {notFound, useParams, useRouter} from "next/navigation"; -import {Tab, TabContent, TabList, TabTrigger} from "@code0-tech/pictor/dist/components/tab/Tab"; -import CardSection from "@code0-tech/pictor/dist/components/card/CardSection"; -import { - ResizableHandle, - ResizablePanel, - ResizablePanelGroup -} from "@code0-tech/pictor/dist/components/resizable/Resizable"; -import {SidebarComponent} from "@core/components/SidebarComponent"; -import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {Runtime} from "@code0-tech/sagittarius-graphql-types"; +import {RuntimeService} from "@edition/runtime/services/Runtime.service"; +import {RuntimeSettingsDialogComponent} from "@edition/runtime/components/RuntimeSettingsDialogComponent"; export const RuntimeSettingsPage: React.FC = () => { - //TODO: add ability check for runtime settings access for every settings tab - const params = useParams() - const namespaceId = params.namespaceId as any as number - const runtimeId = params.runtimeId as any as number + const router = useRouter() + const namespaceIndex = params.namespaceId as any as number + const runtimeIndex = params.runtimeId as any as number const runtimeService = useService(RuntimeService) const runtimeStore = useStore(RuntimeService) - const [, startTransition] = React.useTransition() - const [token, setToken] = React.useState(undefined) - const router = useRouter() - const runtime = React.useMemo(() => runtimeService.getById(`gid://sagittarius/Runtime/${runtimeId}`), [runtimeStore, params]) + const runtimeId = `gid://sagittarius/Runtime/${runtimeIndex}` as Runtime['id'] + const runtime = React.useMemo( + () => runtimeService.getById(runtimeId), + [runtimeStore, runtimeId] + ) - if (runtime?.userAbilities && (!runtime?.userAbilities?.updateRuntime || !runtime.userAbilities.deleteRuntime || !runtime.userAbilities.rotateRuntimeToken)) { + if (runtime?.userAbilities && (!runtime.userAbilities.updateRuntime || !runtime.userAbilities.deleteRuntime || !runtime.userAbilities.rotateRuntimeToken)) { notFound() } - const initialValues = React.useMemo(() => ({ - name: runtime?.name, - description: runtime?.description, - }), [runtime]) - - const [inputs, validate] = useForm({ - useInitialValidation: false, - initialValues: initialValues, - validate: { - name: (value) => { - if (!value) return "Name is required" - return null - }, - description: (value) => { - if (!value) return "Description is required" - return null - } - }, - onSubmit: (values) => { - startTransition(() => { - runtimeService.runtimeUpdate({ - name: values.name as unknown as string, - description: values.description as unknown as string, - runtimeId: runtime?.id!! - }).then(payload => { - if ((payload?.errors?.length ?? 0) <= 0) { - addIslandSuccessNotification({ - message: "Updated runtime" - }) - } - }) - }) - } - }) - - const deleteRuntime = React.useCallback(() => { - startTransition(() => { - runtimeService.runtimeDelete({ - runtimeId: runtime?.id!! - }).then(payload => { - if ((payload?.errors?.length ?? 0) <= 0) { - addIslandSuccessNotification({ - message: "Deleted runtime" - }) - } - router.push(namespaceId ? `/namespace/${namespaceId}/runtimes` : "/runtimes") - }) - }) - }, [runtime, router]) - - const rotateToken = React.useCallback(() => { - startTransition(() => { - runtimeService.runtimeRotateToken({ - runtimeId: runtime?.id!! - }).then(payload => { - if ((payload?.errors?.length ?? 0) <= 0) { - if (payload?.runtime?.token) { - setToken(payload.runtime.token) - addIslandSuccessNotification({ - message: "Rotated runtime token" - }) - } - } - }) - }) - }, [runtime]) - - - return - - - - - - - - - - - - - - - - - <> - - - General - - - - - - - Name - - - - - - Description - - - - - - - How to connect - - - - - - Access token - Is used to connect the runtime to our - service - - {token ? ( - - ) : ( - - )} - - - - - - - - Delete - - - - - - - This will delete the runtime and cannot be undone. - - - - - - - - - - -} \ No newline at end of file + const runtimesHref = namespaceIndex ? `/namespace/${namespaceIndex}/runtimes` : "/runtimes" + + return { + if (open) return + + const nav = (window as unknown as { navigation?: { entries(): { url: string }[], currentEntry?: { index: number } } }).navigation + if (!nav?.entries) { + router.back() + return + } + + const index = nav.currentEntry?.index ?? 0 + if (index > 0) router.back() + else router.push(runtimesHref) + }}/> +} diff --git a/src/packages/ce/src/runtime/pages/RuntimesPage.tsx b/src/packages/ce/src/runtime/pages/RuntimesPage.tsx deleted file mode 100644 index aff01cab..00000000 --- a/src/packages/ce/src/runtime/pages/RuntimesPage.tsx +++ /dev/null @@ -1,145 +0,0 @@ -"use client" - -import React from "react"; -import { - Button, - Flex, - Menu, - MenuCheckboxItem, - MenuContent, - MenuPortal, - MenuTrigger, - Spacing, - Text, - useService, - useStore -} from "@code0-tech/pictor"; -import Link from "next/link"; -import {notFound, useParams, useRouter} from "next/navigation"; -import {UserService} from "@edition/user/services/User.service"; -import {RuntimeDataTableComponent} from "@edition/runtime/components/RuntimeDataTableComponent"; -import {DataTableFilterProps, DataTableSortProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; -import {RuntimeDataTableFilterInputComponent} from "@edition/runtime/components/RuntimeDataTableFilterInputComponent"; -import {IconMinus, IconSortAscending, IconSortDescending} from "@tabler/icons-react"; -import {ButtonGroup} from "@code0-tech/pictor/dist/components/button-group/ButtonGroup"; -import {useUserSession} from "@edition/user/hooks/User.session.hook"; - -export const RuntimesPage: React.FC = () => { - - const params = useParams() - const currentSession = useUserSession() - const userStore = useStore(UserService) - const userService = useService(UserService) - const currentUser = React.useMemo(() => userService.getById(currentSession?.user?.id), [userStore, currentSession]) - - const [filter, setFilter] = React.useState({}) - const [sort, setSort] = React.useState({}) - - const namespaceIndex = params.namespaceId as any as number - - if (!namespaceIndex && currentUser && !currentUser.admin) { - notFound() - } - - const router = useRouter() - return
    - - - - Runtimes - - - Manage runtimes that you have access to. Runtimes are the environments where your applications run. - - - - - - - - - - - - - { - if (sort["name"] === undefined) - setSort(prev => ({...prev, name: "asc"})) - else if (sort["name"] === "asc") - setSort(prev => ({...prev, name: "desc"})) - else - setSort(prev => ({...prev, name: undefined})) - - event.preventDefault() - event.stopPropagation() - }}> - {sort["name"] === undefined ? : sort["name"] === "asc" ? - : } - Name - - { - if (sort["createdAt"] === undefined) - setSort(prev => ({...prev, createdAt: "asc"})) - else if (sort["createdAt"] === "asc") - setSort(prev => ({...prev, createdAt: "desc"})) - else - setSort(prev => ({...prev, createdAt: undefined})) - - event.preventDefault() - event.stopPropagation() - }}> - {sort["createdAt"] === undefined ? - : sort["createdAt"] === "asc" ? - : } - Created At - - { - if (sort["updatedAt"] === undefined) - setSort(prev => ({...prev, updatedAt: "asc"})) - else if (sort["updatedAt"] === "asc") - setSort(prev => ({...prev, updatedAt: "desc"})) - else - setSort(prev => ({...prev, updatedAt: undefined})) - - event.preventDefault() - event.stopPropagation() - }}> - {sort["updatedAt"] === undefined ? - : sort["updatedAt"] === "asc" ? - : } - Updated At - - - - - - - -
    - namespaceIndex ? true : !runtime?.namespace?.id} - namespaceId={namespaceIndex ? `gid://sagittarius/Namespace/${namespaceIndex}` : undefined} - onChange={filter => setFilter(filter)}/> -
    - - namespaceIndex ? true : !runtime?.namespace?.id} - onSelect={(runtime) => { - const number = runtime?.id?.match(/Runtime\/(\d+)$/)?.[1] - router.push(namespaceIndex ? `/namespace/${namespaceIndex}/runtimes/${number}/settings` : `/runtimes/${number}/settings`) - }}/> -
    -} \ No newline at end of file diff --git a/src/packages/ce/src/user/components/UserCreateDialogComponent.tsx b/src/packages/ce/src/user/components/UserCreateDialogComponent.tsx new file mode 100644 index 00000000..77d319a7 --- /dev/null +++ b/src/packages/ce/src/user/components/UserCreateDialogComponent.tsx @@ -0,0 +1,151 @@ +"use client" + +import React, {startTransition} from "react"; +import { + Button, + Card, + Col, + Dialog, + DialogClose, + DialogContent, + DialogOverlay, + DialogPortal, + EmailInput, + emailValidation, + Flex, + PasswordInput, + passwordValidation, + Row, + Spacing, + SwitchInput, + Text, + TextInput, + useForm, + useService +} from "@code0-tech/pictor"; +import CardSection from "@code0-tech/pictor/dist/components/card/CardSection"; +import {IconAt, IconLock, IconMail, IconUser} from "@tabler/icons-react"; +import {UserService} from "@edition/user/services/User.service"; +import {toast} from "@code0-tech/pictor/dist/components/toast/Toast"; +import {InputDialog} from "@core/components/InputDialog"; + +export interface UserCreateDialogComponentProps { + open?: boolean + onOpenChange?: (open: boolean) => void +} + +export const UserCreateDialogComponent: React.FC = ({open, onOpenChange}) => { + + const userService = useService(UserService) + + const initialValues = React.useMemo(() => ({ + email: null, + username: null, + firstname: null, + lastname: null, + password: null, + repeatPassword: null, + admin: false + }), []) + + const [inputs, validate] = useForm<{ + email: string | null, + username: string | null, + firstname: string | null, + lastname: string | null, + password: string | null, + repeatPassword: string | null, + admin: boolean | null, + }>({ + useInitialValidation: false, + initialValues: initialValues, + validate: { + email: (value) => { + if (!value) return "Email is required" + if (!emailValidation(value)) return "Please provide a valid email" + return null + }, + username: (value) => { + if (!value) return "Username is required" + return null + }, + password: passwordValidation, + repeatPassword: (value, values) => { + if (passwordValidation(value) != null) return passwordValidation(value) + if (value != values?.password) return "Passwords do not match" + return null + } + }, + onSubmit: (values) => { + if (!values.email || !values.username || !values.password || !values.repeatPassword) return + startTransition(async () => { + await userService.usersCreate({ + email: values.email!!, + username: values.username!!, + firstname: values.firstname, + lastname: values.lastname, + password: values.password!!, + passwordRepeat: values.repeatPassword!!, + admin: values.admin, + }).then(payload => { + if (payload?.user) { + onOpenChange?.(false) + toast({title: "Invited user", color: "success"}) + } + }) + }) + } + }) + + return onOpenChange?.(open)}> + + + } leftType={"icon"} + {...inputs.getInputProps("firstname")}/> + + + } leftType={"icon"} + {...inputs.getInputProps("lastname")}/> + + + + } leftType={"icon"} + {...inputs.getInputProps("email")}/> + + } leftType={"icon"} + {...inputs.getInputProps("username")}/> + + } leftType={"icon"} + onChange={() => validate("password")} + {...inputs.getInputProps("password")}/> + + } leftType={"icon"} + onChange={() => validate("repeatPassword")} + {...inputs.getInputProps("repeatPassword")}/> + + + + + + Admin + Grant this user administrator + privileges. + + + + + + + + +} diff --git a/src/packages/ce/src/user/components/UserDataTableFilterInputComponent.tsx b/src/packages/ce/src/user/components/UserDataTableFilterInputComponent.tsx deleted file mode 100644 index 1b2cf287..00000000 --- a/src/packages/ce/src/user/components/UserDataTableFilterInputComponent.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React from "react"; -import { - DataTableFilterInput, - DataTableFilterSuggestionMenu, - MenuCheckboxItem, - useService, - useStore -} from "@code0-tech/pictor"; -import {IconCheck} from "@tabler/icons-react"; -import {DataTableFilterProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; -import {UserService} from "@edition/user/services/User.service"; - -export interface UserDataTableFilterInputComponentProps { - onChange: (filter: DataTableFilterProps) => void -} - -export const UserDataTableFilterInputComponent: React.FC = (props) => { - - const {onChange} = props - - const userService = useService(UserService) - const userStore = useStore(UserService) - - const users = React.useMemo( - () => userService.values(), - [userStore] - ) - - return { - - const split = currentValue.split(",").map(s => s.trim()).filter(Boolean) - - return - {users.map(user => { - - const isChecked = split.includes(user?.username!) - - return { - e.preventDefault(); - e.stopPropagation(); - const updated = isChecked - ? split.filter(name => name !== user?.username) - : [...split, user?.username]; - applySuggestion(updated.join(","), true); - }}> - {isChecked ? : } - @{user?.username} - - })} - - } - }, { - token: "Email", - key: "email", - operators: ["isOneOf"], - suggestion: (context, operator, currentValue, applySuggestion) => { - - const split = currentValue.split(",").map(s => s.trim()).filter(Boolean) - - return - {users.map(user => { - - const isChecked = split.includes(user?.email!) - - return { - e.preventDefault(); - e.stopPropagation(); - const updated = isChecked - ? split.filter(name => name !== user?.email) - : [...split, user?.email]; - applySuggestion(updated.join(","), true); - }}> - {isChecked ? : } - {user?.email} - - })} - - } - }, { - token: "Owner", - key: "admin", - operators: ["isOneOf"], - suggestion: (context, operator, currentValue, applySuggestion) => { - - const split = currentValue.split(",").map(s => s.trim()).filter(Boolean) - - return - {["true", "false"].map(admin => { - - const isChecked = split.includes(admin) - - return { - e.preventDefault(); - e.stopPropagation(); - const updated = isChecked - ? split.filter(name => name !== admin) - : [...split, admin]; - applySuggestion(updated.join(","), true); - }}> - {isChecked ? : } - {admin} - - })} - - } - } - ]}/> -} \ No newline at end of file diff --git a/src/packages/ce/src/user/components/UserEditDialogComponent.tsx b/src/packages/ce/src/user/components/UserEditDialogComponent.tsx index dbbb28c9..2238f679 100644 --- a/src/packages/ce/src/user/components/UserEditDialogComponent.tsx +++ b/src/packages/ce/src/user/components/UserEditDialogComponent.tsx @@ -2,15 +2,9 @@ import React from "react"; import { - Badge, Button, Card, Col, - Dialog, - DialogClose, - DialogContent, - DialogOverlay, - DialogPortal, EmailInput, emailValidation, Flex, @@ -27,15 +21,14 @@ import { useStore } from "@code0-tech/pictor"; import CardSection from "@code0-tech/pictor/dist/components/card/CardSection"; -import {Tab, TabContent, TabList, TabTrigger} from "@code0-tech/pictor/dist/components/tab/Tab"; +import {TabContent, TabList, TabTrigger} from "@code0-tech/pictor/dist/components/tab/Tab"; import {User, UsersUpdateInput} from "@code0-tech/sagittarius-graphql-types"; import {UserService} from "@edition/user/services/User.service"; import {useUserSession} from "@edition/user/hooks/User.session.hook"; -import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {toast} from "@code0-tech/pictor/dist/components/toast/Toast"; import {IconAt, IconBackground, IconLock, IconMail, IconSettings2, IconShieldLock} from "@tabler/icons-react"; -import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout"; -import {motion} from "framer-motion"; import {UserSessionsDataTableComponent} from "@edition/user/components/UserSessionsDataTableComponent"; +import {SettingDialog} from "@core/components/SettingDialog"; export interface UserEditDialogComponentProps { userId?: User['id'] @@ -126,232 +119,185 @@ export const UserEditDialogComponent: React.FC = ( // nothing but the userId changed, so there is nothing to update if (Object.keys(payload).length <= 1) { - onOpenChange?.(false) return } startTransition(async () => { await userService.usersUpdate(payload).then(payload => { if (payload?.user && (payload?.errors?.length ?? 0) <= 0) { - onOpenChange?.(false) - addIslandSuccessNotification({ - message: "Updated user", - }) + toast({title: "Updated user", color: "success"}) } }) }) } }) - return onOpenChange?.(open)}> - - - - - - - Settings of @{user?.username ?? ""} - - - - Edit the general settings, permissions and security for user - @{user?.username ?? ""} - - - - - - - {!isSelf && ( - - - - )} - - - - - - - - - - - - }> - - - - General - + return onOpenChange?.(open)} + title={`Settings of @${user?.username ?? ""}`} + description={`Edit the general settings, permissions and security for user @${user?.username ?? ""}`} + trigger={ + + + + {!isSelf && ( + + + + )} + + + + + + + }> + + + General + - - - - - - - - - - + + + + The name, email and general profile details for @{user?.username ?? ""}. + + + + + + + + + + - + - } leftType={"icon"} - {...inputs.getInputProps("email")}/> - - } leftType={"icon"} - {...inputs.getInputProps("username")}/> - - - - - - Access - - - - - - - - Admin - - Grant this user administrator privileges. - - - - - - - - - Blocked - - Block this user from accessing the application. - - - - - - - - - - Security - + } leftType={"icon"} + {...inputs.getInputProps("email")}/> + + } leftType={"icon"} + {...inputs.getInputProps("username")}/> + + + + + + Access + + + + + Manage administrator privileges and access for @{user?.username ?? ""}. + + + + + + + Admin + + Grant this user administrator privileges. + + + + + + + + + Blocked + + Block this user from accessing the application. + + + + + + + + + + Security + - - - } leftType={"icon"} - onChange={() => validate("password")} - {...inputs.getInputProps("password")}/> - - } leftType={"icon"} - onChange={() => validate("repeatPassword")} - {...inputs.getInputProps("repeatPassword")}/> - - - - Sessions - - - - Active sign-in sessions for @{user?.username ?? ""}. Log out a session to revoke - its access. - - - - - - - - - - +
    + + + Update the password used to sign in to @{user?.username ?? ""}. + + + } leftType={"icon"} + onChange={() => validate("password")} + {...inputs.getInputProps("password")}/> + + } leftType={"icon"} + onChange={() => validate("repeatPassword")} + {...inputs.getInputProps("repeatPassword")}/> +
    + + + Sessions + + + + Active sign-in sessions for @{user?.username ?? ""}. Log out a session to revoke + its access. + + + + + } diff --git a/src/packages/ce/src/user/components/UserMenuComponent.tsx b/src/packages/ce/src/user/components/UserMenuComponent.tsx deleted file mode 100644 index d69c9782..00000000 --- a/src/packages/ce/src/user/components/UserMenuComponent.tsx +++ /dev/null @@ -1,59 +0,0 @@ -"use client" - -import React from "react" -import {Scalars} from "@code0-tech/sagittarius-graphql-types"; -import { - Avatar, - Button, - Flex, - Menu, - MenuContent, - MenuPortal, - MenuProps, - MenuTrigger, - Text, - useService, - useStore -} from "@code0-tech/pictor"; -import {UserService} from "@edition/user/services/User.service"; - -export interface UserMenuComponentProps extends MenuProps { - userId: Scalars['UserID']['output'] -} - -const UserMenuComponent: React.FC = props => { - const userService = useService(UserService) - const userStore = useStore(UserService) - const currentUser = React.useMemo(() => userService.getById(props.userId), [userStore, userService]) - - return React.useMemo(() => { - return ( - - - - - - - {props.children} - - - - ) - }, [currentUser]) -} - -export default UserMenuComponent \ No newline at end of file diff --git a/src/packages/ce/src/user/components/UserReadmeComponent.style.scss b/src/packages/ce/src/user/components/UserReadmeComponent.style.scss new file mode 100644 index 00000000..0dfd7672 --- /dev/null +++ b/src/packages/ce/src/user/components/UserReadmeComponent.style.scss @@ -0,0 +1,103 @@ +@use "@core/style/variables"; + +.user-readme { + color: rgba(variables.$white, variables.$hierarchySecondary); + font-size: variables.$md; + line-height: 1.8; + word-break: break-word; + + h1, h2, h3, h4, h5, h6 { + color: rgba(variables.$white, variables.$hierarchyPrimary); + line-height: 1.3; + margin: 2rem 0 1rem; + font-weight: 600; + } + + h1 { font-size: variables.$xl; } + h2 { font-size: variables.$lg; } + h3 { font-size: variables.$md; } + h4, h5, h6 { font-size: variables.$sm; } + + > :first-child { + margin-top: 0; + } + + p { + margin: 0 0 1rem; + } + + a { + color: variables.$info; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + + ul, ol { + margin: 0 0 1rem; + padding-left: 1.5rem; + } + + li { + margin: 0.25rem 0; + } + + blockquote { + margin: 0 0 1rem; + padding: 0.25rem 0 0.25rem 1rem; + border-left: 2px solid rgba(variables.$white, 0.2); + color: rgba(variables.$white, variables.$hierarchyTertiary); + } + + code { + font-family: monospace; + font-size: variables.$sm; + background: rgba(variables.$white, 0.08); + padding: 0.1rem 0.35rem; + border-radius: 0.35rem; + } + + pre { + margin: 0 0 1rem; + padding: 1rem; + background: rgba(variables.$white, 0.05); + border-radius: variables.$xs; + overflow-x: auto; + + code { + background: none; + padding: 0; + } + } + + img { + max-width: 100%; + border-radius: variables.$xs; + } + + hr { + border: none; + border-top: 1px dashed rgba(variables.$white, 0.1); + margin: 2rem 0; + } + + table { + width: 100%; + border-collapse: collapse; + margin: 0 0 1rem; + font-size: variables.$sm; + } + + th, td { + border: 1px solid rgba(variables.$white, 0.1); + padding: 0.5rem 0.75rem; + text-align: left; + } + + th { + color: rgba(variables.$white, variables.$hierarchyPrimary); + font-weight: 600; + } +} diff --git a/src/packages/ce/src/user/components/UserReadmeComponent.tsx b/src/packages/ce/src/user/components/UserReadmeComponent.tsx new file mode 100644 index 00000000..d2a1e913 --- /dev/null +++ b/src/packages/ce/src/user/components/UserReadmeComponent.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import "./UserReadmeComponent.style.scss"; + +export interface UserReadmeComponentProps { + readme: string +} + +export const UserReadmeComponent: React.FC = ({readme}) => { + return
    + + {readme} + +
    +} diff --git a/src/packages/ce/src/user/components/UserSessionsDataTableRowComponent.tsx b/src/packages/ce/src/user/components/UserSessionsDataTableRowComponent.tsx index 03f4d811..a70da004 100644 --- a/src/packages/ce/src/user/components/UserSessionsDataTableRowComponent.tsx +++ b/src/packages/ce/src/user/components/UserSessionsDataTableRowComponent.tsx @@ -7,7 +7,7 @@ import {UserService} from "@edition/user/services/User.service"; import {useUserSession} from "@edition/user/hooks/User.session.hook"; import {formatDistanceToNow} from "date-fns"; import {IconDeviceDesktop, IconLogout} from "@tabler/icons-react"; -import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {toast} from "@code0-tech/pictor/dist/components/toast/Toast"; export interface UserSessionsDataTableRowComponentProps { userId: User['id'] @@ -33,9 +33,7 @@ export const UserSessionsDataTableRowComponent: React.FC { if ((payload?.errors?.length ?? 0) <= 0) { - addIslandSuccessNotification({ - message: "Logged out session" - }) + toast({title: "Logged out session", color: "success"}) } }) }, [sessionId]) diff --git a/src/packages/ce/src/user/pages/UserInvitePage.tsx b/src/packages/ce/src/user/pages/UserInvitePage.tsx index 87a042d1..57c82f0f 100644 --- a/src/packages/ce/src/user/pages/UserInvitePage.tsx +++ b/src/packages/ce/src/user/pages/UserInvitePage.tsx @@ -1,168 +1,39 @@ "use client" import React from "react"; -import { - Button, - Col, - EmailInput, - emailValidation, - Flex, - PasswordInput, - passwordValidation, - Spacing, - SwitchInput, - Text, - TextInput, - useForm, - useService, - useStore -} from "@code0-tech/pictor"; -import Link from "next/link"; -import {useRouter, notFound} from "next/navigation"; +import {useService, useStore} from "@code0-tech/pictor"; +import {notFound, useRouter} from "next/navigation"; import {UserService} from "@edition/user/services/User.service"; import {useUserSession} from "@edition/user/hooks/User.session.hook"; -import {addIslandSuccessNotification, addIslandErrorNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; -import {IconAt, IconLock, IconMail, IconUser} from "@tabler/icons-react"; +import {UserCreateDialogComponent} from "@edition/user/components/UserCreateDialogComponent"; export const UserInvitePage: React.FC = () => { + const router = useRouter() const currentSession = useUserSession() const userService = useService(UserService) const userStore = useStore(UserService) - const currentUser = React.useMemo(() => userService.getById(currentSession?.user?.id), [userStore, currentSession]) - const router = useRouter() - const [, startTransition] = React.useTransition() - - const [inputs, validate] = useForm<{ - email: string | null, - username: string | null, - firstname: string | null, - lastname: string | null, - password: string | null, - repeatPassword: string | null, - admin: boolean | null, - }>({ - useInitialValidation: false, - initialValues: { - email: null, - username: null, - firstname: null, - lastname: null, - password: null, - repeatPassword: null, - admin: false, - }, - validate: { - email: (value) => { - if (!value) return "Email is required" - if (!emailValidation(value)) return "Please provide a valid email" - return null - }, - username: (value) => { - if (!value) return "Username is required" - return null - }, - password: passwordValidation, - repeatPassword: (value, values) => { - if (passwordValidation(value) != null) return passwordValidation(value) - if (value != values?.password) return "Passwords do not match" - return null - } - }, - onSubmit: (values) => { - if (!values.email || !values.username || !values.password || !values.repeatPassword) return - startTransition(async () => { - await userService.usersCreate({ - email: values.email!!, - username: values.username!!, - firstname: values.firstname, - lastname: values.lastname, - password: values.password!!, - passwordRepeat: values.repeatPassword!!, - admin: values.admin, - }).then(payload => { - if (payload?.user) { - router.push("/users") - addIslandSuccessNotification({ - message: "Invited user", - }) - } - }) - }) - } - }) + const currentUser = React.useMemo( + () => userService.getById(currentSession?.user?.id), + [userStore, currentSession] + ) if (currentUser && !currentUser.admin) { notFound() } - return
    - - - - Invite a new user - - - - Create a new user with access to your instance. They can log in with the credentials you provide. - - - - {/*@ts-ignore*/} - } leftType={"icon"} - {...inputs.getInputProps("firstname")}/> - {/*@ts-ignore*/} - } leftType={"icon"} - {...inputs.getInputProps("lastname")}/> - - - {/*@ts-ignore*/} - } leftType={"icon"} - {...inputs.getInputProps("email")}/> - - {/*@ts-ignore*/} - } leftType={"icon"} - {...inputs.getInputProps("username")}/> - - } leftType={"icon"} - onChange={() => validate("password")} - {...inputs.getInputProps("password")}/> - - } leftType={"icon"} - onChange={() => validate("repeatPassword")} - {...inputs.getInputProps("repeatPassword")}/> - - - - Admin - Grant this user administrator privileges. - - - - - - - - - - - - -
    + return { + if (open) return + + const nav = (window as unknown as { navigation?: { entries(): { url: string }[], currentEntry?: { index: number } } }).navigation + if (!nav?.entries) { + router.back() + return + } + + const index = nav.currentEntry?.index ?? 0 + if (index > 0) router.back() + else router.push("/users") + }}/> } diff --git a/src/packages/ce/src/user/pages/UserLoginPage.tsx b/src/packages/ce/src/user/pages/UserLoginPage.tsx index 0cce0049..bcef4b2e 100644 --- a/src/packages/ce/src/user/pages/UserLoginPage.tsx +++ b/src/packages/ce/src/user/pages/UserLoginPage.tsx @@ -32,7 +32,6 @@ export const UserLoginPage: React.FC = () => { if (!values.password || !values.email || !emailValidation(values.email)) return if (isInTimeout) { toast({ - dismissible: false, title: "Too many failed attempts. Please try again in 5 seconds.", color: "error", duration: 5000, diff --git a/src/packages/ce/src/user/pages/UserPage.tsx b/src/packages/ce/src/user/pages/UserPage.tsx index bb168afd..55761909 100644 --- a/src/packages/ce/src/user/pages/UserPage.tsx +++ b/src/packages/ce/src/user/pages/UserPage.tsx @@ -1,17 +1,12 @@ "use client" import React from "react"; -import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout"; import {useParams} from "next/navigation"; -import Link from "next/link"; import { - AuroraBackground, - Avatar, - Badge, - Button, - Col, Flex, ScrollArea, + ScrollAreaScrollbar, + ScrollAreaThumb, ScrollAreaViewport, Spacing, Text, @@ -20,97 +15,63 @@ import { } from "@code0-tech/pictor"; import {UserService} from "@edition/user/services/User.service"; import {useUserSession} from "@edition/user/hooks/User.session.hook"; +import {UserReadmeComponent} from "@edition/user/components/UserReadmeComponent"; import {User} from "@code0-tech/sagittarius-graphql-types"; -import {IconMail, IconSparkles, IconUser} from "@tabler/icons-react"; export const UserPage: React.FC = () => { - const params = useParams() const userService = useService(UserService) const userStore = useStore(UserService) const currentSession = useUserSession() - const currentUser = React.useMemo( - () => userService.getById(currentSession?.user?.id), - [userStore, currentSession] - ) + + const sessionUser = currentSession?.user const userIndex = decodeURIComponent(String(params.userId ?? "")) const userId: User['id'] = userIndex === "@me" - ? currentSession?.user?.id! + ? sessionUser?.id! : `gid://sagittarius/User/${userIndex as any as number}` const user = React.useMemo( - () => userService.getById(userId), - [userStore, userId, userService] + () => userService.getById(userId) ?? (userId === sessionUser?.id ? sessionUser : undefined), + [userStore, userId, currentSession] ) + const readme = user?.readme?.trim() - const leftContent = - - -
    - - - 👋 - -
    - - {user?.firstname} {user?.lastname} - - - - - @{user?.username} - - - - {user?.email} - - - - BASIC - - - - {currentUser?.id === userId && ( - <> + return
    + + + {readme ? ( +
    + Readme + + +
    + ) : ( + + + @{user?.username} hasn't written a readme yet + - - - - - - + + When they add a profile description, it'll show up here. + + )} - -
    -
    - - return -
    - - - - @{user?.username}'s readme - - - - The user currently does not have a publicly available profile description. - - - -
    -
    + + + + + +
    } - diff --git a/src/packages/ce/src/user/pages/UsersPage.tsx b/src/packages/ce/src/user/pages/UsersPage.tsx deleted file mode 100644 index 6a0bd983..00000000 --- a/src/packages/ce/src/user/pages/UsersPage.tsx +++ /dev/null @@ -1,136 +0,0 @@ -"use client" - -import React from "react"; -import { - Button, - ButtonGroup, - Flex, - Menu, - MenuCheckboxItem, - MenuContent, - MenuPortal, - MenuTrigger, - Spacing, - Text, - useService, - useStore -} from "@code0-tech/pictor"; -import {UserService} from "@edition/user/services/User.service"; -import Link from "next/link"; -import {notFound, useRouter} from "next/navigation"; -import {UserDataTableComponent} from "@edition/user/components/UserDataTableComponent"; -import {DataTableFilterProps, DataTableSortProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; -import {UserDataTableFilterInputComponent} from "@edition/user/components/UserDataTableFilterInputComponent"; -import {IconMinus, IconSortAscending, IconSortDescending} from "@tabler/icons-react"; -import {useUserSession} from "@edition/user/hooks/User.session.hook"; - -export const UsersPage: React.FC = () => { - - const router = useRouter() - const currentSession = useUserSession() - const userStore = useStore(UserService) - const userService = useService(UserService) - const currentUser = React.useMemo(() => userService.getById(currentSession?.user?.id), [userStore, currentSession]) - - const [filter, setFilter] = React.useState({}) - const [sort, setSort] = React.useState({}) - - if (currentUser && !currentUser.admin) { - notFound() - } - - return
    - - - - Users - - - Manage users who have access to your instance. You can invite new users and remove existing ones. - - - - - - - - - - - - - { - if (sort["username"] === undefined) - setSort(prev => ({...prev, username: "asc"})) - else if (sort["username"] === "asc") - setSort(prev => ({...prev, username: "desc"})) - else - setSort(prev => ({...prev, username: undefined})) - - event.preventDefault() - event.stopPropagation() - }}> - {sort["username"] === undefined ? : sort["name"] === "asc" ? - : } - Username - - { - if (sort["createdAt"] === undefined) - setSort(prev => ({...prev, createdAt: "asc"})) - else if (sort["createdAt"] === "asc") - setSort(prev => ({...prev, createdAt: "desc"})) - else - setSort(prev => ({...prev, createdAt: undefined})) - - event.preventDefault() - event.stopPropagation() - }}> - {sort["createdAt"] === undefined ? - : sort["createdAt"] === "asc" ? - : } - Created At - - { - if (sort["updatedAt"] === undefined) - setSort(prev => ({...prev, updatedAt: "asc"})) - else if (sort["updatedAt"] === "asc") - setSort(prev => ({...prev, updatedAt: "desc"})) - else - setSort(prev => ({...prev, updatedAt: undefined})) - - event.preventDefault() - event.stopPropagation() - }}> - {sort["updatedAt"] === undefined ? - : sort["updatedAt"] === "asc" ? - : } - Updated At - - - - - - - -
    - setFilter(filter)}/> -
    - - { - if (item?.id) router.push(`/users/${item.id.split("/").pop()}/settings`) - }}/> -
    -} \ No newline at end of file diff --git a/src/packages/ce/src/user/services/User.service.ts b/src/packages/ce/src/user/services/User.service.ts index cb69a642..40d50e67 100644 --- a/src/packages/ce/src/user/services/User.service.ts +++ b/src/packages/ce/src/user/services/User.service.ts @@ -187,11 +187,21 @@ export class UserService extends ReactiveArrayService { }) if (result.data && result.data.usersUpdate && result.data.usersUpdate.user) { - //TODO: dont use the result. Instead merge result and already existing data together const updatedUser = result.data.usersUpdate.user const index = super.values().findIndex(user => user.id === updatedUser.id) - if (index >= 0) this.set(index, new View(updatedUser)) - else if (!this.hasById(updatedUser.id)) this.add(new View(updatedUser)) + if (index >= 0) { + // The update mutation only returns the UserBasic fragment, which omits + // connection fields (namespaceMemberships, identities, sessions). Merge the + // result into the existing user so those connections are preserved. + const existingUser = super.values()[index] + this.set(index, new View({ + ...existingUser, + ...updatedUser, + namespaceMemberships: existingUser.namespaceMemberships, + identities: existingUser.identities, + sessions: existingUser.sessions, + })) + } else if (!this.hasById(updatedUser.id)) this.add(new View(updatedUser)) } return result.data?.usersUpdate ?? undefined diff --git a/src/packages/ce/src/user/views/UserProfileView.tsx b/src/packages/ce/src/user/views/UserProfileView.tsx new file mode 100644 index 00000000..28fb83a8 --- /dev/null +++ b/src/packages/ce/src/user/views/UserProfileView.tsx @@ -0,0 +1,98 @@ +"use client" + +import React from "react"; +import {useParams} from "next/navigation"; +import Link from "next/link"; +import { + AuroraBackground, + Avatar, + Badge, + Button, + Flex, + Spacing, + Text, + useService, + useStore +} from "@code0-tech/pictor"; +import {UserService} from "@edition/user/services/User.service"; +import {useUserSession} from "@edition/user/hooks/User.session.hook"; +import {User} from "@code0-tech/sagittarius-graphql-types"; +import {IconMail, IconSparkles, IconUser} from "@tabler/icons-react"; + +const separator =
    + +export const UserProfileView: React.FC = () => { + + const params = useParams() + const userService = useService(UserService) + const userStore = useStore(UserService) + + const currentSession = useUserSession() + const currentUser = React.useMemo( + () => userService.getById(currentSession?.user?.id), + [userStore, currentSession] + ) + + const sessionUser = currentSession?.user + + const userIndex = decodeURIComponent(String(params.userId ?? "")) + const userId: User['id'] = userIndex === "@me" + ? sessionUser?.id! + : `gid://sagittarius/User/${userIndex as any as number}` + + const user = React.useMemo( + () => userService.getById(userId) ?? (userId === sessionUser?.id ? sessionUser : undefined), + [userStore, userId, userService, sessionUser] + ) + + const isCurrentUser = currentUser?.id === userId + + return + + + + + + {user?.firstname} {user?.lastname} + + + @{user?.username} + + + + + {separator} + + + Details + + + + {user?.email} + + + + + BASIC + + + + Member + + + + {isCurrentUser && ( +
    + + + + + +
    + )} +
    +} diff --git a/src/packages/cloud/src/application/views/ApplicationLicensesTabTriggerView.tsx b/src/packages/cloud/src/application/views/ApplicationLicensesTabTriggerView.tsx new file mode 100644 index 00000000..08204491 --- /dev/null +++ b/src/packages/cloud/src/application/views/ApplicationLicensesTabTriggerView.tsx @@ -0,0 +1,3 @@ +import {ApplicationLicensesTabTriggerView as CEApplicationLicensesTabTriggerView} from "@ce-internal/application/views/ApplicationLicensesTabTriggerView" + +export const ApplicationLicensesTabTriggerView = CEApplicationLicensesTabTriggerView diff --git a/src/packages/cloud/src/application/views/ApplicationTabListView.tsx b/src/packages/cloud/src/application/views/ApplicationTabListView.tsx deleted file mode 100644 index 0d3b6d4f..00000000 --- a/src/packages/cloud/src/application/views/ApplicationTabListView.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import {ApplicationTabListView as CEApplicationTabListView} from "@ce-internal/application/views/ApplicationTabListView" - -export const ApplicationTabListView = CEApplicationTabListView \ No newline at end of file diff --git a/src/packages/cloud/src/namespace/components/NamespaceLicensesDataTableRowComponent.tsx b/src/packages/cloud/src/namespace/components/NamespaceLicensesDataTableRowComponent.tsx index 255c0889..81439fe0 100644 --- a/src/packages/cloud/src/namespace/components/NamespaceLicensesDataTableRowComponent.tsx +++ b/src/packages/cloud/src/namespace/components/NamespaceLicensesDataTableRowComponent.tsx @@ -4,7 +4,7 @@ import {Badge, Button, DataTableColumn, Flex, Progress, Spacing, Text, useServic import {formatDistanceToNow, isFuture, isPast} from "date-fns"; import {IconX} from "@tabler/icons-react"; import {NamespaceService} from "@cloud-internal/namespace/services/Namespace.service"; -import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {toast} from "@code0-tech/pictor/dist/components/toast/Toast"; export interface LicensesDataTableRowComponentProps { namespaceId: Namespace['id'] @@ -28,9 +28,7 @@ export const NamespaceLicensesDataTableRowComponent: React.FC { if ((payload?.errors?.length ?? 0) <= 0) { - addIslandSuccessNotification({ - message: "Deleted license" - }) + toast({title: "Deleted license", color: "success"}) } }) }, []) diff --git a/src/packages/cloud/src/namespace/pages/NamespaceSettingsPage.tsx b/src/packages/cloud/src/namespace/pages/NamespaceSettingsPage.tsx deleted file mode 100644 index e5caae5a..00000000 --- a/src/packages/cloud/src/namespace/pages/NamespaceSettingsPage.tsx +++ /dev/null @@ -1,60 +0,0 @@ -"use client" - -import React from "react"; -import {Tab, TabContent} from "@code0-tech/pictor/dist/components/tab/Tab"; -import {NamespaceDeleteView} from "@edition/namespace/views/NamespaceDeleteView"; -import {NamespaceGeneralSettingsView} from "@edition/namespace/views/NamespaceGeneralSettingsView"; -import { - ResizableHandle, - ResizablePanel, - ResizablePanelGroup -} from "@code0-tech/pictor/dist/components/resizable/Resizable"; -import {SidebarComponent} from "@core/components/SidebarComponent"; -import {NamespaceTabListView} from "@edition/namespace/views/NamespaceTabListView"; -import {useParams} from "next/navigation"; -import {useService, useStore} from "@code0-tech/pictor"; -import {NamespaceService} from "@edition/namespace/services/Namespace.service"; -import {Namespace} from "@code0-tech/sagittarius-graphql-types"; -import {NamespaceLicensesView} from "@cloud-internal/namespace/views/NamespaceLicensesView"; - -export const NamespaceSettingsPage: React.FC = () => { - - const params = useParams() - const namespaceService = useService(NamespaceService) - const namespaceStore = useStore(NamespaceService) - - const namespaceIndex = params.namespaceId as any as number - const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` - - const namespace = React.useMemo( - () => namespaceService.getById(namespaceId), - [namespaceStore, namespaceId] - ) - - //TODO: add ability check for organization settings access for every settings tab - - return - - - - - - - <> - - - - - - - - - - - - - -} \ No newline at end of file diff --git a/src/packages/cloud/src/namespace/views/NamespaceLicensesTabTriggerView.tsx b/src/packages/cloud/src/namespace/views/NamespaceLicensesTabTriggerView.tsx new file mode 100644 index 00000000..549df74d --- /dev/null +++ b/src/packages/cloud/src/namespace/views/NamespaceLicensesTabTriggerView.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import {TabTrigger} from "@code0-tech/pictor/dist/components/tab/Tab"; +import {Button, Text} from "@code0-tech/pictor"; +import {IconLicense} from "@tabler/icons-react"; + +export const NamespaceLicensesTabTriggerView: React.FC = () => { + return + + +} diff --git a/src/packages/cloud/src/namespace/views/NamespaceTabListView.tsx b/src/packages/cloud/src/namespace/views/NamespaceTabListView.tsx deleted file mode 100644 index 27bfad5d..00000000 --- a/src/packages/cloud/src/namespace/views/NamespaceTabListView.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from "react"; -import {TabList, TabTrigger} from "@code0-tech/pictor/dist/components/tab/Tab"; -import {Button, Text, useService, useStore} from "@code0-tech/pictor"; -import {useParams} from "next/navigation"; -import {NamespaceService} from "@edition/namespace/services/Namespace.service"; -import {Namespace} from "@code0-tech/sagittarius-graphql-types"; - -export const NamespaceTabListView: React.FC = () => { - - const params = useParams() - const namespaceService = useService(NamespaceService) - const namespaceStore = useStore(NamespaceService) - - const namespaceIndex = params.namespaceId as any as number - const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` - - const namespace = React.useMemo( - () => namespaceService.getById(namespaceId), - [namespaceStore, namespaceId] - ) - - return - {namespace?.parent?.__typename === "Organization" && ( - <> - - - - - - - - )} - - - - -} \ No newline at end of file diff --git a/src/packages/core/src/components/InputDialog.tsx b/src/packages/core/src/components/InputDialog.tsx new file mode 100644 index 00000000..3533930f --- /dev/null +++ b/src/packages/core/src/components/InputDialog.tsx @@ -0,0 +1,83 @@ +"use client" + +import React from "react"; +import { + Button, + Card, + Dialog, + DialogClose, + DialogContent, + DialogOverlay, + DialogPortal, + ScrollArea, + ScrollAreaScrollbar, + ScrollAreaThumb, + ScrollAreaViewport, + Spacing, + Text +} from "@code0-tech/pictor"; +import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout"; +import {IconArrowLeft} from "@tabler/icons-react"; + +export interface InputDialogProps { + open?: boolean + onOpenChange?: (open: boolean) => void + title?: string + description?: string + children?: React.ReactElement[] +} + +export const InputDialog: React.FC = (props) => { + + const {open, onOpenChange, title, description, children} = props + + return onOpenChange?.(open)}> + + + + + + {title} + + + + {description} + + + + +
    + }> + + + +
    + {children!} +
    +
    + + + +
    +
    + + + + + +} \ No newline at end of file diff --git a/src/packages/core/src/components/SettingDialog.tsx b/src/packages/core/src/components/SettingDialog.tsx new file mode 100644 index 00000000..28dfef32 --- /dev/null +++ b/src/packages/core/src/components/SettingDialog.tsx @@ -0,0 +1,85 @@ +"use client" + +import React from "react"; +import { + Button, + Card, + Dialog, + DialogClose, + DialogContent, + DialogOverlay, + DialogPortal, ScrollArea, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport, + Spacing, + Text +} from "@code0-tech/pictor"; +import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout"; +import {IconArrowLeft} from "@tabler/icons-react"; +import {Tab} from "@code0-tech/pictor/dist/components/tab/Tab"; + +export interface SettingDialogProps { + open?: boolean + onOpenChange?: (open: boolean) => void + title?: string + description?: string + trigger?: React.ReactElement[] | React.ReactElement + children?: React.ReactElement[] | React.ReactElement +} + +export const SettingDialog: React.FC = (props) => { + + const {open, onOpenChange, title, description, trigger, children} = props + + return onOpenChange?.(open)}> + + + + + + + {title} + + + + {description} + + + {trigger} + + + +
    + }> + + + +
    + {children!} +
    +
    + + + +
    +
    + + + + + + +} \ No newline at end of file diff --git a/src/packages/ee/src/application/components/ApplicationLicensesDataTableRowComponent.tsx b/src/packages/ee/src/application/components/ApplicationLicensesDataTableRowComponent.tsx index c2d75528..52061578 100644 --- a/src/packages/ee/src/application/components/ApplicationLicensesDataTableRowComponent.tsx +++ b/src/packages/ee/src/application/components/ApplicationLicensesDataTableRowComponent.tsx @@ -4,7 +4,7 @@ import {Badge, Button, DataTableColumn, Flex, Progress, Spacing, Text, useServic import {ApplicationService} from "@ee-internal/application/services/Application.service"; import {formatDistanceToNow, isFuture, isPast} from "date-fns"; import {IconX} from "@tabler/icons-react"; -import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {toast} from "@code0-tech/pictor/dist/components/toast/Toast"; export interface LicensesDataTableRowComponentProps { licenseId: License['id'] @@ -27,9 +27,7 @@ export const ApplicationLicensesDataTableRowComponent: React.FC { if ((payload?.errors?.length ?? 0) <= 0) { - addIslandSuccessNotification({ - message: "Deleted license" - }) + toast({title: "Deleted license", color: "success"}) } }) }, []) diff --git a/src/packages/ee/src/application/views/ApplicationLicensesTabTriggerView.tsx b/src/packages/ee/src/application/views/ApplicationLicensesTabTriggerView.tsx new file mode 100644 index 00000000..6b68d184 --- /dev/null +++ b/src/packages/ee/src/application/views/ApplicationLicensesTabTriggerView.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import {Button, Text} from "@code0-tech/pictor"; +import {TabTrigger} from "@code0-tech/pictor/dist/components/tab/Tab"; +import {IconLicense} from "@tabler/icons-react"; + +export const ApplicationLicensesTabTriggerView: React.FC = () => { + return + + +} diff --git a/src/packages/ee/src/application/views/ApplicationTabListView.tsx b/src/packages/ee/src/application/views/ApplicationTabListView.tsx deleted file mode 100644 index 16f01624..00000000 --- a/src/packages/ee/src/application/views/ApplicationTabListView.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from "react"; -import {TabList, TabTrigger} from "@code0-tech/pictor/dist/components/tab/Tab"; -import {Button, Text} from "@code0-tech/pictor"; - -export const ApplicationTabListView: React.FC = () => { - return - - - - - - - - - - -} \ No newline at end of file diff --git a/src/packages/ee/src/license/pages/LicenseAddPage.tsx b/src/packages/ee/src/license/pages/LicenseAddPage.tsx index a0b36f6a..d788790c 100644 --- a/src/packages/ee/src/license/pages/LicenseAddPage.tsx +++ b/src/packages/ee/src/license/pages/LicenseAddPage.tsx @@ -26,7 +26,7 @@ import {IconFileInfoFilled, IconX} from "@tabler/icons-react"; import {FileUploadFileChangeDetails} from "@ark-ui/react"; import {ApplicationService} from "@ee-internal/application/services/Application.service"; import {useRouter} from "next/navigation"; -import {addIslandSuccessNotification} from "@code0-tech/pictor/dist/components/island/Island.hook"; +import {toast} from "@code0-tech/pictor/dist/components/toast/Toast"; export const LicenseAddPage: React.FC = () => { @@ -51,9 +51,7 @@ export const LicenseAddPage: React.FC = () => { data: fileContent ?? "" }).then(payload => { if ((payload?.errors?.length ?? 0) <= 0) { - addIslandSuccessNotification({ - message: "Added license" - }) + toast({title: "Added license", color: "success"}) router.push("/") } })