diff --git a/.gitignore b/.gitignore index d5cc564d..6c32bf9d 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,7 @@ bin/ # Local e2e environment artefacts (Kind kubeconfigs, etc.) tmp/ + +# UI consumer/provider plugins (own package.json/build per plugin) +ui/**/node_modules/ +ui/**/dist/ diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 00000000..fc95b190 --- /dev/null +++ b/ui/README.md @@ -0,0 +1,14 @@ +# UI + +Frontend code for the compute service, split by which side of the +[Portal Plugin System](https://github.com/datum-cloud/cloud-portal/blob/main/docs/enhancements/portal-plugin-system.md) +it belongs to: + +- **[`consumer/`](./consumer)** — portal plugins: standalone Module Federation + remotes that the cloud portal discovers and loads at runtime to render + compute's UI inside the portal shell. See + [`consumer/workloads`](./consumer/workloads) for the reference plugin. +- **`provider/`** — reserved for compute-authored UI building blocks (e.g. + shared components or data hooks) meant to be consumed by other services' + plugins or by cloud-portal itself, rather than rendered directly. Empty for + now; add code here once there's a concrete cross-service UI dependency. diff --git a/ui/consumer/workloads/README.md b/ui/consumer/workloads/README.md new file mode 100644 index 00000000..5b67e49a --- /dev/null +++ b/ui/consumer/workloads/README.md @@ -0,0 +1,156 @@ +# Compute Portal Plugin + +A read-only operational dashboard for the compute service (`compute.datumapis.com`), +shipped as a **Module Federation remote** that the cloud portal loads at +runtime — the [Portal Plugin System](https://github.com/datum-cloud/cloud-portal/blob/main/docs/enhancements/portal-plugin-system.md). +Structural template: [`examples/sample-plugin/`](https://github.com/datum-cloud/cloud-portal/tree/main/examples/sample-plugin) +in the `cloud-portal` repo. + +## Scope — v1 is read-only + +This is a **CLI-first** service: workload creation, deployment, scaling, +restarts, and deletion all happen via `datumctl compute …`. The plugin's job +is to give operators visibility into what's already running: + +- **Workloads** (`workloads`) — list, with health, ready count, placements, + age. +- **Workload detail** (`workloads/:workloadName`) — Overview tab only: stat + tiles, configuration, and an embedded running-instances table. +- **Instance detail** (`workloads/:workloadName/instances/:instanceName`) — + Overview tab only: stat tiles (uptime, region, instance type, image), + network card, health conditions, runtime config. + +No deploy/edit/delete forms, and no Activity/Metrics/Settings tabs in v1 — +those stay out of scope until there's a real telemetry/activity source to back +them. `CliBanner`/`SectionCard` (in `src/components/cli-section.tsx`) point +users at the equivalent `datumctl` commands wherever the portal can't do +something itself. + +## Relationship to cloud-portal PR #1315 + +An earlier, unmerged cloud-portal PR (#1315) built this same dashboard as +**native portal routes**. This plugin ports that UI (Zod schemas, adapters, +JSX) into a standalone remote living in this repo instead, rewriting the +parts that depended on portal internals: + +| PR #1315 (portal-internal) | This plugin | +|---|---| +| Generated SDK (`@/modules/control-plane/compute`) | Plain `fetch()` against `/api/proxy/…` (`src/lib/api.ts`) | +| `useResourceWatch` (watch-stream) | `useQuery` with `refetchInterval` (~10s) polling | +| `runDetailLoader`/`runListLoader` server-side RBAC gate | Client-side catch of a non-ok/403 `fetch()` response → inline restricted state | +| `paths.config.ts` + `getPathWithParams` | `useParams()` / `useLocation()` / `useNavigate()` from the shared `react-router` singleton | + +`workload.schema.ts`/`instance.schema.ts` (combined into `src/schema.ts`), +`workload.adapter.ts`/`instance.adapter.ts` (combined into `src/adapter.ts`), +`cli-section.tsx`, and `compute.helper.ts` (as `src/lib/format.ts`) ported +over near-verbatim — they were already free of portal internals (aside from +one hook swap: `@/hooks/useCopyToClipboard` → `@datum-cloud/datum-ui/hooks`). + +The activity-feed link-resolver changes in PR #1315 +(`activity-link-resolvers.ts`/`kinds.ts`) are cloud-portal-side and out of +scope here — a small follow-up for that repo, not this plugin. + +## Data fetching — every call goes through Milo + +The browser only ever calls the portal origin; the portal mediates all data. +Every API call goes through the portal's existing authenticated proxy at +`/api/proxy/apis/resourcemanager.miloapis.com/v1alpha1/projects//control-plane/…`, +reaching the compute aggregated apiserver the same way as any other Milo +resource: + +``` +GET .../control-plane/apis/compute.datumapis.com/v1alpha/namespaces/default/workloads +GET .../control-plane/apis/compute.datumapis.com/v1alpha/namespaces/default/instances?labelSelector=compute.datumapis.com/workload-name= +``` + +Note the API group version is **`v1alpha`**, not `v1alpha1` — verified against +`api/v1alpha/groupversion_info.go` in this repo. + +Data fetching uses `@tanstack/react-query` (a host-shared singleton), so +plugin queries live in the host's cache next to built-in pages. This plugin +does **not** create its own `QueryClient`. In place of PR #1315's watch-stream +hook (`useResourceWatch`, portal-internal and unavailable to plugins), each +query hook in `src/lib/api.ts` sets `refetchInterval: 10_000` for live-ish +updates — a deliberate v1 trade-off, not equivalent to a real watch. + +### RBAC — known v1 gap + +PR #1315 used a server-side loader for per-resource permission checks with a +friendly "restricted" page on 403. A plugin has no server loader, so this +plugin instead catches a non-ok `fetch()` response in `src/lib/api.ts` +(`ApiError`, carrying the HTTP status) and each page renders an inline +"Access restricted" card on `status === 403` (see +`src/components/states.tsx`). This is coarser-grained than PR #1315's +version — accepted for v1. + +## This is its own project + +`ui/consumer/workloads/` has its **own** `package.json` and lockfile, +independent of the rest of this repo. It never touches the repo's Makefile, +CI, or other root files. Install and run it in isolation: + +```bash +cd ui/consumer/workloads +bun install +bun run dev # Vite dev server (:7778, HMR) — standalone/direct +bun run preview # built dist/ served (:7778) — proxy-safe +bun run build # static dist/ (remoteEntry.js + plugin-manifest.json + chunks) +bun run typecheck # tsc --noEmit +``` + +### Serve `dev` or `preview`? (it matters for the portal proxy) + +The portal loads plugin assets through its **same-origin asset proxy** +(`/api/plugins//…`), never directly from `:7778`. That changes which +server you want running: + +- **`preview`** (built) — remote-entry/chunk imports are **proxy-relative**, + so they load correctly through the portal proxy. Use this whenever the + portal will load the plugin. +- **`dev`** (Vite, HMR) — the dev remote entry imports host-absolute URLs + that 404 through the proxy. Use this only for the standalone preview at + `http://localhost:7778/` (direct, no proxy). + +## How it fits the contract + +`public/plugin-manifest.json` declares: + +- **`portal.nav/project`** — a "Workloads" sidebar item (icon `server`), + gated on `{compute.datumapis.com, workloads, list}`. +- **`portal.page/project`** ×3 — `workloads` (`WorkloadList`), + `workloads/:workloadName` (`WorkloadDetail`), and + `workloads/:workloadName/instances/:instanceName` (`InstanceDetail`, gated + on `{compute.datumapis.com, instances, get}`). + +Three things stay in lockstep, same as the sample plugin: + +1. MF container `name` (`vite.config.ts`) = manifest `name` = the + `PortalPlugin` metadata.name (`workload.compute.datumapis.com`). +2. `filename` (`vite.config.ts`) = manifest `remoteEntry` (`remoteEntry.js`). +3. MF `exposes` keys = manifest `exposedModules` keys = the `$codeRef` values. + +### Shared singletons + +The host provides `react`, `react-dom`, `react-router`, and +`@tanstack/react-query` as shared singletons (host's exact instances — see +`vite.config.ts`). This plugin bundles its own copy of `@datum-cloud/datum-ui` +(not shared as a singleton in v1), so its CSS ships with the bundle. + +## File layout + +``` +ui/consumer/workloads/ + vite.config.ts MF container "workload.compute.datumapis.com" + public/plugin-manifest.json + src/ + lib/api.ts fetch wrappers + useQuery hooks (workloads/instances) + lib/format.ts formatUptime / splitSlashValue (ported as-is) + schema.ts Zod schemas for Workload / Instance + adapter.ts raw K8s JSON → schema mappers + components/cli-section.tsx CommandBlock / SectionCard / CliBanner + components/states.tsx Loading / Error / Restricted state components + pages/workload-list.tsx + pages/workload-detail.tsx + pages/instance-detail.tsx + main.tsx standalone preview harness only +``` diff --git a/ui/consumer/workloads/bun.lock b/ui/consumer/workloads/bun.lock new file mode 100644 index 00000000..3d296ca2 --- /dev/null +++ b/ui/consumer/workloads/bun.lock @@ -0,0 +1,512 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "@datum-cloud/compute-portal-plugin", + "dependencies": { + "@datum-cloud/datum-ui": "^1.4.0", + "@tanstack/react-query": "5.101.0", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", + "lucide-react": "^1.21.0", + "react": "19.2.7", + "react-dom": "19.2.7", + "react-router": "7.18.0", + "sonner": "^2.0.7", + "zod": "^4.4.3", + }, + "devDependencies": { + "@module-federation/vite": "^1.16.16", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.2.0", + "typescript": "^6.0.3", + "vite": "^7.3.5", + }, + }, + }, + "packages": { + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.11", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@csstools/css-calc": "^3.2.0", "@csstools/css-color-parser": "^4.1.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg=="], + + "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.1.1", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1" } }, "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ=="], + + "@asamuzakjp/generational-cache": ["@asamuzakjp/generational-cache@1.0.1", "", {}, "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg=="], + + "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="], + + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], + + "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], + + "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], + + "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], + + "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="], + + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="], + + "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], + + "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], + + "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="], + + "@csstools/color-helpers": ["@csstools/color-helpers@6.1.0", "", {}, "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg=="], + + "@csstools/css-calc": ["@csstools/css-calc@3.3.0", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ=="], + + "@csstools/css-color-parser": ["@csstools/css-color-parser@4.1.10", "", { "dependencies": { "@csstools/color-helpers": "^6.1.0", "@csstools/css-calc": "^3.3.0" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw=="], + + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="], + + "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.7", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig=="], + + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="], + + "@datum-cloud/datum-ui": ["@datum-cloud/datum-ui@1.6.0", "", { "dependencies": { "@radix-ui/react-avatar": "^1.2.0", "@radix-ui/react-checkbox": "^1.3.5", "@radix-ui/react-collapsible": "^1.1.14", "@radix-ui/react-dialog": "^1.1.17", "@radix-ui/react-dropdown-menu": "^2.1.18", "@radix-ui/react-hover-card": "^1.1.17", "@radix-ui/react-label": "^2.1.10", "@radix-ui/react-popover": "^1.1.17", "@radix-ui/react-radio-group": "^1.4.1", "@radix-ui/react-select": "^2.3.1", "@radix-ui/react-separator": "^1.1.10", "@radix-ui/react-slot": "^1.3.0", "@radix-ui/react-switch": "^1.3.1", "@radix-ui/react-tabs": "^1.1.15", "@radix-ui/react-tooltip": "^1.2.10", "@radix-ui/react-visually-hidden": "^1.2.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "isomorphic-dompurify": "^3.18.0", "tailwind-merge": "^3.6.0", "tw-animate-css": "^1.4.0" }, "peerDependencies": { "@conform-to/react": ">=1.19.4 <2", "@conform-to/zod": ">=1.19.4 <2", "@dnd-kit/core": ">=6.3.1 <7", "@dnd-kit/sortable": ">=10 <11", "@hookform/resolvers": ">=5.4.0 <6", "@monaco-editor/react": ">=4.7.0 <5", "@stepperize/react": ">=7 <8", "@streamdown/code": ">=1 <2", "@tanstack/react-table": ">=8.21.3 <9", "@tanstack/react-virtual": ">=3.14.3 <4", "@tiptap/extension-character-count": ">=3.27.1 <4", "@tiptap/extension-link": ">=3.27.1 <4", "@tiptap/extension-placeholder": ">=3.27.1 <4", "@tiptap/extension-underline": ">=3.27.1 <4", "@tiptap/react": ">=3.27.1 <4", "@tiptap/starter-kit": ">=3.27.1 <4", "ai": ">=7 <8", "date-fns": ">=4.1.0 <5", "date-fns-tz": ">=3 <4", "js-yaml": ">=5 <6", "leaflet": ">=1.9 <2", "leaflet-draw": ">=1 <2", "leaflet.fullscreen": ">=5 <6", "leaflet.markercluster": ">=1.5.3 <2", "lucide-react": ">=1 <2", "monaco-editor": ">=0.44.0 <1", "motion": ">=12 <13", "nprogress": ">=0.2 <1", "nuqs": ">=2 <3", "react": ">=19.2.7 <20", "react-day-picker": ">=10 <11", "react-dom": ">=19.2.7 <20", "react-dropzone": ">=15 <16", "react-hook-form": ">=7.80.0 <8", "react-leaflet": ">=5 <6", "react-leaflet-markercluster": ">=5.0.0-rc.0 <6", "react-number-format": ">=5.4.5 <6", "recharts": ">=3 <4", "sonner": ">=2.0.7 <3", "streamdown": ">=2 <3", "zod": ">=4 <5" }, "optionalPeers": ["@conform-to/react", "@conform-to/zod", "@dnd-kit/core", "@dnd-kit/sortable", "@hookform/resolvers", "@monaco-editor/react", "@stepperize/react", "@streamdown/code", "@tanstack/react-table", "@tanstack/react-virtual", "@tiptap/extension-character-count", "@tiptap/extension-link", "@tiptap/extension-placeholder", "@tiptap/extension-underline", "@tiptap/react", "@tiptap/starter-kit", "ai", "date-fns", "date-fns-tz", "js-yaml", "leaflet", "leaflet-draw", "leaflet.fullscreen", "leaflet.markercluster", "monaco-editor", "motion", "nprogress", "nuqs", "react-day-picker", "react-dropzone", "react-hook-form", "react-leaflet", "react-leaflet-markercluster", "react-number-format", "recharts", "sonner", "streamdown", "zod"] }, "sha512-pezWJg3O9Qug5te44S5mF8o4BVeopUZnNPysXewPIbKBxzo06gv6+ylpoIeOxojEwFFTlkL6s8Jk0K3yrBG2RA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], + + "@exodus/bytes": ["@exodus/bytes@1.15.1", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q=="], + + "@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.9", "", { "dependencies": { "@floating-ui/dom": "^1.8.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@module-federation/dts-plugin": ["@module-federation/dts-plugin@2.8.0", "", { "dependencies": { "@module-federation/error-codes": "2.8.0", "@module-federation/managers": "2.8.0", "@module-federation/sdk": "2.8.0", "@module-federation/third-party-dts-extractor": "2.8.0", "adm-zip": "0.5.10", "isomorphic-ws": "5.0.0", "undici": "7.28.0", "ws": "8.21.0" }, "peerDependencies": { "typescript": "^4.9.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "vue-tsc": ">=1.0.24" }, "optionalPeers": ["vue-tsc"] }, "sha512-defjq4jOWMEfeejezPWLP5sc8kw0O6FqTT7/E5rbZPEVyjB1A0U3ynhW6GDE5/6hk9/TzdbWS+fBNi4MqUOY6Q=="], + + "@module-federation/error-codes": ["@module-federation/error-codes@2.8.0", "", {}, "sha512-Gaog9904EmxYOQV0hli3XQ7jXeFaADfh5bnBtTCtbZ37Qd/Sz9kQfd+gYQRyIj7RGmkv9DPiN/SsmrTMrTymKw=="], + + "@module-federation/managers": ["@module-federation/managers@2.8.0", "", { "dependencies": { "@module-federation/sdk": "2.8.0" } }, "sha512-SnVBCwmi962WGg6hLFElxZUCnrRJdR6glE2ZKPBY/iK07AHUN2ZxuaBCBsVzyws+xLZGHZxBHmVstijTh8dSUA=="], + + "@module-federation/runtime": ["@module-federation/runtime@2.8.0", "", { "dependencies": { "@module-federation/error-codes": "2.8.0", "@module-federation/runtime-core": "2.8.0", "@module-federation/sdk": "2.8.0" } }, "sha512-cGtUBQ1/TVy7KrXy6xPgy3FEmOGyIYkBA2T4iGH3ZH5PNPPTmqN9jF2AfneTSOj0RtBr7Pxq3CUt81E/UCvK1A=="], + + "@module-federation/runtime-core": ["@module-federation/runtime-core@2.8.0", "", { "dependencies": { "@module-federation/error-codes": "2.8.0", "@module-federation/sdk": "2.8.0" } }, "sha512-Tf98+epGGiPSHqmQHuXa2uXZMMvjGf1IqJDR1/FpXfmobv5ECN0mGZCjUHGNSyxvoDyXKIkKwJu7IwEoh0ouQA=="], + + "@module-federation/sdk": ["@module-federation/sdk@2.8.0", "", {}, "sha512-yBP+9+0Z8nlvKEXAZS3AsQVy7bFbZf8eMivGk4q4ZdwG3TsLMlsPjb1dQb2i7gcAG6ux9y2LWLkj/0LVk74cnQ=="], + + "@module-federation/third-party-dts-extractor": ["@module-federation/third-party-dts-extractor@2.8.0", "", {}, "sha512-nAMlr74OKIylkfRwlunOhytQbmsgb3gCqdXWnPQhG+ZtqWXGELLfMT4a1Q1ht3cS+sRpWj2SZRqK2M7GadI6tA=="], + + "@module-federation/vite": ["@module-federation/vite@1.19.1", "", { "dependencies": { "@module-federation/dts-plugin": "2.8.0", "@module-federation/runtime": "2.8.0", "@module-federation/sdk": "2.8.0" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-f1iVNfyvFztTrm6wAoBuVN3kxT0Wx3Zi0n82O50LXmPo5/LUPMGCsBpZAHA3WyMW3UXoDCBStKscVHksEHF7pA=="], + + "@radix-ui/number": ["@radix-ui/number@1.1.3", "", {}, "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA=="], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.7", "", {}, "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q=="], + + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.13", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.8" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0Q310knIY0K+mkmncU9FxLggfW7V49Ok2oY+iu27KkERTsQXxYCIC8XW5QoK4w7Jp1z00vT5xj3oxTcn7QlcTg=="], + + "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.2.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-context": "1.2.1", "@radix-ui/react-primitive": "2.1.8", "@radix-ui/react-use-callback-ref": "1.1.3", "@radix-ui/react-use-is-hydrated": "0.1.2", "@radix-ui/react-use-layout-effect": "1.1.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-u4Sz6ajq2WobGMqbsNXnFI/3JP/o7TUBkEmQ4BRUYofPDQawdO3WWYy5907c9VgOFX+ETjKI3VxnnrKYAF7Pmg=="], + + "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.9", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.4", "@radix-ui/react-context": "1.2.1", "@radix-ui/react-presence": "1.1.9", "@radix-ui/react-primitive": "2.1.8", "@radix-ui/react-use-controllable-state": "1.2.5", "@radix-ui/react-use-size": "1.1.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YPXZlqjrKVqKtVD2F828CApHwu+PEu30i7x0Y9gg6SbQ3fJbcaQlDkGPT0gW6HjxoCjAEt8uJ494F1S61KpIjQ=="], + + "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.18", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.4", "@radix-ui/react-context": "1.2.1", "@radix-ui/react-id": "1.1.3", "@radix-ui/react-presence": "1.1.9", "@radix-ui/react-primitive": "2.1.8", "@radix-ui/react-use-controllable-state": "1.2.5", "@radix-ui/react-use-layout-effect": "1.1.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aNMp8humfYzWERz5gxGb/eLNkB0ZthypLbNlNolzdgzFTeN78e/rnn9rxCdOaSsg/zClTOJ/OkjXYx9PUEPATw=="], + + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.13", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.4", "@radix-ui/react-context": "1.2.1", "@radix-ui/react-primitive": "2.1.8", "@radix-ui/react-slot": "1.3.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Q8xYqNRFObXPmi45bFSkGdM2JA5hjBeYGFqzg6lZR3wp6b+cciraVq8DdneTabtws+uOYqjmsvmKOufk/M9Cyg=="], + + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pWJo6lQAfR6uy1n7ii7PaCc9dLPwTXDYbQpORZU5B548Aqvl2pP1SM1vJGKyxIFqZMHRopRO4CQYX2iXAIB5jA=="], + + "@radix-ui/react-context": ["@radix-ui/react-context@1.2.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EraVbFjiIjibpLr6EjvEDmSCYJU2SlKDMiO+qEK/D9GOWnQoAQlpQo2occGYC1UM9MBeEx5Bek3UtW/Qi57vAg=="], + + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.21", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.4", "@radix-ui/react-context": "1.2.1", "@radix-ui/react-dismissable-layer": "1.1.17", "@radix-ui/react-focus-guards": "1.1.5", "@radix-ui/react-focus-scope": "1.1.14", "@radix-ui/react-id": "1.1.3", "@radix-ui/react-portal": "1.1.15", "@radix-ui/react-presence": "1.1.9", "@radix-ui/react-primitive": "2.1.8", "@radix-ui/react-slot": "1.3.1", "@radix-ui/react-use-controllable-state": "1.2.5", "@radix-ui/react-use-layout-effect": "1.1.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-h+7qMDDmZJ8qTSPrwNyKb/PACY0ehtN8QOBlCz+C2C1jgehKekdhmHddG9YQk8BF/sHJqglPjte+jA1Jrp9HcA=="], + + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-OdgAA/xb6WOQMKNn0mtbYoruMG6YMaBOnq+evWxSpMmiEMBdeFZawnIWRfPPeVXCRm+0lnN8jpJLjhD7/UIxWw=="], + + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.17", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.4", "@radix-ui/react-primitive": "2.1.8", "@radix-ui/react-use-callback-ref": "1.1.3", "@radix-ui/react-use-effect-event": "0.0.4" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QAXwa38pG0xNAYh1pjdSaf86NrkqsMoDNmget/Y7X8O8E/C3Iqlj9GAPE4DfX9BPLXc7WH2TWSzMRnIoCdcjzQ=="], + + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.22", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.4", "@radix-ui/react-context": "1.2.1", "@radix-ui/react-id": "1.1.3", "@radix-ui/react-menu": "2.1.22", "@radix-ui/react-primitive": "2.1.8", "@radix-ui/react-use-controllable-state": "1.2.5" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wZRLTjZaJf1HTxx0YepFKZUuo+SZMi+Cr4kJ2fuA5FxDSUE0vHDTscahUaU+jfz6LFk4OuguwqJs5K0JsIsIBA=="], + + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.5", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-UQvlB7L/BYh3P8MLvwZnQkH521EDos40Rwnbt5+Qpg4Vbk0z3xJjRUmR6+aka4aT1IQQXFdO5bNPoE7cvFl5xQ=="], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.14", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.4", "@radix-ui/react-primitive": "2.1.8", "@radix-ui/react-use-callback-ref": "1.1.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/x4htnJfmW53MplkrePaDpf1o/rN1C++g88WpVobULXbSyC19NtLkXmewuJ/HCaceSmfKDNL5gOXcBGnuAvnvQ=="], + + "@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.21", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.4", "@radix-ui/react-context": "1.2.1", "@radix-ui/react-dismissable-layer": "1.1.17", "@radix-ui/react-popper": "1.3.5", "@radix-ui/react-portal": "1.1.15", "@radix-ui/react-presence": "1.1.9", "@radix-ui/react-primitive": "2.1.8", "@radix-ui/react-use-controllable-state": "1.2.5" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ByFLyi+kdTGkObxIt5wKB3Ru3rtJRuYUqPEtqiv1HNYqutqbn1wDuNmVzI6iwus9qfY3vwpNgvx/UVZUGTWC1A=="], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.3", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-f/Wxm0ctyMymUJK0fqTSQlm85rbzdAkoNbPXJQ5+6caowVO8Yx+NWGjGz/oGhs/D+WIbbQpOrU0hU2Li2/42xQ=="], + + "@radix-ui/react-label": ["@radix-ui/react-label@2.1.13", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.8" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kj7BXyM4uG0dyeoEdVmPQAOFN291+iJOtiGTvkXe1ijJWLQbAHpFNkopvqquaMVn+zjqTZRhnefESAinMErHKg=="], + + "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.22", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collection": "1.1.13", "@radix-ui/react-compose-refs": "1.1.4", "@radix-ui/react-context": "1.2.1", "@radix-ui/react-direction": "1.1.3", "@radix-ui/react-dismissable-layer": "1.1.17", "@radix-ui/react-focus-guards": "1.1.5", "@radix-ui/react-focus-scope": "1.1.14", "@radix-ui/react-id": "1.1.3", "@radix-ui/react-popper": "1.3.5", "@radix-ui/react-portal": "1.1.15", "@radix-ui/react-presence": "1.1.9", "@radix-ui/react-primitive": "2.1.8", "@radix-ui/react-roving-focus": "1.1.17", "@radix-ui/react-slot": "1.3.1", "@radix-ui/react-use-callback-ref": "1.1.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-xCFrVLttGDPwMjIPI6GS+tm3IGUNKsPXffrJQIPpQJuE9Qy1bU9GNUCkwranU/pEqyBlwMxcBIK/FUJFAZ9SqQ=="], + + "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.21", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.4", "@radix-ui/react-context": "1.2.1", "@radix-ui/react-dismissable-layer": "1.1.17", "@radix-ui/react-focus-guards": "1.1.5", "@radix-ui/react-focus-scope": "1.1.14", "@radix-ui/react-id": "1.1.3", "@radix-ui/react-popper": "1.3.5", "@radix-ui/react-portal": "1.1.15", "@radix-ui/react-presence": "1.1.9", "@radix-ui/react-primitive": "2.1.8", "@radix-ui/react-slot": "1.3.1", "@radix-ui/react-use-controllable-state": "1.2.5", "@radix-ui/react-use-layout-effect": "1.1.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+AJ7P5oRcFqFtv7MaCD8fOuK3l5qQBAMdWEOnUOYd3JfHOeIUe7TnHHSSP15CtWwH4KrotyDCTzodOIoONh8vw=="], + + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.3.5", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.13", "@radix-ui/react-compose-refs": "1.1.4", "@radix-ui/react-context": "1.2.1", "@radix-ui/react-primitive": "2.1.8", "@radix-ui/react-use-callback-ref": "1.1.3", "@radix-ui/react-use-layout-effect": "1.1.3", "@radix-ui/react-use-rect": "1.1.3", "@radix-ui/react-use-size": "1.1.3", "@radix-ui/rect": "1.1.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-6hLng2Rs45IZcvZ31BNvMeaFEMMEbtsog4xPcb8LZrGfyoV9fdAXqB9UKhLpTHtL0+E2DhSr6VW54Tehd6ksAQ=="], + + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.15", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.8", "@radix-ui/react-use-layout-effect": "1.1.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kAfBVJUKNNKZuyGQXXG6rKolAV2KAmxxVkPXJgoq9dEFTl39286RufHQFNTL8rzha4vP8159BJ6hMGpB+bqv7A=="], + + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.9", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-LTi1v05bprIb8/GSY/GWusI0jfsYjQ3CD3Nin8o7jVxnpHzVQfzjOQJoJTQkE9bdmOnsS7SFdhkXiBv8PrYnxw=="], + + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.8", "", { "dependencies": { "@radix-ui/react-slot": "1.3.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-DOlK1BdcIeYYUcFkSYFka4v1h95XTov93b0jCgW1EEiZuIhdwHY2NlE1teLIh+p0uBsuZI5A+voay+iVWpprfA=="], + + "@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.4.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.4", "@radix-ui/react-context": "1.2.1", "@radix-ui/react-direction": "1.1.3", "@radix-ui/react-presence": "1.1.9", "@radix-ui/react-primitive": "2.1.8", "@radix-ui/react-roving-focus": "1.1.17", "@radix-ui/react-use-controllable-state": "1.2.5", "@radix-ui/react-use-size": "1.1.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-BM1xQgw1gpgPSw05U4hJCMDX9GmAS6b6vKZANuNQZo0vf/ah7NZDAnjLd4tPg3z2vMvlI0CV+B2CFM9Z19oQDA=="], + + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.17", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collection": "1.1.13", "@radix-ui/react-compose-refs": "1.1.4", "@radix-ui/react-context": "1.2.1", "@radix-ui/react-direction": "1.1.3", "@radix-ui/react-id": "1.1.3", "@radix-ui/react-primitive": "2.1.8", "@radix-ui/react-use-callback-ref": "1.1.3", "@radix-ui/react-use-controllable-state": "1.2.5", "@radix-ui/react-use-is-hydrated": "0.1.2", "@radix-ui/react-use-layout-effect": "1.1.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YdJmjETw7Py+4Nziv71jgZ6fH7xQgB5p+pQ5rV8Iufyrm0RHSijec/UV1zPfQoK/YcfzB9nOl/2bi1zf1SjRMQ=="], + + "@radix-ui/react-select": ["@radix-ui/react-select@2.3.5", "", { "dependencies": { "@radix-ui/number": "1.1.3", "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collection": "1.1.13", "@radix-ui/react-compose-refs": "1.1.4", "@radix-ui/react-context": "1.2.1", "@radix-ui/react-direction": "1.1.3", "@radix-ui/react-dismissable-layer": "1.1.17", "@radix-ui/react-focus-guards": "1.1.5", "@radix-ui/react-focus-scope": "1.1.14", "@radix-ui/react-id": "1.1.3", "@radix-ui/react-popper": "1.3.5", "@radix-ui/react-portal": "1.1.15", "@radix-ui/react-presence": "1.1.9", "@radix-ui/react-primitive": "2.1.8", "@radix-ui/react-slot": "1.3.1", "@radix-ui/react-use-callback-ref": "1.1.3", "@radix-ui/react-use-controllable-state": "1.2.5", "@radix-ui/react-use-layout-effect": "1.1.3", "@radix-ui/react-use-previous": "1.1.3", "@radix-ui/react-visually-hidden": "1.2.9", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-6CQx0xtdarAkSCsqgLJaz+coYQaru7rt8K6gMItR2rA+WP8/Ig6kRAzP6Zw88RccG98IZnszqJlJx9hejYOL3A=="], + + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.13", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.8" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wbagLhy4grekqiT6hU0WNP09FpmemTXbNWg4QEawaw9dix7hRIcrmDNnLDasOsKVLtArEznhgZKe5Gm4U6oPXQ=="], + + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.1", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Bu/aAQHFFh6/QAvXAeUMurJ9fbW0JUIqlojU/yBXZ7cAVqy75Y7JYYyuCr9zLNF0p4WWoJYV54CTUIf4l7FzTw=="], + + "@radix-ui/react-switch": ["@radix-ui/react-switch@1.3.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.4", "@radix-ui/react-context": "1.2.1", "@radix-ui/react-primitive": "2.1.8", "@radix-ui/react-use-controllable-state": "1.2.5", "@radix-ui/react-use-size": "1.1.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-e1YrvGvEjIqDhqKjQ4Q+IDkMfiY30u3FPz9CZXZMlQ8mU3kZ2PdeBM8Gssc48CDopPizNorKumGhjySBfO7BDQ=="], + + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-context": "1.2.1", "@radix-ui/react-direction": "1.1.3", "@radix-ui/react-id": "1.1.3", "@radix-ui/react-presence": "1.1.9", "@radix-ui/react-primitive": "2.1.8", "@radix-ui/react-roving-focus": "1.1.17", "@radix-ui/react-use-controllable-state": "1.2.5" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4wbGf98pX2ZwYqHMVB/dakeMPyH4xSJ9MSDaCCWXlIpYt0VYY6mtZxKSTiGS/F3tk0yrEYmXE1V8uwG7TxDCcQ=="], + + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.4", "@radix-ui/react-context": "1.2.1", "@radix-ui/react-dismissable-layer": "1.1.17", "@radix-ui/react-id": "1.1.3", "@radix-ui/react-popper": "1.3.5", "@radix-ui/react-portal": "1.1.15", "@radix-ui/react-presence": "1.1.9", "@radix-ui/react-primitive": "2.1.8", "@radix-ui/react-slot": "1.3.1", "@radix-ui/react-use-controllable-state": "1.2.5", "@radix-ui/react-use-layout-effect": "1.1.3", "@radix-ui/react-visually-hidden": "1.2.9" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-C/JxCKJJac+wtHPW1yFBSN8Ssuaufy2jYQMgyiJYyW4Fw0WJfDWXQDAly1qsbd1YDznH+sYitaljhf8igPCvmA=="], + + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-AUS7HoBBAncIsGMLNG+CcpLuJ+JIBbZzmyM8Qdb1eIThX0AlhSSC6wn40xfBlPE+ypx/vSSiRWnklUAjy3U3UA=="], + + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-use-effect-event": "0.0.4", "@radix-ui/react-use-layout-effect": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-UB1dXpxvHjR48poyKdKdTm7jT0kp3elkUKdKQiOkirlbYumqXinSJtrjDsr9maXNPvL12bKI4CDSmydms/9Aeg=="], + + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.4", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-XYcfa6wlXDCwQtePuEiPmXLSAhGL4DWtedSyRgGbG3y10mw+OnrLp6SyeY1gJFMiYF0Dx0nMAX9InylKbLEFQQ=="], + + "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2+gAVu9uaSLbopCTvvuWX9MIgEOlyqXC1Ok+KLCO7cZPSHLENs7dL0KbFQUGFIcFKmDW/bmaQRJtd8E3cnMRUw=="], + + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rDiah9wvtqihWtWz02XreeRKIxt2EJF8y5D9rtY9l5A2zxePAtcPiOMpDugNRw5bFHz+1/8viVoc7ZVKiJknCw=="], + + "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kbefNKla5AGhgvMqrg/qS7mYVLXTEHQLqoFIS03nKN2dAfQQ5PK5Tp+2LxS7BV6a6l4HHv4jTg6rtV2Uajwe2w=="], + + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.3", "", { "dependencies": { "@radix-ui/rect": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-W0GSYZFKEfi6raMiMEfJSngvVbFDIyxtW5JuVg5NoQBY59l0dVQVGLbhog/tOdwq0qtZ+TXuX1ikSNan4/IZXA=="], + + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.3", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jJq6tQQLvO/z4uWbztjwPV+1a/+H4rCaWypasLB/ac8DEdd6+p7dIm7o3F6P2KZPGkPMqD4s5424EdAdoU+GLA=="], + + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.8" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-seuZXNZVCz1kLSQMRO/TdNOSahBI3S5nXE4jzO7u7aFRWQlMAnwyrr8vKMkEU115fCLEyzR2YyjnP+aRMxK34w=="], + + "@radix-ui/rect": ["@radix-ui/rect@1.1.3", "", {}, "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.2", "", { "os": "android", "cpu": "arm" }, "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.2", "", { "os": "none", "cpu": "arm64" }, "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA=="], + + "@tanstack/query-core": ["@tanstack/query-core@5.101.0", "", {}, "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.101.0", "", { "dependencies": { "@tanstack/query-core": "5.101.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], + + "adm-zip": ["adm-zip@0.5.10", "", {}, "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ=="], + + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.1", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A=="], + + "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], + + "browserslist": ["browserslist@4.28.7", "", { "dependencies": { "baseline-browser-mapping": "^2.10.44", "caniuse-lite": "^1.0.30001806", "electron-to-chromium": "^1.5.393", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001806", "", {}, "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw=="], + + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="], + + "date-fns": ["date-fns@4.4.0", "", {}, "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w=="], + + "date-fns-tz": ["date-fns-tz@3.2.0", "", { "peerDependencies": { "date-fns": "^3.0.0 || ^4.0.0" } }, "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], + + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + + "dompurify": ["dompurify@3.4.12", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.395", "", {}, "sha512-7zt9Aw+SrmxLWLN0zhaTWZQiCdryLVrYTq5R7iZakLvi2UQPYMMsROYV/2qVCzMeCiSXHwKOU+sZ4zOVVlrtKA=="], + + "entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="], + + "esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + + "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], + + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + + "isomorphic-dompurify": ["isomorphic-dompurify@3.19.0", "", { "dependencies": { "dompurify": "^3.4.12", "jsdom": "^29.1.1" } }, "sha512-ynZ/6cVv4Trpt1FEd5TkHsHtLs+y/YQGb+ocIIVRdc+DkiYRbxlEL/IiSeSeQXwopQcG5FhoeIUJa1l26IgSpw=="], + + "isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsdom": ["jsdom@29.1.1", "", { "dependencies": { "@asamuzakjp/css-color": "^5.1.11", "@asamuzakjp/dom-selector": "^7.1.1", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.3", "@exodus/bytes": "^1.15.0", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.3.5", "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.1", "undici": "^7.25.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "lucide-react": ["lucide-react@1.26.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-raglYVR2+VkMfJL158krjVmE+rV5ST2lzA/KQm1FRSjMHT4MnWaegHxoVEpmc2So3nOEhp9oGejJwAPX8MoAjg=="], + + "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], + + "node-releases": ["node-releases@2.0.51", "", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="], + + "parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + + "postcss": ["postcss@8.5.22", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], + + "react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], + + "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], + + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "react-router": ["react-router@7.18.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ=="], + + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "rollup": ["rollup@4.62.2", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="], + + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + + "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + + "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "tldts": ["tldts@7.4.9", "", { "dependencies": { "tldts-core": "^7.4.9" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA=="], + + "tldts-core": ["tldts-core@7.4.9", "", {}, "sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg=="], + + "tough-cookie": ["tough-cookie@6.0.2", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA=="], + + "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], + + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + + "undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + + "vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="], + + "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + + "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], + + "whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="], + + "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + + "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], + + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + + "jsdom/lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], + } +} diff --git a/ui/consumer/workloads/index.html b/ui/consumer/workloads/index.html new file mode 100644 index 00000000..3689dd6b --- /dev/null +++ b/ui/consumer/workloads/index.html @@ -0,0 +1,19 @@ + + + + + + Compute Portal Plugin — standalone preview + + + +
+ + + diff --git a/ui/consumer/workloads/package.json b/ui/consumer/workloads/package.json new file mode 100644 index 00000000..00c37220 --- /dev/null +++ b/ui/consumer/workloads/package.json @@ -0,0 +1,33 @@ +{ + "name": "@datum-cloud/compute-portal-plugin", + "private": true, + "version": "0.1.0", + "type": "module", + "description": "Compute service Portal Plugin: read-only workload/instance operational dashboard, a Module Federation remote reading data exclusively through the portal's Milo control-plane proxy. Workload creation stays in datumctl (CLI-first).", + "scripts": { + "dev": "vite", + "preview": "vite preview", + "build": "vite build", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@datum-cloud/datum-ui": "^1.4.0", + "@tanstack/react-query": "5.101.0", + "lucide-react": "^1.21.0", + "react": "19.2.7", + "react-dom": "19.2.7", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", + "react-router": "7.18.0", + "sonner": "^2.0.7", + "zod": "^4.4.3" + }, + "devDependencies": { + "@module-federation/vite": "^1.16.16", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.2.0", + "typescript": "^6.0.3", + "vite": "^7.3.5" + } +} diff --git a/ui/consumer/workloads/public/plugin-manifest.json b/ui/consumer/workloads/public/plugin-manifest.json new file mode 100644 index 00000000..e37e3268 --- /dev/null +++ b/ui/consumer/workloads/public/plugin-manifest.json @@ -0,0 +1,62 @@ +{ + "name": "workload.compute.datumapis.com", + "version": "0.1.0", + "sdk": { + "name": "@datum-cloud/portal-plugin-sdk", + "range": "^1.0.0" + }, + "remoteEntry": "remoteEntry.js", + "exposedModules": { + "WorkloadList": "./src/pages/workload-list.tsx", + "WorkloadDetail": "./src/pages/workload-detail.tsx", + "InstanceDetail": "./src/pages/instance-detail.tsx" + }, + "extensions": [ + { + "type": "portal.nav/project", + "properties": { + "id": "compute-workloads", + "title": "Workloads", + "icon": "server", + "path": "workloads", + "order": 80 + }, + "requirements": { + "permissions": [ + { "group": "compute.datumapis.com", "resource": "workloads", "verb": "list" } + ] + } + }, + { + "type": "portal.page/project", + "properties": { + "path": "workloads", + "component": { "$codeRef": "WorkloadList" } + }, + "requirements": { + "permissions": [ + { "group": "compute.datumapis.com", "resource": "workloads", "verb": "list" } + ] + } + }, + { + "type": "portal.page/project", + "properties": { + "path": "workloads/:workloadName", + "component": { "$codeRef": "WorkloadDetail" } + } + }, + { + "type": "portal.page/project", + "properties": { + "path": "workloads/:workloadName/instances/:instanceName", + "component": { "$codeRef": "InstanceDetail" } + }, + "requirements": { + "permissions": [ + { "group": "compute.datumapis.com", "resource": "instances", "verb": "get" } + ] + } + } + ] +} diff --git a/ui/consumer/workloads/src/adapter.ts b/ui/consumer/workloads/src/adapter.ts new file mode 100644 index 00000000..769f0d08 --- /dev/null +++ b/ui/consumer/workloads/src/adapter.ts @@ -0,0 +1,238 @@ +/** + * Raw K8s resource → schema mappers — pure functions, ported from cloud-portal + * PR #1315's `app/resources/workloads/workload.adapter.ts` and + * `app/resources/instances/instance.adapter.ts`. + * + * PR #1315 typed `raw` against the generated SDK + * (`ComDatumapisComputeV1AlphaWorkload` / `...Instance` from + * `@/modules/control-plane/compute`), which a plugin has no access to. The + * `Raw*` interfaces below are hand-written against the actual Go JSON tags in + * this repo (`api/v1alpha/workload_types.go`, `api/v1alpha/instance_types.go`, + * `api/v1alpha/labels.go`) instead — same shape, no generated-SDK dependency. + */ +import type { + Instance, + InstanceStatusValue, + Workload, + WorkloadHealth, +} from './schema'; + +// ── Shared ─────────────────────────────────────────────────────────────── + +interface RawCondition { + type?: string; + status?: 'True' | 'False' | 'Unknown'; + reason?: string; + message?: string; + lastTransitionTime?: string; + observedGeneration?: number; +} + +interface RawObjectMeta { + uid?: string; + name?: string; + namespace?: string; + resourceVersion?: string; + creationTimestamp?: string; + labels?: Record; +} + +interface RawSandboxContainer { + name?: string; + image?: string; + ports?: { name?: string; port?: number; protocol?: string }[]; +} + +interface RawRuntime { + resources?: { + instanceType?: string; + // corev1.ResourceList — a map of resource name -> quantity string, e.g. + // { cpu: "1", memory: "512Mi" }. + requests?: Record; + }; + sandbox?: { containers?: RawSandboxContainer[] }; + virtualMachine?: unknown; +} + +// ── Workload ───────────────────────────────────────────────────────────── + +/** Well-known labels stamped onto instances by the compute controllers. */ +export const INSTANCE_LABELS = { + workloadName: 'compute.datumapis.com/workload-name', + workloadUid: 'compute.datumapis.com/workload-uid', + cityCode: 'compute.datumapis.com/city-code', + placementName: 'compute.datumapis.com/placement-name', +} as const; + +interface RawWorkloadPlacement { + name: string; + cityCodes?: string[]; + scaleSettings?: { minReplicas?: number; maxReplicas?: number }; +} + +export interface RawWorkload { + metadata?: RawObjectMeta; + spec?: { + template?: { spec?: { runtime?: RawRuntime } }; + placements?: RawWorkloadPlacement[]; + }; + status?: { + conditions?: RawCondition[]; + deployments?: number; + replicas?: number; + currentReplicas?: number; + updatedReplicas?: number; + desiredReplicas?: number; + readyReplicas?: number; + }; +} + +export interface RawWorkloadList { + items?: RawWorkload[]; +} + +function deriveWorkloadHealth(conditions: RawCondition[]): WorkloadHealth { + if (!conditions || conditions.length === 0) return 'Unknown'; + + const available = conditions.find((c) => c.type === 'Available'); + const progressing = conditions.find((c) => c.type === 'Progressing'); + + if (!available) return 'Unknown'; + + if (available.status === 'True') return 'Available'; + if (available.status === 'False' && progressing?.status === 'True') return 'Degraded'; + if (available.status === 'False') return 'Unavailable'; + return 'Unknown'; +} + +/** Builds a human-readable resource summary, e.g. "datumcloud/d1-standard-2 · 1 vCPU · 512Mi". */ +function deriveResources(runtime?: RawRuntime): string | undefined { + const res = runtime?.resources; + if (!res) return undefined; + + const parts: string[] = []; + if (res.instanceType) parts.push(res.instanceType); + + const requests = res.requests ?? {}; + if (requests.cpu !== undefined) parts.push(`${requests.cpu} vCPU`); + if (requests.memory !== undefined) parts.push(String(requests.memory)); + + return parts.length > 0 ? parts.join(' · ') : undefined; +} + +/** Returns the per-region replica count only when every placement shares the same minReplicas. */ +function deriveReplicasPerRegion(placements: RawWorkloadPlacement[]): number | undefined { + if (!placements || placements.length === 0) return undefined; + + const mins = placements.map((p) => p.scaleSettings?.minReplicas); + const first = mins[0]; + if (first === undefined) return undefined; + + return mins.every((m) => m === first) ? first : undefined; +} + +export function toWorkload(raw: RawWorkload): Workload { + const conditions = raw.status?.conditions ?? []; + const placements = raw.spec?.placements ?? []; + const runtime = raw.spec?.template?.spec?.runtime; + + return { + uid: raw.metadata?.uid ?? '', + name: raw.metadata?.name ?? '', + namespace: raw.metadata?.namespace, + resourceVersion: raw.metadata?.resourceVersion, + createdAt: raw.metadata?.creationTimestamp + ? new Date(raw.metadata.creationTimestamp) + : new Date(), + image: runtime?.sandbox?.containers?.[0]?.image, + health: deriveWorkloadHealth(conditions), + currentReplicas: raw.status?.currentReplicas ?? 0, + desiredReplicas: raw.status?.desiredReplicas ?? 0, + placements: placements.map((p) => p.name), + runtimeType: runtime ? (runtime.sandbox ? 'Container sandbox' : 'Virtual machine') : undefined, + regions: Array.from(new Set(placements.flatMap((p) => p.cityCodes ?? []))), + resources: deriveResources(runtime), + replicasPerRegion: deriveReplicasPerRegion(placements), + conditions: conditions.map((c) => ({ + type: c.type ?? '', + status: c.status ?? 'Unknown', + reason: c.reason, + message: c.message, + lastTransitionTime: c.lastTransitionTime, + observedGeneration: c.observedGeneration, + })), + }; +} + +export function toWorkloadList(items: RawWorkload[]): Workload[] { + return items.map(toWorkload); +} + +// ── Instance ───────────────────────────────────────────────────────────── + +export interface RawInstance { + metadata?: RawObjectMeta; + spec?: { runtime?: RawRuntime }; + status?: { + conditions?: RawCondition[]; + networkInterfaces?: { + assignments?: { networkIP?: string; externalIP?: string }; + }[]; + }; +} + +export interface RawInstanceList { + items?: RawInstance[]; +} + +// The compute API has no explicit "Failed" status field, so we infer it from +// the Available condition's reason/message text — best-effort, matching PR +// #1315's heuristic. +function deriveInstanceStatus(conditions: RawCondition[]): InstanceStatusValue { + if (!conditions || conditions.length === 0) return 'Unknown'; + + const available = conditions.find((c) => c.type === 'Available'); + if (!available) return 'Unknown'; + if (available.status === 'True') return 'Available'; + + const text = `${available.reason ?? ''} ${available.message ?? ''}`; + if (/fail|error/i.test(text)) return 'Failed'; + return 'Pending'; +} + +export function toInstance(raw: RawInstance): Instance { + const labels = raw.metadata?.labels ?? {}; + const assignments = raw.status?.networkInterfaces?.[0]?.assignments; + const container = raw.spec?.runtime?.sandbox?.containers?.[0]; + const conditions = raw.status?.conditions ?? []; + + return { + uid: raw.metadata?.uid ?? '', + name: raw.metadata?.name ?? '', + namespace: raw.metadata?.namespace, + createdAt: raw.metadata?.creationTimestamp + ? new Date(raw.metadata.creationTimestamp) + : new Date(), + workloadName: labels[INSTANCE_LABELS.workloadName], + workloadUid: labels[INSTANCE_LABELS.workloadUid], + city: labels[INSTANCE_LABELS.cityCode], + placement: labels[INSTANCE_LABELS.placementName], + instanceType: raw.spec?.runtime?.resources?.instanceType, + image: container?.image, + ports: (container?.ports ?? []).map((p) => `${p.port}/${p.protocol ?? 'TCP'}`), + status: deriveInstanceStatus(conditions), + externalIP: assignments?.externalIP, + internalIP: assignments?.networkIP, + conditions: conditions.map((c) => ({ + type: c.type ?? '', + status: c.status ?? 'Unknown', + reason: c.reason, + message: c.message, + lastTransitionTime: c.lastTransitionTime, + })), + }; +} + +export function toInstanceList(items: RawInstance[]): Instance[] { + return items.map(toInstance); +} diff --git a/ui/consumer/workloads/src/components/cli-section.tsx b/ui/consumer/workloads/src/components/cli-section.tsx new file mode 100644 index 00000000..30c496f1 --- /dev/null +++ b/ui/consumer/workloads/src/components/cli-section.tsx @@ -0,0 +1,107 @@ +/** + * Ported near-verbatim from cloud-portal PR #1315's + * `app/features/workload/cli-section.tsx`. Only the imports changed: + * - `@/hooks/useCopyToClipboard` (portal-internal) → `@datum-cloud/datum-ui/hooks`, + * which exports the same hook. + * Everything else — `CommandBlock`, `SectionCard`, `CliBanner` — only depends + * on `@datum-cloud/datum-ui` and `lucide-react`, so it needs no other changes. + */ +import { useCopyToClipboard } from '@datum-cloud/datum-ui/hooks'; +import { Card, CardContent } from '@datum-cloud/datum-ui/card'; +import { toast } from '@datum-cloud/datum-ui/toast'; +import { cn } from '@datum-cloud/datum-ui/utils'; +import { BookOpenIcon, CheckIcon, CopyIcon, DownloadIcon, SquareTerminalIcon } from 'lucide-react'; +import { useState } from 'react'; + +export function CommandBlock({ value, danger }: { value: string; danger?: boolean }) { + const [, copy] = useCopyToClipboard(); + const [copied, setCopied] = useState(false); + + const handleCopy = () => { + copy(value).then(() => { + toast.success('Copied to clipboard'); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + }; + + return ( +
+ + $ + {value} + + +
+ ); +} + +export function SectionCard({ + icon, + title, + description, + commands, + danger, +}: { + icon: React.ReactNode; + title: string; + description: string; + commands: string[]; + danger?: boolean; +}) { + return ( + + +
+ + {icon} + +

{title}

+
+

{description}

+
+ {commands.map((cmd) => ( + + ))} +
+
+
+ ); +} + +/** Banner pointing users at the datumctl CLI docs — shown wherever a resource is CLI-managed only. */ +export function CliBanner({ title, description }: { title: string; description: string }) { + return ( +
+ +
+

{title}

+

{description}

+
+ +
+ ); +} diff --git a/ui/consumer/workloads/src/components/plugin-tabs.tsx b/ui/consumer/workloads/src/components/plugin-tabs.tsx new file mode 100644 index 00000000..cba82d59 --- /dev/null +++ b/ui/consumer/workloads/src/components/plugin-tabs.tsx @@ -0,0 +1,28 @@ +/** + * Shared tab bar for the workload/instance detail pages. Only the first tab + * ("Overview") has a real view in this plugin's v1 scope — the rest render + * disabled rather than linking to pages that don't exist yet. + */ +import { cn } from '@datum-cloud/datum-ui/utils'; + +export function PluginTabs({ tabs, testId }: { tabs: string[]; testId?: string }) { + return ( +
+ {tabs.map((tab, index) => { + const active = index === 0; + return ( +
+ {tab} +
+ ); + })} +
+ ); +} diff --git a/ui/consumer/workloads/src/components/stat-strip.tsx b/ui/consumer/workloads/src/components/stat-strip.tsx new file mode 100644 index 00000000..853a029f --- /dev/null +++ b/ui/consumer/workloads/src/components/stat-strip.tsx @@ -0,0 +1,28 @@ +/** + * Shared horizontal stat strip (a divided row of label/value pairs) used as + * the "fleet bar" on both the workload list and workload detail pages. + */ +import { cn } from '@datum-cloud/datum-ui/utils'; + +export interface Stat { + label: string; + value: string; + className?: string; +} + +export function StatStrip({ stats, testId }: { stats: Stat[]; testId?: string }) { + return ( +
+ {stats.map((s) => ( +
+ + {s.label} + + {s.value} +
+ ))} +
+ ); +} diff --git a/ui/consumer/workloads/src/components/states.tsx b/ui/consumer/workloads/src/components/states.tsx new file mode 100644 index 00000000..da37d5ac --- /dev/null +++ b/ui/consumer/workloads/src/components/states.tsx @@ -0,0 +1,80 @@ +/** + * Shared loading / empty / error / restricted-access states for the plugin's + * pages. Not part of PR #1315 (which used the portal's server-side + * `runDetailLoader`/`runListLoader` RBAC gate + `EmptyContent` for this) — + * written fresh here since a plugin has no server loader. See `lib/api.ts`'s + * `ApiError` for how a 403 response reaches these. + */ +import { ApiError } from '../lib/api'; +import { Card, CardContent } from '@datum-cloud/datum-ui/card'; +import { Skeleton } from '@datum-cloud/datum-ui/skeleton'; +import { LockIcon, ServerCrashIcon } from 'lucide-react'; + +export function LoadingSkeleton() { + return ( +
+ + + +
+ ); +} + +export function RestrictedState({ message }: { message: string }) { + return ( +
+ + + +
+

Access restricted

+

{message}

+
+
+
+
+ ); +} + +export function ErrorState({ error, onRetry }: { error: unknown; onRetry: () => void }) { + const message = error instanceof Error ? error.message : 'An unknown error occurred'; + + return ( +
+ + + +
+

Failed to load

+

{message}

+
+ +
+
+
+ ); +} + +/** + * Renders the restricted state for a 403 `ApiError`, otherwise the generic + * error state. Call at the top of a page once `error` is truthy. + */ +export function ErrorOrRestrictedState({ + error, + restrictedMessage, + onRetry, +}: { + error: unknown; + restrictedMessage: string; + onRetry: () => void; +}) { + if (error instanceof ApiError && error.status === 403) { + return ; + } + return ; +} diff --git a/ui/consumer/workloads/src/lib/api.ts b/ui/consumer/workloads/src/lib/api.ts new file mode 100644 index 00000000..cda829cf --- /dev/null +++ b/ui/consumer/workloads/src/lib/api.ts @@ -0,0 +1,146 @@ +/** + * Data-fetching for the compute plugin. + * + * Every API call goes through the portal's existing Milo control-plane proxy + * at `/api/proxy/…` — exactly like `examples/sample-plugin/src/lib/api.ts`'s + * `fetchDnsZones`. There is no plugin-declared backend proxy, and no + * generated SDK (PR #1315's `@/modules/control-plane/compute` is + * portal-internal and unavailable here) — these are plain `fetch()` calls + * against the compute aggregated apiserver, reached the same way as any other + * Milo resource. + * + * `@tanstack/react-query` is a host-shared singleton (see vite.config.ts), so + * plugin queries live in the host's cache alongside built-in pages. This + * plugin must NOT create its own QueryClient. + * + * v1 simplifications (accepted, documented in the plugin's README): + * - Polling via `refetchInterval` instead of PR #1315's watch-stream hook + * (`useResourceWatch`, portal-internal). + * - Client-side 403 handling instead of PR #1315's server-loader RBAC gate + * (`runDetailLoader`, portal-internal) — see `ApiError` below. + */ +import { toInstance, toInstanceList, toWorkload, toWorkloadList, INSTANCE_LABELS } from '../adapter'; +import type { RawInstance, RawInstanceList, RawWorkload, RawWorkloadList } from '../adapter'; +import type { Instance, Workload } from '../schema'; +import { useQuery, type UseQueryResult } from '@tanstack/react-query'; + +/** + * Query keys are NAMESPACED under the canonical plugin id. Plugin queries + * share the host's single QueryCache (flat global key namespace), so + * prefixing with the plugin id prevents collisions with host keys or other + * plugins' keys. + */ +export const PLUGIN_ID = 'workload.compute.datumapis.com'; + +/** Live-ish polling interval — the v1 substitute for watch-stream updates. */ +const REFETCH_INTERVAL_MS = 10_000; + +/** Thrown for non-ok proxy responses; carries the HTTP status for 403 handling. */ +export class ApiError extends Error { + status: number; + + constructor(status: number, message: string) { + super(message); + this.name = 'ApiError'; + this.status = status; + } +} + +function getProjectScopedBase(projectId: string): string { + // Project-scoped control-plane path, forwarded server-side by /api/proxy + // with the user's token. Mirrors app/resources/base/utils.ts. + return `/api/proxy/apis/resourcemanager.miloapis.com/v1alpha1/projects/${projectId}/control-plane`; +} + +// NOTE: v1alpha, NOT v1alpha1 — verified against api/v1alpha/groupversion_info.go +// in the compute repo. +const WORKLOADS_PATH = '/apis/compute.datumapis.com/v1alpha/namespaces/default/workloads'; +const INSTANCES_PATH = '/apis/compute.datumapis.com/v1alpha/namespaces/default/instances'; + +async function proxyFetch(projectId: string, path: string): Promise { + const url = `${getProjectScopedBase(projectId)}${path}`; + const res = await fetch(url, { headers: { Accept: 'application/json' } }); + if (!res.ok) { + throw new ApiError(res.status, `Request failed (${res.status}): ${path}`); + } + return res.json() as Promise; +} + +// ── Workloads ──────────────────────────────────────────────────────────── + +async function fetchWorkloads(projectId: string): Promise { + const body = await proxyFetch(projectId, `${WORKLOADS_PATH}?limit=100`); + return toWorkloadList(body.items ?? []); +} + +async function fetchWorkload(projectId: string, name: string): Promise { + const raw = await proxyFetch(projectId, `${WORKLOADS_PATH}/${name}`); + return toWorkload(raw); +} + +export function useWorkloads(projectId: string | undefined): UseQueryResult { + return useQuery({ + queryKey: [PLUGIN_ID, 'workloads', projectId], + enabled: !!projectId, + queryFn: () => fetchWorkloads(projectId as string), + refetchInterval: REFETCH_INTERVAL_MS, + retry: false, // RBAC/entitlement failures shouldn't retry-storm + }); +} + +export function useWorkload( + projectId: string | undefined, + name: string | undefined +): UseQueryResult { + return useQuery({ + queryKey: [PLUGIN_ID, 'workload', projectId, name], + enabled: !!projectId && !!name, + queryFn: () => fetchWorkload(projectId as string, name as string), + refetchInterval: REFETCH_INTERVAL_MS, + retry: false, + }); +} + +// ── Instances ──────────────────────────────────────────────────────────── + +/** Builds the labelSelector that scopes instances to a single workload. */ +function workloadInstancesSelector(workloadName: string): string { + return `${INSTANCE_LABELS.workloadName}=${workloadName}`; +} + +async function fetchWorkloadInstances(projectId: string, workloadName: string): Promise { + const query = new URLSearchParams({ labelSelector: workloadInstancesSelector(workloadName) }); + const body = await proxyFetch(projectId, `${INSTANCES_PATH}?${query.toString()}`); + return toInstanceList(body.items ?? []); +} + +async function fetchInstance(projectId: string, instanceName: string): Promise { + const raw = await proxyFetch(projectId, `${INSTANCES_PATH}/${instanceName}`); + return toInstance(raw); +} + +export function useWorkloadInstances( + projectId: string | undefined, + workloadName: string | undefined +): UseQueryResult { + return useQuery({ + queryKey: [PLUGIN_ID, 'workload-instances', projectId, workloadName], + enabled: !!projectId && !!workloadName, + queryFn: () => fetchWorkloadInstances(projectId as string, workloadName as string), + refetchInterval: REFETCH_INTERVAL_MS, + retry: false, + }); +} + +export function useInstance( + projectId: string | undefined, + instanceName: string | undefined +): UseQueryResult { + return useQuery({ + queryKey: [PLUGIN_ID, 'instance', projectId, instanceName], + enabled: !!projectId && !!instanceName, + queryFn: () => fetchInstance(projectId as string, instanceName as string), + refetchInterval: REFETCH_INTERVAL_MS, + retry: false, + }); +} diff --git a/ui/consumer/workloads/src/lib/format.ts b/ui/consumer/workloads/src/lib/format.ts new file mode 100644 index 00000000..467f75c8 --- /dev/null +++ b/ui/consumer/workloads/src/lib/format.ts @@ -0,0 +1,22 @@ +/** + * Ported as-is from cloud-portal PR #1315's `app/utils/helpers/compute.helper.ts` + * — pure functions, no portal dependencies. + */ +export function formatUptime(createdAt: Date): string { + const diffSecs = Math.floor((Date.now() - createdAt.getTime()) / 1000); + const days = Math.floor(diffSecs / 86400); + const hours = Math.floor((diffSecs % 86400) / 3600); + const mins = Math.floor((diffSecs % 3600) / 60); + const parts: string[] = []; + if (days > 0) parts.push(`${days}d`); + if (hours > 0 || days > 0) parts.push(`${hours}h`); + parts.push(`${mins}m`); + return parts.join(' '); +} + +/** Split a slash-delimited path value (e.g. "provider/name") into its last segment and prefix. */ +export function splitSlashValue(value: string): { main: string; sub: string } { + const idx = value.lastIndexOf('/'); + if (idx === -1) return { main: value, sub: '' }; + return { main: value.slice(idx + 1), sub: value.slice(0, idx) }; +} diff --git a/ui/consumer/workloads/src/main.tsx b/ui/consumer/workloads/src/main.tsx new file mode 100644 index 00000000..6d348061 --- /dev/null +++ b/ui/consumer/workloads/src/main.tsx @@ -0,0 +1,53 @@ +import InstanceDetail from './pages/instance-detail'; +import WorkloadDetail from './pages/workload-detail'; +import WorkloadList from './pages/workload-list'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { Link, MemoryRouter, Route, Routes } from 'react-router'; + +// Standalone preview only. Wraps the pages in a MemoryRouter so `useParams()` +// resolves the same params the host mount would supply, and a +// QueryClientProvider so the data hooks run — exactly what the host provides +// in production. Data calls hit /api/proxy/... which 404s standalone (no +// portal proxy), so the data pages show their error states; that's expected +// here. Run the full portal to see live data. +const queryClient = new QueryClient(); + +const base = '/project/:projectId/services/:serviceSlug'; + +createRoot(document.getElementById('root')!).render( + + +
+

+ Standalone preview — the portal loads this plugin via + /plugin-manifest.json and /remoteEntry.js, not this page. Data + pages show their error state here (no portal proxy). Run the full portal to see live + data. +

+ + + + } /> + } /> + } + /> + + +
+
+
+); diff --git a/ui/consumer/workloads/src/pages/instance-detail.tsx b/ui/consumer/workloads/src/pages/instance-detail.tsx new file mode 100644 index 00000000..092c1b25 --- /dev/null +++ b/ui/consumer/workloads/src/pages/instance-detail.tsx @@ -0,0 +1,368 @@ +/** + * `portal.page/project` extension at + * `workloads/:workloadName/instances/:instanceName`, exposed as + * `InstanceDetail`. + * + * Ported from cloud-portal PR #1315's + * `app/routes/project/detail/compute/workloads/detail/instances/index.tsx` + * (Overview tab only — its Settings tab is intentionally not included in + * v1, matching the plugin's read-only scope). + * + * Rewritten for the plugin runtime: + * - `useInstance` polls `/api/proxy/…` (see `lib/api.ts`) instead of PR + * #1315's generated-SDK service. + * - No server loader — loading/error/restricted states are handled inline. + * - PR #1315's `TextCopy` (portal-internal, `@/components/text-copy`) is + * replaced by a small local `CopyableText` using the same + * `useCopyToClipboard` hook `cli-section.tsx` uses (from + * `@datum-cloud/datum-ui/hooks`). + */ +import { CommandBlock } from '../components/cli-section'; +import { PluginTabs } from '../components/plugin-tabs'; +import { ErrorOrRestrictedState, LoadingSkeleton } from '../components/states'; +import { useInstance } from '../lib/api'; +import { formatUptime, splitSlashValue } from '../lib/format'; +import { instanceStatusToBadgeType, type InstanceCondition } from '../schema'; +import { Badge } from '@datum-cloud/datum-ui/badge'; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from '@datum-cloud/datum-ui/breadcrumb'; +import { Card, CardContent, CardHeader, CardTitle } from '@datum-cloud/datum-ui/card'; +import { useCopyToClipboard } from '@datum-cloud/datum-ui/hooks'; +import { PageTitle } from '@datum-cloud/datum-ui/page-title'; +import { toast } from '@datum-cloud/datum-ui/toast'; +import { + BoxIcon, + CheckIcon, + CheckCircle2Icon, + CircleIcon, + CopyIcon, + CpuIcon, + GlobeIcon, + HomeIcon, + LinkIcon, + SquareTerminalIcon, + TimerIcon, + WifiIcon, + XCircleIcon, +} from 'lucide-react'; +import { useState } from 'react'; +import { useLocation, useParams } from 'react-router'; + +const INSTANCE_TABS = ['Overview', 'Metrics', 'Logs', 'Manage', 'Activity']; + +function StatCard({ + icon, + label, + value, + sub, +}: { + icon: React.ReactNode; + label: string; + value: string; + sub?: string; +}) { + return ( + +
+
{icon}
+
+ + {label} + + {value} + {sub && {sub}} +
+
+
+ ); +} + +function ConditionIcon({ status }: { status: InstanceCondition['status'] }) { + if (status === 'True') + return ; + if (status === 'False') + return ; + return ; +} + +/** Minimal local stand-in for the portal's internal `TextCopy` component. */ +function CopyableText({ value, text, className }: { value: string; text?: string; className?: string }) { + const [, copy] = useCopyToClipboard(); + const [copied, setCopied] = useState(false); + + return ( + + ); +} + +export default function InstanceDetail() { + const { projectId, workloadName, instanceName } = useParams<{ + projectId: string; + workloadName: string; + instanceName: string; + }>(); + const location = useLocation(); + + const { data: instance, isLoading, error, refetch } = useInstance(projectId, instanceName); + + const basePath = location.pathname.replace(/\/$/, ''); + const instancesHref = basePath.replace(/\/instances\/[^/]+$/, ''); + const workloadsHref = instancesHref.replace(/\/[^/]+$/, ''); + const projectHref = projectId ? `/project/${projectId}` : '/'; + + if (isLoading) return ; + + if (error || !instance) { + return ( + void refetch()} + /> + ); + } + + const { main: imageShort, sub: imageRegistry } = splitSlashValue(instance.image ?? ''); + const { main: instanceTypeShort, sub: instanceTypeProvider } = splitSlashValue( + instance.instanceType ?? '' + ); + + const firstPort = instance.ports[0]; + + return ( +
+ + + + + + + + + + Workloads + + + + {workloadName} + + + + {instance.name} + + + + + + + {instance.status} + + {instance.city && {instance.city}} + + } + /> + + + + {/* Stat tiles */} +
+ } + label="Uptime" + value={formatUptime(instance.createdAt)} + sub={`Since ${instance.createdAt.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}`} + /> + } + label="Region" + value={instance.city ?? '—'} + /> + {instance.instanceType && ( + } + label="Instance Type" + value={instanceTypeShort} + sub={instanceTypeProvider} + /> + )} + {instance.image && ( + } + label="Image" + value={imageShort} + sub={imageRegistry} + /> + )} +
+ + {/* Main two-column layout */} +
+ {/* Network */} + + + Network + + + {instance.externalIP && ( +
+ + External Endpoint + +
+ + +
+
+ )} + {instance.internalIP && ( +
+ + Internal IP + +
+ + +
+
+ )} + {!instance.externalIP && !instance.internalIP && ( +

No network assignments yet

+ )} +
+
+ + {/* Right column */} +
+ {/* Health Conditions */} + {instance.conditions.length > 0 && ( + + + Health Conditions + + +
+ {instance.conditions.map((c) => ( +
+ +
+ {c.type} + {(c.message || c.reason) && ( + + {c.message || c.reason} + + )} +
+
+ ))} +
+
+
+ )} + + {/* Runtime */} + + + Runtime + + +
+ {instance.image && ( +
+
Image
+
+ {instance.image} +
+
+ )} + {instance.ports.length > 0 && ( +
+
Ports
+
{instance.ports.join(', ')}
+
+ )} + {instance.placement && ( +
+
Placement
+
{instance.placement}
+
+ )} +
+
Created
+
+ {instance.createdAt.toLocaleDateString('en-US', { + day: '2-digit', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + timeZoneName: 'short', + })} +
+
+
+
+
+
+
+ + {/* CLI reference */} + + + + datumctl Commands + CLI + + +
+ Get instance + +
+
+ Describe instance + +
+
+ List instances + +
+
+ Get workload + +
+
+
+
+ ); +} diff --git a/ui/consumer/workloads/src/pages/workload-detail.tsx b/ui/consumer/workloads/src/pages/workload-detail.tsx new file mode 100644 index 00000000..9549de75 --- /dev/null +++ b/ui/consumer/workloads/src/pages/workload-detail.tsx @@ -0,0 +1,268 @@ +/** + * `portal.page/project` extension at `workloads/:workloadName`, exposed as + * `WorkloadDetail`. + * + * Ported from cloud-portal PR #1315's + * `app/routes/project/detail/compute/workloads/detail/index.tsx` (Overview + * tab) with its embedded instance list from + * `.../workloads/detail/instances/index.tsx`'s sibling table in the workload + * overview. Per the plugin's scope, only the Overview tab is ported — the + * Activity/Metrics/Settings tabs from PR #1315 are intentionally not + * included in v1. + * + * Rewritten for the plugin runtime: + * - `useWorkload`/`useWorkloadInstances` poll `/api/proxy/…` (see + * `lib/api.ts`) instead of PR #1315's generated-SDK service + + * `useResourceWatch`/`useGuardedRouteData`. + * - No server loader — loading/error/restricted states are handled inline + * instead of via `defineResourceRoute`/`runDetailLoader`. + * - Instance row navigation builds the child path from `useLocation()` + * instead of the portal's `paths.config.ts` + `getPathWithParams`. + */ +import { PluginTabs } from '../components/plugin-tabs'; +import { StatStrip, type Stat } from '../components/stat-strip'; +import { ErrorOrRestrictedState, LoadingSkeleton } from '../components/states'; +import { useWorkload, useWorkloadInstances } from '../lib/api'; +import { formatUptime, splitSlashValue } from '../lib/format'; +import { instanceStatusToBadgeType, workloadHealthToBadgeType, type Instance } from '../schema'; +import { Badge } from '@datum-cloud/datum-ui/badge'; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from '@datum-cloud/datum-ui/breadcrumb'; +import { Card, CardContent, CardHeader, CardTitle } from '@datum-cloud/datum-ui/card'; +import { PageTitle } from '@datum-cloud/datum-ui/page-title'; +import { cn } from '@datum-cloud/datum-ui/utils'; +import { ArrowRightIcon, HomeIcon } from 'lucide-react'; +import { useLocation, useNavigate, useParams } from 'react-router'; + +const INSTANCE_STATUS_DOT: Record = { + Available: 'bg-green-500', + Pending: 'bg-yellow-500', + Failed: 'bg-red-500', + Unknown: 'bg-muted-foreground', +}; + +const WORKLOAD_TABS = ['Overview', 'Deployments', 'Metrics', 'Activity']; + +function InstanceCard({ instance, onClick }: { instance: Instance; onClick: () => void }) { + return ( +
+
+
+

{instance.name}

+

{instance.city ?? 'Unknown region'}

+
+ + {instance.status} + +
+ + {(instance.instanceType || instance.ports.length > 0) && ( +
+ {instance.instanceType && ( + + {instance.instanceType} + + )} + {instance.ports.map((port) => ( + + {port} + + ))} +
+ )} + +
+ + + Up {formatUptime(instance.createdAt)} + + + View instance + + +
+
+ ); +} + +export default function WorkloadDetail() { + const { projectId, workloadName } = useParams<{ projectId: string; workloadName: string }>(); + const navigate = useNavigate(); + const location = useLocation(); + + const { data: workload, isLoading, error, refetch } = useWorkload(projectId, workloadName); + const { data: instances = [] } = useWorkloadInstances(projectId, workloadName); + + const basePath = location.pathname.replace(/\/$/, ''); + const instanceHref = (name: string) => `${basePath}/instances/${name}`; + const workloadsHref = basePath.replace(/\/[^/]+$/, ''); + const projectHref = projectId ? `/project/${projectId}` : '/'; + + if (isLoading) return ; + + if (error || !workload) { + return ( + void refetch()} + /> + ); + } + + // Health counts — prefer live instance data, fall back to replica counts. + const healthyCount = instances.length + ? instances.filter((i) => i.status === 'Available').length + : workload.currentReplicas; + const totalCount = instances.length || workload.desiredReplicas; + const allHealthy = totalCount > 0 && healthyCount === totalCount; + + const regions = workload.regions ?? []; + const { main: resourceShort } = splitSlashValue(workload.resources ?? ''); + + const createdFormatted = workload.createdAt + ? workload.createdAt.toLocaleDateString('en-US', { + month: 'long', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) + : null; + + const stats: Stat[] = [ + { + label: 'Instances', + value: `${healthyCount}/${totalCount}`, + className: allHealthy ? 'text-green-600 dark:text-green-500' : undefined, + }, + { label: 'Regions', value: regions.length > 0 ? regions.join(', ') : '—' }, + ...(workload.resources ? [{ label: 'Resources', value: resourceShort }] : []), + { + label: 'Replicas', + value: + workload.replicasPerRegion !== undefined + ? `${workload.replicasPerRegion}/region · ${workload.desiredReplicas} total` + : `${workload.desiredReplicas} total`, + }, + ]; + + return ( +
+ + + + + + + + + + Workloads + + + + {workload.name} + + + + + + {workload.health} + + } + /> + + + + + + {/* Configuration */} + + + Configuration + + +
+
+
Resource name
+
{workload.name}
+
+ {workload.runtimeType && ( +
+
Runtime
+
{workload.runtimeType}
+
+ )} + {workload.image && ( +
+
Container image
+
{workload.image}
+
+ )} + {regions.length > 0 && ( +
+
Regions
+
{regions.join(', ')}
+
+ )} + {workload.resources && ( +
+
Resources
+
{workload.resources}
+
+ )} +
+
Replicas
+
+ {workload.replicasPerRegion !== undefined + ? `${workload.replicasPerRegion} per region · ${workload.desiredReplicas} total` + : `${workload.desiredReplicas} total`} +
+
+ {createdFormatted && ( +
+
Created
+
{createdFormatted}
+
+ )} +
+
+
+ + {/* Instance Locations */} +
+ + + {instances.length === 0 ? ( +

No running instances

+ ) : ( +
+ {instances.map((instance) => ( + navigate(instanceHref(instance.name))} + /> + ))} +
+ )} +
+
+ ); +} diff --git a/ui/consumer/workloads/src/pages/workload-list.tsx b/ui/consumer/workloads/src/pages/workload-list.tsx new file mode 100644 index 00000000..7dda8f57 --- /dev/null +++ b/ui/consumer/workloads/src/pages/workload-list.tsx @@ -0,0 +1,244 @@ +/** + * `portal.page/project` extension at `workloads`, exposed as `WorkloadList`. + * + * Ported from cloud-portal PR #1315's + * `app/routes/project/detail/compute/workloads/index.tsx`. Rewritten for the + * plugin runtime: + * - `useWorkloads` now polls `/api/proxy/…` directly (see `lib/api.ts`) + * instead of PR #1315's generated-SDK service + `useResourceWatch`. + * - No server loader — table rows come straight from the query; loading / + * error / restricted / empty states are handled inline instead of via + * `defineResourceRoute`/`runListLoader`. + * - Row navigation uses a plain relative `Link`/`useNavigate` instead of the + * portal's `paths.config.ts` + `getPathWithParams`. + * - The portal's internal `Table` component isn't available to plugins, so + * this renders a plain semantic `` (matching the shape of PR + * #1315's own instances table). + */ +import { CliBanner, SectionCard } from '../components/cli-section'; +import { StatStrip } from '../components/stat-strip'; +import { ErrorOrRestrictedState, LoadingSkeleton } from '../components/states'; +import { useWorkloads } from '../lib/api'; +import { workloadHealthToBadgeType, type Workload, type WorkloadHealth } from '../schema'; +import { Badge } from '@datum-cloud/datum-ui/badge'; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from '@datum-cloud/datum-ui/breadcrumb'; +import { PageTitle } from '@datum-cloud/datum-ui/page-title'; +import { cn } from '@datum-cloud/datum-ui/utils'; +import { formatDistanceToNowStrict } from 'date-fns'; +import { ArrowRightIcon, HomeIcon, RocketIcon, SearchIcon } from 'lucide-react'; +import { useLocation, useNavigate, useParams } from 'react-router'; + +const HEALTH_DOT_CLASS: Record = { + Available: 'bg-green-500', + Degraded: 'bg-yellow-500', + Unavailable: 'bg-red-500', + Unknown: 'bg-muted-foreground', +}; + +function FleetSummary({ workloads }: { workloads: Workload[] }) { + const readyInstances = workloads.reduce((sum, w) => sum + w.currentReplicas, 0); + const desiredInstances = workloads.reduce((sum, w) => sum + w.desiredReplicas, 0); + const healthy = workloads.filter((w) => w.health === 'Available').length; + const degraded = workloads.filter((w) => w.health === 'Degraded').length; + const unavailable = workloads.filter( + (w) => w.health === 'Unavailable' || w.health === 'Unknown' + ).length; + + const stats: { label: string; value: string; className?: string }[] = [ + { label: 'Workloads', value: String(workloads.length) }, + { label: 'Instances', value: `${readyInstances}/${desiredInstances}` }, + { + label: 'Healthy', + value: String(healthy), + className: healthy > 0 ? 'text-green-600 dark:text-green-500' : undefined, + }, + { + label: 'Degraded', + value: String(degraded), + className: degraded > 0 ? 'text-yellow-600 dark:text-yellow-500' : undefined, + }, + { + label: 'Unavailable', + value: String(unavailable), + className: unavailable > 0 ? 'text-red-600 dark:text-red-500' : undefined, + }, + ]; + + return ; +} + +function WorkloadCard({ workload, onClick }: { workload: Workload; onClick: () => void }) { + return ( +
+
+
+
+

{workload.name}

+ {workload.runtimeType && ( + + {workload.runtimeType} + + )} +
+ {workload.image && ( +

+ {workload.image} +

+ )} +
+ + {workload.health} + +
+ +
+
+

+ Ready +

+

+ {workload.currentReplicas}/{workload.desiredReplicas} +

+
+
+

Age

+

+ {formatDistanceToNowStrict(workload.createdAt, { addSuffix: true })} +

+
+
+ + {workload.placements.length > 0 && ( +
+

+ Placements +

+ {workload.placements.map((placement) => ( +
+ + {placement} +
+ ))} +
+ )} + +
+ Created {workload.createdAt.toLocaleDateString()} + + View workload + + +
+
+ ); +} + +export default function WorkloadList() { + const { projectId } = useParams<{ projectId: string; serviceSlug: string }>(); + const navigate = useNavigate(); + const location = useLocation(); + const { data: workloads, isLoading, error, refetch } = useWorkloads(projectId); + + // Build the child route path from the current URL rather than the portal's + // internal `paths.config.ts` (unavailable to plugins) — the host mounts this + // page at `/project/:projectId/services/:serviceSlug/workloads`. + const basePath = location.pathname.replace(/\/$/, ''); + const workloadHref = (name: string) => `${basePath}/${name}`; + const projectHref = projectId ? `/project/${projectId}` : '/'; + + return ( +
+ + + + + + + + + + Workloads + + + + + + Read-only operational view of your project's compute workloads. Workloads are + created and managed with datumctl. + + } + /> + + {isLoading && } + + {!isLoading && error && ( + void refetch()} + /> + )} + + {!isLoading && !error && (workloads?.length ?? 0) === 0 && ( +
+ +
+ } + title="Deploy a workload" + description="Create a workload manifest and deploy it to your project. The dashboard will reflect the new workload within seconds." + commands={[ + 'datumctl compute deploy -f workload.yaml', + `datumctl compute deploy --project=${projectId ?? ''} -f workload.yaml`, + ]} + /> + } + title="List & inspect workloads" + description="Confirm your workload deployed successfully and inspect its current health and placement status." + commands={[ + 'datumctl compute workloads list', + 'datumctl compute workloads describe ', + ]} + /> +
+
+ )} + + {!isLoading && !error && workloads && workloads.length > 0 && ( + <> + +
+ {workloads.map((workload) => ( + navigate(workloadHref(workload.name))} + /> + ))} +
+ + )} +
+ ); +} diff --git a/ui/consumer/workloads/src/schema.ts b/ui/consumer/workloads/src/schema.ts new file mode 100644 index 00000000..46d43deb --- /dev/null +++ b/ui/consumer/workloads/src/schema.ts @@ -0,0 +1,126 @@ +/** + * Zod schemas for the compute plugin's two resources — pure data shapes, + * ported near-verbatim from cloud-portal PR #1315's + * `app/resources/workloads/workload.schema.ts` and + * `app/resources/instances/instance.schema.ts`. Combined into one file here + * since the plugin only exposes list/detail read views for both resources. + */ +import { z } from 'zod'; + +// ── Workload ───────────────────────────────────────────────────────────── + +export type WorkloadHealth = 'Available' | 'Degraded' | 'Unavailable' | 'Unknown'; + +const workloadConditionSchema = z.object({ + type: z.string(), + status: z.enum(['True', 'False', 'Unknown']), + reason: z.string().optional(), + message: z.string().optional(), + lastTransitionTime: z.string().optional(), + observedGeneration: z.number().optional(), +}); + +export const workloadResourceSchema = z.object({ + uid: z.string(), + name: z.string(), + namespace: z.string().optional(), + resourceVersion: z.string().optional(), + createdAt: z.coerce.date(), + image: z.string().optional(), + health: z.enum(['Available', 'Degraded', 'Unavailable', 'Unknown']), + currentReplicas: z.number(), + desiredReplicas: z.number(), + placements: z.array(z.string()), + conditions: z.array(workloadConditionSchema).default([]), + // Detail/overview fields + runtimeType: z.string().optional(), + regions: z.array(z.string()).default([]), + resources: z.string().optional(), + replicasPerRegion: z.number().optional(), +}); + +export type Workload = z.infer; + +export const workloadListSchema = z.object({ + items: z.array(workloadResourceSchema), +}); + +export type WorkloadList = z.infer; + +/** + * Maps a workload health value to a `Badge` `type` prop understood by + * `@datum-cloud/datum-ui/badge`. Shared by the list and detail pages. + */ +export function workloadHealthToBadgeType( + health: WorkloadHealth +): 'success' | 'warning' | 'danger' | 'muted' { + switch (health) { + case 'Available': + return 'success'; + case 'Degraded': + return 'warning'; + case 'Unavailable': + return 'danger'; + default: + return 'muted'; + } +} + +// ── Instance ───────────────────────────────────────────────────────────── + +export type InstanceStatusValue = 'Available' | 'Pending' | 'Failed' | 'Unknown'; + +const instanceConditionSchema = z.object({ + type: z.string(), + status: z.enum(['True', 'False', 'Unknown']), + reason: z.string().optional(), + message: z.string().optional(), + lastTransitionTime: z.string().optional(), +}); + +export type InstanceCondition = z.infer; + +export const instanceResourceSchema = z.object({ + uid: z.string(), + name: z.string(), + namespace: z.string().optional(), + createdAt: z.coerce.date(), + workloadName: z.string().optional(), + workloadUid: z.string().optional(), + city: z.string().optional(), + placement: z.string().optional(), + instanceType: z.string().optional(), + image: z.string().optional(), + ports: z.array(z.string()).default([]), + status: z.enum(['Available', 'Pending', 'Failed', 'Unknown']), + externalIP: z.string().optional(), + internalIP: z.string().optional(), + conditions: z.array(instanceConditionSchema).default([]), +}); + +export type Instance = z.infer; + +export const instanceListSchema = z.object({ + items: z.array(instanceResourceSchema), +}); + +export type InstanceList = z.infer; + +/** + * Maps an instance status value to a `Badge` `type` prop understood by + * `@datum-cloud/datum-ui/badge`. + */ +export function instanceStatusToBadgeType( + status: InstanceStatusValue +): 'success' | 'warning' | 'danger' | 'muted' { + switch (status) { + case 'Available': + return 'success'; + case 'Pending': + return 'warning'; + case 'Failed': + return 'danger'; + default: + return 'muted'; + } +} diff --git a/ui/consumer/workloads/tsconfig.json b/ui/consumer/workloads/tsconfig.json new file mode 100644 index 00000000..2af885b1 --- /dev/null +++ b/ui/consumer/workloads/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true, + "types": ["vite/client"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/ui/consumer/workloads/vite.config.ts b/ui/consumer/workloads/vite.config.ts new file mode 100644 index 00000000..6fb60f0f --- /dev/null +++ b/ui/consumer/workloads/vite.config.ts @@ -0,0 +1,76 @@ +import { federation } from '@module-federation/vite'; +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +// Compute Portal Plugin — a Module Federation remote loaded by the cloud-portal +// host at runtime. Structural template: examples/sample-plugin/ in the +// cloud-portal repo (see its README.md and +// docs/enhancements/portal-plugin-system.md there). +// +// The host (cloud-portal) loads this remote via @module-federation/runtime and +// provides react / react-dom / react-router / @tanstack/react-query as shared +// singletons, so the plugin renders with the host's exact React, router, and +// query client. `shared` below marks those as singletons with the host's +// version so the host copy always wins and React is never duplicated (two +// React instances break hooks). +// +// Assets are fetched server-side by the portal's asset proxy and served under +// /api/plugins//…, so plain http://localhost during dev is fine and the +// browser never contacts this origin directly. MF's automatic publicPath makes +// federated chunks resolve relative to remoteEntry.js, which is what lets them +// load correctly through that same-origin proxy prefix. +export default defineConfig({ + server: { + port: 7778, + strictPort: true, + // Allow cross-origin fetches of the manifest/remote during Tier 0/standalone. + cors: true, + }, + preview: { + port: 7778, + strictPort: true, + cors: true, + }, + build: { + target: 'esnext', + // Keep the plugin readable when inspecting the built bundle. + minify: false, + }, + plugins: [ + react(), + federation({ + // MUST equal the manifest `name` — the host keys the remote by this id. + name: 'workload.compute.datumapis.com', + // The manifest's `remoteEntry` field points the host at this filename, + // requested through the asset proxy as /api/plugins/compute/remoteEntry.js. + filename: 'remoteEntry.js', + manifest: true, + // Exposed keys map 1:1 to the manifest's `exposedModules` keys / $codeRefs. + // The host loads e.g. loadRemote('workload.compute.datumapis.com/WorkloadList'). + exposes: { + './WorkloadList': './src/pages/workload-list.tsx', + './WorkloadDetail': './src/pages/workload-detail.tsx', + './InstanceDetail': './src/pages/instance-detail.tsx', + }, + // Host-pinned singletons. requiredVersion tracks the host's majors + // (react 19, react-router 7, react-query 5). singleton:true guarantees + // one instance — the host provides all of these, so plugin queries share + // the host's QueryClient cache. + shared: { + react: { singleton: true, requiredVersion: '^19.0.0' }, + 'react-dom': { singleton: true, requiredVersion: '^19.0.0' }, + 'react-router': { singleton: true, requiredVersion: '^7.0.0' }, + '@tanstack/react-query': { singleton: true, requiredVersion: '^5.0.0' }, + // Curated datum-ui subset shared by the host (see the host's + // federation-host.ts DATUM_UI_SHARED). requiredVersion:false — the + // host's copy always wins, which is what keeps styling identical to + // built-in pages; the local install is types + standalone fallback. + '@datum-cloud/datum-ui/badge': { singleton: true, requiredVersion: false }, + '@datum-cloud/datum-ui/button': { singleton: true, requiredVersion: false }, + '@datum-cloud/datum-ui/card': { singleton: true, requiredVersion: false }, + '@datum-cloud/datum-ui/separator': { singleton: true, requiredVersion: false }, + '@datum-cloud/datum-ui/skeleton': { singleton: true, requiredVersion: false }, + }, + }), + ], +}); diff --git a/ui/provider/README.md b/ui/provider/README.md new file mode 100644 index 00000000..bbc34e57 --- /dev/null +++ b/ui/provider/README.md @@ -0,0 +1,8 @@ +# provider + +Reserved for compute-authored UI building blocks meant to be consumed by +other services' portal plugins or by cloud-portal itself — as opposed to +[`ui/consumer`](../consumer), which holds compute's own portal plugin(s). + +No code here yet. Add a subdirectory once there's a concrete cross-service UI +dependency to build.