From 1de11780861146fa3ba303052f1ccaabe0d77be5 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Thu, 29 Oct 2026 06:56:55 +0000 Subject: [PATCH 01/62] feat(ui): add the orchestration UI prototype as a SvelteKit app Port the prototype pages, components and styles into ui/ as a static single-page app, with neutral example owners and data. --- ui/.gitignore | 5 + ui/.npmrc | 1 + ui/README.md | 22 + ui/components.json | 20 + ui/package-lock.json | 2235 +++++++++++++++++ ui/package.json | 43 + ui/src/app.d.ts | 13 + ui/src/app.html | 23 + ui/src/lib/assets/favicon.svg | 1 + ui/src/lib/components/app-sidebar.svelte | 141 ++ ui/src/lib/components/app-topbar.svelte | 42 + .../lib/components/code/code-preview.svelte | 150 ++ ui/src/lib/components/code/sql-viewer.svelte | 655 +++++ .../lib/components/lineage/asset-node.svelte | 245 ++ .../components/lineage/col-cell-node.svelte | 54 + .../lineage/inspector-section.svelte | 68 + .../lineage/node-fields-popover.svelte | 37 + .../lib/components/list/filter-button.svelte | 36 + .../lib/components/list/list-toolbar.svelte | 117 + .../lib/components/list/relative-time.svelte | 14 + .../components/list/segmented-filter.svelte | 52 + .../lib/components/list/status-label.svelte | 27 + .../components/timeline/run-timeline.svelte | 517 ++++ ui/src/lib/components/ui/badge/badge.svelte | 49 + ui/src/lib/components/ui/badge/index.ts | 2 + ui/src/lib/components/ui/button/button.svelte | 82 + ui/src/lib/components/ui/button/index.ts | 17 + .../lib/components/ui/card/card-action.svelte | 23 + .../components/ui/card/card-content.svelte | 20 + .../ui/card/card-description.svelte | 20 + .../lib/components/ui/card/card-footer.svelte | 20 + .../lib/components/ui/card/card-header.svelte | 23 + .../lib/components/ui/card/card-title.svelte | 20 + ui/src/lib/components/ui/card/card.svelte | 22 + ui/src/lib/components/ui/card/index.ts | 25 + .../components/ui/checkbox/checkbox.svelte | 39 + ui/src/lib/components/ui/checkbox/index.ts | 6 + .../dropdown-menu-checkbox-group.svelte | 16 + .../dropdown-menu-checkbox-item.svelte | 44 + .../dropdown-menu-content.svelte | 31 + .../dropdown-menu-group-heading.svelte | 22 + .../dropdown-menu/dropdown-menu-group.svelte | 7 + .../dropdown-menu/dropdown-menu-item.svelte | 27 + .../dropdown-menu/dropdown-menu-label.svelte | 24 + .../dropdown-menu/dropdown-menu-portal.svelte | 7 + .../dropdown-menu-radio-group.svelte | 16 + .../dropdown-menu-radio-item.svelte | 34 + .../dropdown-menu-separator.svelte | 17 + .../dropdown-menu-shortcut.svelte | 20 + .../dropdown-menu-sub-content.svelte | 17 + .../dropdown-menu-sub-trigger.svelte | 29 + .../ui/dropdown-menu/dropdown-menu-sub.svelte | 7 + .../dropdown-menu-trigger.svelte | 7 + .../ui/dropdown-menu/dropdown-menu.svelte | 7 + .../lib/components/ui/dropdown-menu/index.ts | 54 + ui/src/lib/components/ui/popover/index.ts | 28 + .../ui/popover/popover-close.svelte | 7 + .../ui/popover/popover-content.svelte | 31 + .../ui/popover/popover-description.svelte | 20 + .../ui/popover/popover-header.svelte | 20 + .../ui/popover/popover-portal.svelte | 7 + .../ui/popover/popover-title.svelte | 20 + .../ui/popover/popover-trigger.svelte | 17 + .../lib/components/ui/popover/popover.svelte | 7 + ui/src/lib/components/ui/separator/index.ts | 7 + .../components/ui/separator/separator.svelte | 23 + ui/src/lib/components/ui/table/index.ts | 28 + .../lib/components/ui/table/table-body.svelte | 15 + .../components/ui/table/table-caption.svelte | 20 + .../lib/components/ui/table/table-cell.svelte | 15 + .../components/ui/table/table-footer.svelte | 20 + .../lib/components/ui/table/table-head.svelte | 15 + .../components/ui/table/table-header.svelte | 20 + .../lib/components/ui/table/table-row.svelte | 15 + ui/src/lib/components/ui/table/table.svelte | 17 + ui/src/lib/components/ui/tabs/index.ts | 18 + .../components/ui/tabs/tabs-content.svelte | 17 + .../lib/components/ui/tabs/tabs-list.svelte | 40 + .../components/ui/tabs/tabs-trigger.svelte | 23 + ui/src/lib/components/ui/tabs/tabs.svelte | 19 + ui/src/lib/components/ui/tooltip/index.ts | 19 + .../ui/tooltip/tooltip-content.svelte | 52 + .../ui/tooltip/tooltip-portal.svelte | 7 + .../ui/tooltip/tooltip-provider.svelte | 7 + .../ui/tooltip/tooltip-trigger.svelte | 7 + .../lib/components/ui/tooltip/tooltip.svelte | 7 + ui/src/lib/config/config-form.svelte | 178 ++ ui/src/lib/index.ts | 1 + ui/src/lib/lineage/column-lineage.svelte | 549 ++++ ui/src/lib/lineage/flow-controller.svelte | 23 + ui/src/lib/lineage/graph-draw.ts | 96 + ui/src/lib/lineage/graph-layout.ts | 132 + ui/src/lib/lineage/layout.ts | 52 + ui/src/lib/lineage/node-fields.svelte.ts | 72 + ui/src/lib/lineage/scoped-lineage.svelte | 206 ++ ui/src/lib/lineage/trace.ts | 53 + ui/src/lib/mock/model-sql.ts | 92 + ui/src/lib/mock/scale-graph.ts | 158 ++ ui/src/lib/mock/timeline.ts | 81 + ui/src/lib/run/run-panel.svelte | 382 +++ ui/src/lib/sql/diff.ts | 64 + ui/src/lib/sql/highlight.ts | 52 + ui/src/lib/sql/outline.ts | 72 + ui/src/lib/theme.svelte.ts | 21 + ui/src/lib/utils.ts | 13 + ui/src/routes/+layout.svelte | 17 + ui/src/routes/+layout.ts | 5 + ui/src/routes/+page.svelte | 257 ++ ui/src/routes/catalog/+page.svelte | 195 ++ ui/src/routes/catalog/[name]/+page.svelte | 772 ++++++ ui/src/routes/checks/+page.svelte | 132 + ui/src/routes/jobs/+page.svelte | 135 + ui/src/routes/jobs/[name]/+page.svelte | 181 ++ ui/src/routes/layout.css | 456 ++++ ui/src/routes/lineage/+page.svelte | 1579 ++++++++++++ ui/src/routes/providers/+page.svelte | 102 + ui/src/routes/providers/[id]/+page.svelte | 120 + ui/src/routes/runs/+page.svelte | 148 ++ ui/src/routes/runs/[id]/+page.svelte | 832 ++++++ ui/src/routes/triggers/+page.svelte | 189 ++ ui/static/favicon.png | Bin 0 -> 6618 bytes ui/static/logo-light.png | Bin 0 -> 83682 bytes ui/static/logo.png | Bin 0 -> 94277 bytes ui/static/robots.txt | 3 + ui/svelte.config.js | 21 + ui/tsconfig.json | 20 + ui/vite.config.ts | 5 + 127 files changed, 13306 insertions(+) create mode 100644 ui/.gitignore create mode 100644 ui/.npmrc create mode 100644 ui/README.md create mode 100644 ui/components.json create mode 100644 ui/package-lock.json create mode 100644 ui/package.json create mode 100644 ui/src/app.d.ts create mode 100644 ui/src/app.html create mode 100644 ui/src/lib/assets/favicon.svg create mode 100644 ui/src/lib/components/app-sidebar.svelte create mode 100644 ui/src/lib/components/app-topbar.svelte create mode 100644 ui/src/lib/components/code/code-preview.svelte create mode 100644 ui/src/lib/components/code/sql-viewer.svelte create mode 100644 ui/src/lib/components/lineage/asset-node.svelte create mode 100644 ui/src/lib/components/lineage/col-cell-node.svelte create mode 100644 ui/src/lib/components/lineage/inspector-section.svelte create mode 100644 ui/src/lib/components/lineage/node-fields-popover.svelte create mode 100644 ui/src/lib/components/list/filter-button.svelte create mode 100644 ui/src/lib/components/list/list-toolbar.svelte create mode 100644 ui/src/lib/components/list/relative-time.svelte create mode 100644 ui/src/lib/components/list/segmented-filter.svelte create mode 100644 ui/src/lib/components/list/status-label.svelte create mode 100644 ui/src/lib/components/timeline/run-timeline.svelte create mode 100644 ui/src/lib/components/ui/badge/badge.svelte create mode 100644 ui/src/lib/components/ui/badge/index.ts create mode 100644 ui/src/lib/components/ui/button/button.svelte create mode 100644 ui/src/lib/components/ui/button/index.ts create mode 100644 ui/src/lib/components/ui/card/card-action.svelte create mode 100644 ui/src/lib/components/ui/card/card-content.svelte create mode 100644 ui/src/lib/components/ui/card/card-description.svelte create mode 100644 ui/src/lib/components/ui/card/card-footer.svelte create mode 100644 ui/src/lib/components/ui/card/card-header.svelte create mode 100644 ui/src/lib/components/ui/card/card-title.svelte create mode 100644 ui/src/lib/components/ui/card/card.svelte create mode 100644 ui/src/lib/components/ui/card/index.ts create mode 100644 ui/src/lib/components/ui/checkbox/checkbox.svelte create mode 100644 ui/src/lib/components/ui/checkbox/index.ts create mode 100644 ui/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte create mode 100644 ui/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte create mode 100644 ui/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte create mode 100644 ui/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte create mode 100644 ui/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte create mode 100644 ui/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte create mode 100644 ui/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte create mode 100644 ui/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte create mode 100644 ui/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte create mode 100644 ui/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte create mode 100644 ui/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte create mode 100644 ui/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte create mode 100644 ui/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte create mode 100644 ui/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte create mode 100644 ui/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte create mode 100644 ui/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte create mode 100644 ui/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte create mode 100644 ui/src/lib/components/ui/dropdown-menu/index.ts create mode 100644 ui/src/lib/components/ui/popover/index.ts create mode 100644 ui/src/lib/components/ui/popover/popover-close.svelte create mode 100644 ui/src/lib/components/ui/popover/popover-content.svelte create mode 100644 ui/src/lib/components/ui/popover/popover-description.svelte create mode 100644 ui/src/lib/components/ui/popover/popover-header.svelte create mode 100644 ui/src/lib/components/ui/popover/popover-portal.svelte create mode 100644 ui/src/lib/components/ui/popover/popover-title.svelte create mode 100644 ui/src/lib/components/ui/popover/popover-trigger.svelte create mode 100644 ui/src/lib/components/ui/popover/popover.svelte create mode 100644 ui/src/lib/components/ui/separator/index.ts create mode 100644 ui/src/lib/components/ui/separator/separator.svelte create mode 100644 ui/src/lib/components/ui/table/index.ts create mode 100644 ui/src/lib/components/ui/table/table-body.svelte create mode 100644 ui/src/lib/components/ui/table/table-caption.svelte create mode 100644 ui/src/lib/components/ui/table/table-cell.svelte create mode 100644 ui/src/lib/components/ui/table/table-footer.svelte create mode 100644 ui/src/lib/components/ui/table/table-head.svelte create mode 100644 ui/src/lib/components/ui/table/table-header.svelte create mode 100644 ui/src/lib/components/ui/table/table-row.svelte create mode 100644 ui/src/lib/components/ui/table/table.svelte create mode 100644 ui/src/lib/components/ui/tabs/index.ts create mode 100644 ui/src/lib/components/ui/tabs/tabs-content.svelte create mode 100644 ui/src/lib/components/ui/tabs/tabs-list.svelte create mode 100644 ui/src/lib/components/ui/tabs/tabs-trigger.svelte create mode 100644 ui/src/lib/components/ui/tabs/tabs.svelte create mode 100644 ui/src/lib/components/ui/tooltip/index.ts create mode 100644 ui/src/lib/components/ui/tooltip/tooltip-content.svelte create mode 100644 ui/src/lib/components/ui/tooltip/tooltip-portal.svelte create mode 100644 ui/src/lib/components/ui/tooltip/tooltip-provider.svelte create mode 100644 ui/src/lib/components/ui/tooltip/tooltip-trigger.svelte create mode 100644 ui/src/lib/components/ui/tooltip/tooltip.svelte create mode 100644 ui/src/lib/config/config-form.svelte create mode 100644 ui/src/lib/index.ts create mode 100644 ui/src/lib/lineage/column-lineage.svelte create mode 100644 ui/src/lib/lineage/flow-controller.svelte create mode 100644 ui/src/lib/lineage/graph-draw.ts create mode 100644 ui/src/lib/lineage/graph-layout.ts create mode 100644 ui/src/lib/lineage/layout.ts create mode 100644 ui/src/lib/lineage/node-fields.svelte.ts create mode 100644 ui/src/lib/lineage/scoped-lineage.svelte create mode 100644 ui/src/lib/lineage/trace.ts create mode 100644 ui/src/lib/mock/model-sql.ts create mode 100644 ui/src/lib/mock/scale-graph.ts create mode 100644 ui/src/lib/mock/timeline.ts create mode 100644 ui/src/lib/run/run-panel.svelte create mode 100644 ui/src/lib/sql/diff.ts create mode 100644 ui/src/lib/sql/highlight.ts create mode 100644 ui/src/lib/sql/outline.ts create mode 100644 ui/src/lib/theme.svelte.ts create mode 100644 ui/src/lib/utils.ts create mode 100644 ui/src/routes/+layout.svelte create mode 100644 ui/src/routes/+layout.ts create mode 100644 ui/src/routes/+page.svelte create mode 100644 ui/src/routes/catalog/+page.svelte create mode 100644 ui/src/routes/catalog/[name]/+page.svelte create mode 100644 ui/src/routes/checks/+page.svelte create mode 100644 ui/src/routes/jobs/+page.svelte create mode 100644 ui/src/routes/jobs/[name]/+page.svelte create mode 100644 ui/src/routes/layout.css create mode 100644 ui/src/routes/lineage/+page.svelte create mode 100644 ui/src/routes/providers/+page.svelte create mode 100644 ui/src/routes/providers/[id]/+page.svelte create mode 100644 ui/src/routes/runs/+page.svelte create mode 100644 ui/src/routes/runs/[id]/+page.svelte create mode 100644 ui/src/routes/triggers/+page.svelte create mode 100644 ui/static/favicon.png create mode 100644 ui/static/logo-light.png create mode 100644 ui/static/logo.png create mode 100644 ui/static/robots.txt create mode 100644 ui/svelte.config.js create mode 100644 ui/tsconfig.json create mode 100644 ui/vite.config.ts diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 0000000000..dccd4e2705 --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.svelte-kit/ +build/ +.vite/ +vite.config.ts.timestamp-* diff --git a/ui/.npmrc b/ui/.npmrc new file mode 100644 index 0000000000..b6f27f1359 --- /dev/null +++ b/ui/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 0000000000..ee51449d9e --- /dev/null +++ b/ui/README.md @@ -0,0 +1,22 @@ +# SQLBuild UI + +The SQLBuild orchestration web UI: a SvelteKit single-page app built to static +assets (`adapter-static`, SPA fallback, no SSR). + +This is an early, preliminary version. Pages read from a typed data layer in +`src/lib/api/`; until the server API exists, that layer is backed by an +in-memory mock client with neutral example data. + +## Develop + +```sh +npm ci +npm run dev +``` + +## Check and build + +```sh +npm run check +npm run build # writes static assets to build/ +``` diff --git a/ui/components.json b/ui/components.json new file mode 100644 index 0000000000..5e6e1e67aa --- /dev/null +++ b/ui/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://shadcn-svelte.com/schema.json", + "tailwind": { + "css": "src/routes/layout.css", + "baseColor": "neutral" + }, + "aliases": { + "components": "$lib/components", + "utils": "$lib/utils", + "ui": "$lib/components/ui", + "hooks": "$lib/hooks", + "lib": "$lib" + }, + "typescript": true, + "registry": "https://shadcn-svelte.com/registry", + "style": "vega", + "iconLibrary": "lucide", + "menuColor": "default", + "menuAccent": "subtle" +} diff --git a/ui/package-lock.json b/ui/package-lock.json new file mode 100644 index 0000000000..596a6dcf23 --- /dev/null +++ b/ui/package-lock.json @@ -0,0 +1,2235 @@ +{ + "name": "sqlbuild-ui", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sqlbuild-ui", + "version": "0.0.1", + "dependencies": { + "@dagrejs/dagre": "^3.0.0", + "@fontsource-variable/space-grotesk": "^5.2.10", + "@fontsource/ibm-plex-mono": "^5.2.7", + "@xyflow/svelte": "^1.6.0", + "prismjs": "^1.30.0" + }, + "devDependencies": { + "@fontsource-variable/inter": "^5.2.8", + "@internationalized/date": "^3.12.2", + "@lucide/svelte": "^1.17.0", + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.57.0", + "@sveltejs/vite-plugin-svelte": "^7.0.0", + "@tailwindcss/typography": "^0.5.19", + "@tailwindcss/vite": "^4.2.2", + "@types/prismjs": "^1.26.6", + "bits-ui": "^2.18.1", + "clsx": "^2.1.1", + "shadcn-svelte": "^1.3.0", + "svelte": "^5.55.2", + "svelte-check": "^4.4.6", + "tailwind-merge": "^3.6.0", + "tailwind-variants": "^3.2.2", + "tailwindcss": "^4.2.2", + "tw-animate-css": "^1.4.0", + "typescript": "^6.0.2", + "vite": "^8.0.7" + } + }, + "node_modules/@dagrejs/dagre": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-3.0.0.tgz", + "integrity": "sha512-ZzhnTy1rfuoew9Ez3EIw4L2znPGnYYhfn8vc9c4oB8iw6QAsszbiU0vRhlxWPFnmmNSFAkrYeF1PhM5m4lAN0Q==", + "license": "MIT", + "dependencies": { + "@dagrejs/graphlib": "4.0.1" + } + }, + "node_modules/@dagrejs/graphlib": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-4.0.1.tgz", + "integrity": "sha512-IvcV6FduIIAmLwnH+yun+QtV36SC7mERqa86aClNqmMN09WhmPPYU8ckHrZBozErf+UvHPWOTJYaGYiIcs0DgA==", + "license": "MIT" + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@fontsource-variable/inter": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.2.8.tgz", + "integrity": "sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==", + "dev": true, + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource-variable/space-grotesk": { + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/@fontsource-variable/space-grotesk/-/space-grotesk-5.2.10.tgz", + "integrity": "sha512-yJQO/o35/hAP3CFnpdFTwQku2yzJOae2HIpBmqkOVoxhhXJaQP3g+b6Jrz7u+eI7A5ZdCIf88uMWpBJdFiGr5w==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/ibm-plex-mono": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.2.7.tgz", + "integrity": "sha512-MKAb8qV+CaiMQn2B0dIi1OV3565NYzp3WN5b4oT6LTkk+F0jR6j0ZN+5BKJiIhffDC3rtBULsYZE65+0018z9w==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@internationalized/date": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.2.tgz", + "integrity": "sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lucide/svelte": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-1.17.0.tgz", + "integrity": "sha512-q06YCFBN5CO8cd1ADmLCxWRVMVb7xxvHzqC0lvNoxGa+FLW6Cd1Y1AOxgbQk4Iwe68vkAMCRveNHint4WoaVKg==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "svelte": "^5" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@svelte-put/shortcut": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@svelte-put/shortcut/-/shortcut-4.1.0.tgz", + "integrity": "sha512-wImNEIkbxAIWFqlfuhcbC+jRPDeRa/uJGIXHMEVVD+jqL9xCwWNnkGQJ6Qb2XVszuRLHlb8SGZDL3Io/h3vs8w==", + "license": "MIT", + "peerDependencies": { + "svelte": "^5.1.0" + } + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.10.tgz", + "integrity": "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-static": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz", + "integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.61.1", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.61.1.tgz", + "integrity": "sha512-Ny8s1SR1TyQS2hD2Rvw0XKzU2Nw1eUF52dTb6T2bdcgz7wSC+Nyb5IwjWYlR4b2dvbbR5NJDiQwHg3rnNseghg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@sveltejs/acorn-typescript": "^1.0.9", + "@types/cookie": "^0.6.0", + "acorn": "^8.16.0", + "cookie": "^0.6.0", + "devalue": "^5.8.1", + "esm-env": "^1.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "set-cookie-parser": "^3.0.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": "^5.3.3 || ^6.0.0", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.1.2.tgz", + "integrity": "sha512-DrUBA2UXRfDmUX/ZTiEopd3X40yavsJF1FX2RygcuIScHL7o5YX1fMvoYnDhjeJQC4weCOklirpNWlcb2NiSeA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "deepmerge": "^4.3.1", + "magic-string": "^0.30.21", + "obug": "^2.1.0", + "vitefu": "^1.1.2" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.46.4", + "vite": "^8.0.0-beta.7 || ^8.0.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", + "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz", + "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "tailwindcss": "4.3.0" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/prismjs": { + "version": "1.26.6", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/@xyflow/svelte": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@xyflow/svelte/-/svelte-1.6.0.tgz", + "integrity": "sha512-msCT95OqTThh7gWhdFJ03Qzhc0Q8QYfA8K03M4I+KTtHdvatoTuRo9ofkFHJDbMfS243y+fOs5TCvU9T1kEUUA==", + "license": "MIT", + "dependencies": { + "@svelte-put/shortcut": "^4.1.0", + "@xyflow/system": "0.0.77" + }, + "peerDependencies": { + "svelte": "^5.25.0" + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.77", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.77.tgz", + "integrity": "sha512-qCDCMCQAAgUu8yHnhloHG9F5mwPX5E+Wl8McpYIOPSSXfzFJJoZcwOcsDiAjitVKIg2de1WmJbCHfpcvxprsgg==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/bits-ui": { + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.18.1.tgz", + "integrity": "sha512-KkemzKFH4T3gt3H+P86JcnAWExjByv/6vlwjm/BoCwTPHu03yiCdxbghdJLvFReQTe0acCAiRcKfmixxD6XvlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.1", + "@floating-ui/dom": "^1.7.1", + "esm-env": "^1.1.2", + "runed": "^0.35.1", + "svelte-toolbelt": "^0.10.6", + "tabbable": "^6.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/huntabyte" + }, + "peerDependencies": { + "@internationalized/date": "^3.8.1", + "svelte": "^5.33.0" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", + "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.22.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.1.tgz", + "integrity": "sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.9.tgz", + "integrity": "sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "license": "MIT" + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/runed": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.35.1.tgz", + "integrity": "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "esm-env": "^1.0.0", + "lz-string": "^1.5.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.21.0", + "svelte": "^5.7.0" + }, + "peerDependenciesMeta": { + "@sveltejs/kit": { + "optional": true + } + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/set-cookie-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", + "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/shadcn-svelte": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/shadcn-svelte/-/shadcn-svelte-1.3.0.tgz", + "integrity": "sha512-Pd4ICWTkTks/b2YU4c9vF2XsX1x5HFPRl5bKszS1LcnWS83x+7T4WiIvbWz8Qh9knkcGZ+SCz1+Dmhdq+AYooA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.0", + "node-fetch-native": "^1.6.4", + "postcss": "^8.5.5", + "tailwind-merge": "^3.0.0" + }, + "bin": { + "shadcn-svelte": "dist/index.mjs" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/svelte": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.0.tgz", + "integrity": "sha512-kTXr26t1bchFp28ROrb957LtbujpBmBDibmqMGziVpUs7awBi96TGgX6SovrA8BNoEUDVRK2Fb9FkeYlGspoVg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.10", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.8.1", + "esm-env": "^1.2.1", + "esrap": "^2.2.9", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.5.0.tgz", + "integrity": "sha512-9lNwPxCLWniFvQIcEv1LFqjIxcFtO3smb5+5BKbRJ3ttL4o2lXCej5rLF4DAnfLPI66oaA81vAxw6ILdIWI7kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/svelte-toolbelt": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz", + "integrity": "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte" + ], + "dependencies": { + "clsx": "^2.1.1", + "runed": "^0.35.1", + "style-to-object": "^1.0.8" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.7.0" + }, + "peerDependencies": { + "svelte": "^5.30.2" + } + }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwind-variants": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.2.2.tgz", + "integrity": "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.x", + "pnpm": ">=7.x" + }, + "peerDependencies": { + "tailwind-merge": ">=3.0.0", + "tailwindcss": "*" + }, + "peerDependenciesMeta": { + "tailwind-merge": { + "optional": true + } + } + }, + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.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" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "license": "MIT" + } + } +} diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 0000000000..7d6a17a08b --- /dev/null +++ b/ui/package.json @@ -0,0 +1,43 @@ +{ + "name": "sqlbuild-ui", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" + }, + "devDependencies": { + "@fontsource-variable/inter": "^5.2.8", + "@internationalized/date": "^3.12.2", + "@lucide/svelte": "^1.17.0", + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.57.0", + "@sveltejs/vite-plugin-svelte": "^7.0.0", + "@tailwindcss/typography": "^0.5.19", + "@tailwindcss/vite": "^4.2.2", + "@types/prismjs": "^1.26.6", + "bits-ui": "^2.18.1", + "clsx": "^2.1.1", + "shadcn-svelte": "^1.3.0", + "svelte": "^5.55.2", + "svelte-check": "^4.4.6", + "tailwind-merge": "^3.6.0", + "tailwind-variants": "^3.2.2", + "tailwindcss": "^4.2.2", + "tw-animate-css": "^1.4.0", + "typescript": "^6.0.2", + "vite": "^8.0.7" + }, + "dependencies": { + "@dagrejs/dagre": "^3.0.0", + "@fontsource-variable/space-grotesk": "^5.2.10", + "@fontsource/ibm-plex-mono": "^5.2.7", + "@xyflow/svelte": "^1.6.0", + "prismjs": "^1.30.0" + } +} diff --git a/ui/src/app.d.ts b/ui/src/app.d.ts new file mode 100644 index 0000000000..da08e6da59 --- /dev/null +++ b/ui/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/ui/src/app.html b/ui/src/app.html new file mode 100644 index 0000000000..b9a7abfef4 --- /dev/null +++ b/ui/src/app.html @@ -0,0 +1,23 @@ + + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/ui/src/lib/assets/favicon.svg b/ui/src/lib/assets/favicon.svg new file mode 100644 index 0000000000..cc5dc66a3a --- /dev/null +++ b/ui/src/lib/assets/favicon.svg @@ -0,0 +1 @@ +svelte-logo \ No newline at end of file diff --git a/ui/src/lib/components/app-sidebar.svelte b/ui/src/lib/components/app-sidebar.svelte new file mode 100644 index 0000000000..2240d637c7 --- /dev/null +++ b/ui/src/lib/components/app-sidebar.svelte @@ -0,0 +1,141 @@ + + + + + diff --git a/ui/src/lib/components/app-topbar.svelte b/ui/src/lib/components/app-topbar.svelte new file mode 100644 index 0000000000..9f0abc4004 --- /dev/null +++ b/ui/src/lib/components/app-topbar.svelte @@ -0,0 +1,42 @@ + + +
+

{title}

+ {breadcrumb} +
+ + + Synced 4s ago + + {#if children}{@render children()}{/if} + +
+
+ + diff --git a/ui/src/lib/components/code/code-preview.svelte b/ui/src/lib/components/code/code-preview.svelte new file mode 100644 index 0000000000..7c78275902 --- /dev/null +++ b/ui/src/lib/components/code/code-preview.svelte @@ -0,0 +1,150 @@ + + +
+
+ {file} + {#if language === 'sql'} +
+ {#each [['raw', 'Raw'], ['compiled', 'Compiled']] as [m, label] (m)} + + {/each} +
+ {/if} + {#if onOpen} + + {/if} +
+
+ {#each lines.slice(0, maxLines) as line, i (i)} +
{i + 1}{@html line || ' '}
+ {/each} +
+ {#if lines.length > maxLines && onOpen} + + {/if} +
+ + diff --git a/ui/src/lib/components/code/sql-viewer.svelte b/ui/src/lib/components/code/sql-viewer.svelte new file mode 100644 index 0000000000..a448e42537 --- /dev/null +++ b/ui/src/lib/components/code/sql-viewer.svelte @@ -0,0 +1,655 @@ + + + e.key === 'Escape' && onClose?.()} /> + +
+
+ {name}.sql + {raw.split('\n').length} lines +
+ {#each [['raw', 'Raw'], ['compiled', 'Compiled'], ['diff', 'Side by side'], ...(versions.length ? [['drift', 'Drift']] : [])] as [m, label] (m)} + + {/each} +
+ + + {#if onClose} + + {/if} +
+ + {#if mode === 'drift'} +
+ +
+ {#each [['compiled', 'Compiled'], ['raw', 'Raw']] as [v, l] (v)} + + {/each} +
+
+ {#each [['unified', 'Unified'], ['split', 'Split']] as [v, l] (v)} + + {/each} +
+ +{stats.added}−{stats.removed} + +
+
{explanation.text}
+ {/if} + +
+ {#if mode !== 'diff' && mode !== 'drift'} + + {/if} + + + {#if mode === 'drift'} +
+ {#if stats.added + stats.removed === 0} +
No {driftSide} SQL differences from {baseline?.label ?? 'the baseline'}.
+ {:else if layout === 'unified'} +
+ {#each driftRows as r (r.op === 'fold' ? r.key : `${r.op}${r.a ?? ''}-${r.b ?? ''}`)} + {#if r.op === 'fold'} + + {:else} +
+ {r.a ?? ''}{r.b ?? ''} + {r.op === 'add' ? '+' : r.op === 'del' ? '−' : ''} + {@html lineHtml(r)} +
+ {/if} + {/each} +
+ {:else} +
+ {#each splitRows as r (r.key)} + {#if r.fold} + + {:else} +
+
+ {r.left?.a ?? ''}{@html r.left ? (r.left.op === 'same' ? afterHl[(r.left.b ?? 1) - 1] : lineHtml(r.left)) || ' ' : ' '} +
+
+ {r.right?.b ?? ''}{@html r.right ? lineHtml(r.right) : ' '} +
+
+ {/if} + {/each} +
+ {/if} +
+ {:else} +
+
+ {#if mode === 'diff'}
Raw
{/if} + {#each primaryLines as line, i (i)} +
+ {i + 1}{@html line || ' '} +
+ {/each} +
+ {#if mode === 'diff'} +
+
Compiled
+ {#each compiledLines as line, i (i)} +
{i + 1}{@html line || ' '}
+ {/each} +
+ {/if} +
+ {/if} +
+
+ + diff --git a/ui/src/lib/components/lineage/asset-node.svelte b/ui/src/lib/components/lineage/asset-node.svelte new file mode 100644 index 0000000000..be1e2b1a2e --- /dev/null +++ b/ui/src/lib/components/lineage/asset-node.svelte @@ -0,0 +1,245 @@ + + +
+ + +
+ + {d.name} + {#if f.kind}{kindShort}{/if} +
+ + {#if hasDetail} +
+ {#if f.description && d.description}

{d.description}

{/if} + {#if f.owner && d.owner} {d.owner}{/if} + {#if f.columns && d.columnCount != null} {d.columnCount} columns{/if} + {#if f.tags && d.tags?.length}{d.tags.join(', ')}{/if} +
+ {/if} + + {#if f.status || (f.checks && d.checks) || (f.freshness && when)} +
+ {#if f.status} + + {statusLabel[d.status]} + {/if} + {#if f.status && when}{when}{/if} + {#if d.drift} + {d.drift === 'upstream' ? 'upstream' : 'drift'} + {/if} + {#if f.checks && d.checks} + {d.checks} + {/if} +
+ {/if} + + +
+ + diff --git a/ui/src/lib/components/lineage/col-cell-node.svelte b/ui/src/lib/components/lineage/col-cell-node.svelte new file mode 100644 index 0000000000..c1f24b1380 --- /dev/null +++ b/ui/src/lib/components/lineage/col-cell-node.svelte @@ -0,0 +1,54 @@ + + +
+ + +
+ {d.column} +
+ +
+ + {d.model} + {#if d.lens && d.lensLabel} + {d.lensLabel[d.lens]} + {:else} + {d.kind} + {/if} +
+ +
diff --git a/ui/src/lib/components/lineage/inspector-section.svelte b/ui/src/lib/components/lineage/inspector-section.svelte new file mode 100644 index 0000000000..2254e45172 --- /dev/null +++ b/ui/src/lib/components/lineage/inspector-section.svelte @@ -0,0 +1,68 @@ + + +
+
+ + {#if meta}
{@render meta()}
{/if} +
+ {#if open}
{@render children()}
{/if} +
+ + diff --git a/ui/src/lib/components/lineage/node-fields-popover.svelte b/ui/src/lib/components/lineage/node-fields-popover.svelte new file mode 100644 index 0000000000..bbbd695444 --- /dev/null +++ b/ui/src/lib/components/lineage/node-fields-popover.svelte @@ -0,0 +1,37 @@ + + + + + Node fields + {count} + + +
+ Show on nodes +
+
+ {#each NODE_FIELDS as field (field.key)} + + {/each} +
+
+ +
+
+
diff --git a/ui/src/lib/components/list/filter-button.svelte b/ui/src/lib/components/list/filter-button.svelte new file mode 100644 index 0000000000..c3d75e676e --- /dev/null +++ b/ui/src/lib/components/list/filter-button.svelte @@ -0,0 +1,36 @@ + + + + + diff --git a/ui/src/lib/components/list/list-toolbar.svelte b/ui/src/lib/components/list/list-toolbar.svelte new file mode 100644 index 0000000000..2d97289837 --- /dev/null +++ b/ui/src/lib/components/list/list-toolbar.svelte @@ -0,0 +1,117 @@ + + +
+ + {#if filters}
{@render filters()}
{/if} + {#if actions}{@render actions()}{/if} +
+ {#each counts.filter((c) => c.count > 0) as c (c.label)} + {c.count} {c.label} + {/each} + {total} +
+
+ + diff --git a/ui/src/lib/components/list/relative-time.svelte b/ui/src/lib/components/list/relative-time.svelte new file mode 100644 index 0000000000..afb0173575 --- /dev/null +++ b/ui/src/lib/components/list/relative-time.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/ui/src/lib/components/list/segmented-filter.svelte b/ui/src/lib/components/list/segmented-filter.svelte new file mode 100644 index 0000000000..cd86222f3f --- /dev/null +++ b/ui/src/lib/components/list/segmented-filter.svelte @@ -0,0 +1,52 @@ + + +
+ {#each options as o (o.value)} + + {/each} +
+ + diff --git a/ui/src/lib/components/list/status-label.svelte b/ui/src/lib/components/list/status-label.svelte new file mode 100644 index 0000000000..0e5209c961 --- /dev/null +++ b/ui/src/lib/components/list/status-label.svelte @@ -0,0 +1,27 @@ + + +{label} + + diff --git a/ui/src/lib/components/timeline/run-timeline.svelte b/ui/src/lib/components/timeline/run-timeline.svelte new file mode 100644 index 0000000000..8ad84b57e5 --- /dev/null +++ b/ui/src/lib/components/timeline/run-timeline.svelte @@ -0,0 +1,517 @@ + + +
+
+ {#if showSearch} + + {/if} +
+ {#each Object.keys(WINDOWS) as w (w)} + + {/each} +
+
+ + + +
+ {#if actions}
{@render actions()}
{/if} +
+ +
+
+
Jobs {jobs.length}
+
+
+ {#each days as d (d.label)}{d.label}{/each} + {#if nowPct >= 0 && nowPct <= 100} +
+ {#if counts.running}{counts.running}{/if} + {counts.ok} + {#if counts.warn}{counts.warn}{/if} + {#if counts.fail}{counts.fail}{/if} +
+ {/if} +
+
+ {#each ticks.filter((t) => pct(t.t) < 97 && Math.abs(pct(t.t) - nowPct) > 3) as t (t.t)}{t.label}{/each} + {#if nowPct >= 0 && nowPct <= 100}Now{/if} +
+
+
+ +
+
+ {#each ticks as t (t.t)}{/each} + {#if nowPct >= 0 && nowPct <= 100}{/if} +
+ {#each jobs as job (job.name)} + + {:else} +
No jobs match “{query}”.
+ {/each} +
+
+
+ + + +{#if hover} + {@const r = hover.run} +
+
{label[r.status]}{r.job}
+
+ {#if r.status !== 'scheduled'}
Run
{r.id}
{/if} +
{r.status === 'scheduled' ? 'Next tick' : 'Started'}
{clock(r.start)}
+
Duration
{r.status === 'scheduled' ? `~${duration(r)} typical` : r.status === 'running' ? `${duration(r)} so far` : duration(r)}
+
Trigger
{r.trigger}
+
+
+{/if} + + diff --git a/ui/src/lib/components/ui/badge/badge.svelte b/ui/src/lib/components/ui/badge/badge.svelte new file mode 100644 index 0000000000..51bbc23e92 --- /dev/null +++ b/ui/src/lib/components/ui/badge/badge.svelte @@ -0,0 +1,49 @@ + + + + + + {@render children?.()} + diff --git a/ui/src/lib/components/ui/badge/index.ts b/ui/src/lib/components/ui/badge/index.ts new file mode 100644 index 0000000000..64e0aa9b08 --- /dev/null +++ b/ui/src/lib/components/ui/badge/index.ts @@ -0,0 +1,2 @@ +export { default as Badge } from "./badge.svelte"; +export { badgeVariants, type BadgeVariant } from "./badge.svelte"; diff --git a/ui/src/lib/components/ui/button/button.svelte b/ui/src/lib/components/ui/button/button.svelte new file mode 100644 index 0000000000..c38862edb6 --- /dev/null +++ b/ui/src/lib/components/ui/button/button.svelte @@ -0,0 +1,82 @@ + + + + +{#if href} + + {@render children?.()} + +{:else} + +{/if} diff --git a/ui/src/lib/components/ui/button/index.ts b/ui/src/lib/components/ui/button/index.ts new file mode 100644 index 0000000000..fb585d7688 --- /dev/null +++ b/ui/src/lib/components/ui/button/index.ts @@ -0,0 +1,17 @@ +import Root, { + type ButtonProps, + type ButtonSize, + type ButtonVariant, + buttonVariants, +} from "./button.svelte"; + +export { + Root, + type ButtonProps as Props, + // + Root as Button, + buttonVariants, + type ButtonProps, + type ButtonSize, + type ButtonVariant, +}; diff --git a/ui/src/lib/components/ui/card/card-action.svelte b/ui/src/lib/components/ui/card/card-action.svelte new file mode 100644 index 0000000000..7c48844a8f --- /dev/null +++ b/ui/src/lib/components/ui/card/card-action.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/card/card-content.svelte b/ui/src/lib/components/ui/card/card-content.svelte new file mode 100644 index 0000000000..082a7861ba --- /dev/null +++ b/ui/src/lib/components/ui/card/card-content.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/card/card-description.svelte b/ui/src/lib/components/ui/card/card-description.svelte new file mode 100644 index 0000000000..9b20ac701f --- /dev/null +++ b/ui/src/lib/components/ui/card/card-description.svelte @@ -0,0 +1,20 @@ + + +

+ {@render children?.()} +

diff --git a/ui/src/lib/components/ui/card/card-footer.svelte b/ui/src/lib/components/ui/card/card-footer.svelte new file mode 100644 index 0000000000..591c3f7189 --- /dev/null +++ b/ui/src/lib/components/ui/card/card-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/card/card-header.svelte b/ui/src/lib/components/ui/card/card-header.svelte new file mode 100644 index 0000000000..21e9a170ab --- /dev/null +++ b/ui/src/lib/components/ui/card/card-header.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/card/card-title.svelte b/ui/src/lib/components/ui/card/card-title.svelte new file mode 100644 index 0000000000..7d20243be9 --- /dev/null +++ b/ui/src/lib/components/ui/card/card-title.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/card/card.svelte b/ui/src/lib/components/ui/card/card.svelte new file mode 100644 index 0000000000..ba46c045c6 --- /dev/null +++ b/ui/src/lib/components/ui/card/card.svelte @@ -0,0 +1,22 @@ + + +
img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg group/card flex flex-col", className)} + {...restProps} +> + {@render children?.()} +
diff --git a/ui/src/lib/components/ui/card/index.ts b/ui/src/lib/components/ui/card/index.ts new file mode 100644 index 0000000000..4d3fce489d --- /dev/null +++ b/ui/src/lib/components/ui/card/index.ts @@ -0,0 +1,25 @@ +import Root from "./card.svelte"; +import Content from "./card-content.svelte"; +import Description from "./card-description.svelte"; +import Footer from "./card-footer.svelte"; +import Header from "./card-header.svelte"; +import Title from "./card-title.svelte"; +import Action from "./card-action.svelte"; + +export { + Root, + Content, + Description, + Footer, + Header, + Title, + Action, + // + Root as Card, + Content as CardContent, + Description as CardDescription, + Footer as CardFooter, + Header as CardHeader, + Title as CardTitle, + Action as CardAction, +}; diff --git a/ui/src/lib/components/ui/checkbox/checkbox.svelte b/ui/src/lib/components/ui/checkbox/checkbox.svelte new file mode 100644 index 0000000000..e26543b121 --- /dev/null +++ b/ui/src/lib/components/ui/checkbox/checkbox.svelte @@ -0,0 +1,39 @@ + + + + {#snippet children({ checked, indeterminate })} +
+ {#if checked} + + {:else if indeterminate} + + {/if} +
+ {/snippet} +
diff --git a/ui/src/lib/components/ui/checkbox/index.ts b/ui/src/lib/components/ui/checkbox/index.ts new file mode 100644 index 0000000000..6d92d94580 --- /dev/null +++ b/ui/src/lib/components/ui/checkbox/index.ts @@ -0,0 +1,6 @@ +import Root from "./checkbox.svelte"; +export { + Root, + // + Root as Checkbox, +}; diff --git a/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte new file mode 100644 index 0000000000..e0e1971879 --- /dev/null +++ b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte new file mode 100644 index 0000000000..c04d294e02 --- /dev/null +++ b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte @@ -0,0 +1,44 @@ + + + + {#snippet children({ checked, indeterminate })} + + {#if indeterminate} + + {:else if checked} + + {/if} + + {@render childrenProp?.()} + {/snippet} + diff --git a/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte new file mode 100644 index 0000000000..369bab1859 --- /dev/null +++ b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte @@ -0,0 +1,31 @@ + + + + + diff --git a/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte new file mode 100644 index 0000000000..433540fd25 --- /dev/null +++ b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte @@ -0,0 +1,22 @@ + + + diff --git a/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte new file mode 100644 index 0000000000..aca1f7bd55 --- /dev/null +++ b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte @@ -0,0 +1,7 @@ + + + diff --git a/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte new file mode 100644 index 0000000000..0fec5f5cb1 --- /dev/null +++ b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte @@ -0,0 +1,27 @@ + + + diff --git a/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte new file mode 100644 index 0000000000..ad6947a176 --- /dev/null +++ b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte @@ -0,0 +1,24 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte new file mode 100644 index 0000000000..274cfef78c --- /dev/null +++ b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte new file mode 100644 index 0000000000..189aef40e4 --- /dev/null +++ b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte new file mode 100644 index 0000000000..d3888ccfb6 --- /dev/null +++ b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte @@ -0,0 +1,34 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.({ checked })} + {/snippet} + diff --git a/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte new file mode 100644 index 0000000000..90f1b6f10a --- /dev/null +++ b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte new file mode 100644 index 0000000000..ed7cc85ab5 --- /dev/null +++ b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte new file mode 100644 index 0000000000..254de664b9 --- /dev/null +++ b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte @@ -0,0 +1,17 @@ + + + diff --git a/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte new file mode 100644 index 0000000000..a390e2d483 --- /dev/null +++ b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte new file mode 100644 index 0000000000..f044581392 --- /dev/null +++ b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte @@ -0,0 +1,7 @@ + + + diff --git a/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte new file mode 100644 index 0000000000..cb053444d7 --- /dev/null +++ b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/ui/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte new file mode 100644 index 0000000000..cb4bc621c2 --- /dev/null +++ b/ui/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte @@ -0,0 +1,7 @@ + + + diff --git a/ui/src/lib/components/ui/dropdown-menu/index.ts b/ui/src/lib/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000000..7850c6a300 --- /dev/null +++ b/ui/src/lib/components/ui/dropdown-menu/index.ts @@ -0,0 +1,54 @@ +import Root from "./dropdown-menu.svelte"; +import Sub from "./dropdown-menu-sub.svelte"; +import CheckboxGroup from "./dropdown-menu-checkbox-group.svelte"; +import CheckboxItem from "./dropdown-menu-checkbox-item.svelte"; +import Content from "./dropdown-menu-content.svelte"; +import Group from "./dropdown-menu-group.svelte"; +import Item from "./dropdown-menu-item.svelte"; +import Label from "./dropdown-menu-label.svelte"; +import RadioGroup from "./dropdown-menu-radio-group.svelte"; +import RadioItem from "./dropdown-menu-radio-item.svelte"; +import Separator from "./dropdown-menu-separator.svelte"; +import Shortcut from "./dropdown-menu-shortcut.svelte"; +import Trigger from "./dropdown-menu-trigger.svelte"; +import SubContent from "./dropdown-menu-sub-content.svelte"; +import SubTrigger from "./dropdown-menu-sub-trigger.svelte"; +import GroupHeading from "./dropdown-menu-group-heading.svelte"; +import Portal from "./dropdown-menu-portal.svelte"; + +export { + CheckboxGroup, + CheckboxItem, + Content, + Portal, + Root as DropdownMenu, + CheckboxGroup as DropdownMenuCheckboxGroup, + CheckboxItem as DropdownMenuCheckboxItem, + Content as DropdownMenuContent, + Portal as DropdownMenuPortal, + Group as DropdownMenuGroup, + Item as DropdownMenuItem, + Label as DropdownMenuLabel, + RadioGroup as DropdownMenuRadioGroup, + RadioItem as DropdownMenuRadioItem, + Separator as DropdownMenuSeparator, + Shortcut as DropdownMenuShortcut, + Sub as DropdownMenuSub, + SubContent as DropdownMenuSubContent, + SubTrigger as DropdownMenuSubTrigger, + Trigger as DropdownMenuTrigger, + GroupHeading as DropdownMenuGroupHeading, + Group, + GroupHeading, + Item, + Label, + RadioGroup, + RadioItem, + Root, + Separator, + Shortcut, + Sub, + SubContent, + SubTrigger, + Trigger, +}; diff --git a/ui/src/lib/components/ui/popover/index.ts b/ui/src/lib/components/ui/popover/index.ts new file mode 100644 index 0000000000..5f18036395 --- /dev/null +++ b/ui/src/lib/components/ui/popover/index.ts @@ -0,0 +1,28 @@ +import Root from "./popover.svelte"; +import Close from "./popover-close.svelte"; +import Content from "./popover-content.svelte"; +import Description from "./popover-description.svelte"; +import Header from "./popover-header.svelte"; +import Title from "./popover-title.svelte"; +import Trigger from "./popover-trigger.svelte"; +import Portal from "./popover-portal.svelte"; + +export { + Root, + Content, + Description, + Header, + Title, + Trigger, + Close, + Portal, + // + Root as Popover, + Content as PopoverContent, + Description as PopoverDescription, + Header as PopoverHeader, + Title as PopoverTitle, + Trigger as PopoverTrigger, + Close as PopoverClose, + Portal as PopoverPortal, +}; diff --git a/ui/src/lib/components/ui/popover/popover-close.svelte b/ui/src/lib/components/ui/popover/popover-close.svelte new file mode 100644 index 0000000000..c360925ac7 --- /dev/null +++ b/ui/src/lib/components/ui/popover/popover-close.svelte @@ -0,0 +1,7 @@ + + + diff --git a/ui/src/lib/components/ui/popover/popover-content.svelte b/ui/src/lib/components/ui/popover/popover-content.svelte new file mode 100644 index 0000000000..baab39748b --- /dev/null +++ b/ui/src/lib/components/ui/popover/popover-content.svelte @@ -0,0 +1,31 @@ + + + + + diff --git a/ui/src/lib/components/ui/popover/popover-description.svelte b/ui/src/lib/components/ui/popover/popover-description.svelte new file mode 100644 index 0000000000..c28ab236ee --- /dev/null +++ b/ui/src/lib/components/ui/popover/popover-description.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/popover/popover-header.svelte b/ui/src/lib/components/ui/popover/popover-header.svelte new file mode 100644 index 0000000000..b53953daa9 --- /dev/null +++ b/ui/src/lib/components/ui/popover/popover-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/popover/popover-portal.svelte b/ui/src/lib/components/ui/popover/popover-portal.svelte new file mode 100644 index 0000000000..dd8265fe80 --- /dev/null +++ b/ui/src/lib/components/ui/popover/popover-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/ui/src/lib/components/ui/popover/popover-title.svelte b/ui/src/lib/components/ui/popover/popover-title.svelte new file mode 100644 index 0000000000..ee9f2ff54d --- /dev/null +++ b/ui/src/lib/components/ui/popover/popover-title.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/ui/src/lib/components/ui/popover/popover-trigger.svelte b/ui/src/lib/components/ui/popover/popover-trigger.svelte new file mode 100644 index 0000000000..586323c719 --- /dev/null +++ b/ui/src/lib/components/ui/popover/popover-trigger.svelte @@ -0,0 +1,17 @@ + + + diff --git a/ui/src/lib/components/ui/popover/popover.svelte b/ui/src/lib/components/ui/popover/popover.svelte new file mode 100644 index 0000000000..6b1aa5f994 --- /dev/null +++ b/ui/src/lib/components/ui/popover/popover.svelte @@ -0,0 +1,7 @@ + + + diff --git a/ui/src/lib/components/ui/separator/index.ts b/ui/src/lib/components/ui/separator/index.ts new file mode 100644 index 0000000000..82442d2cdc --- /dev/null +++ b/ui/src/lib/components/ui/separator/index.ts @@ -0,0 +1,7 @@ +import Root from "./separator.svelte"; + +export { + Root, + // + Root as Separator, +}; diff --git a/ui/src/lib/components/ui/separator/separator.svelte b/ui/src/lib/components/ui/separator/separator.svelte new file mode 100644 index 0000000000..5fd8a42900 --- /dev/null +++ b/ui/src/lib/components/ui/separator/separator.svelte @@ -0,0 +1,23 @@ + + + diff --git a/ui/src/lib/components/ui/table/index.ts b/ui/src/lib/components/ui/table/index.ts new file mode 100644 index 0000000000..14695c8154 --- /dev/null +++ b/ui/src/lib/components/ui/table/index.ts @@ -0,0 +1,28 @@ +import Root from "./table.svelte"; +import Body from "./table-body.svelte"; +import Caption from "./table-caption.svelte"; +import Cell from "./table-cell.svelte"; +import Footer from "./table-footer.svelte"; +import Head from "./table-head.svelte"; +import Header from "./table-header.svelte"; +import Row from "./table-row.svelte"; + +export { + Root, + Body, + Caption, + Cell, + Footer, + Head, + Header, + Row, + // + Root as Table, + Body as TableBody, + Caption as TableCaption, + Cell as TableCell, + Footer as TableFooter, + Head as TableHead, + Header as TableHeader, + Row as TableRow, +}; diff --git a/ui/src/lib/components/ui/table/table-body.svelte b/ui/src/lib/components/ui/table/table-body.svelte new file mode 100644 index 0000000000..935feae719 --- /dev/null +++ b/ui/src/lib/components/ui/table/table-body.svelte @@ -0,0 +1,15 @@ + + + + {@render children?.()} + diff --git a/ui/src/lib/components/ui/table/table-caption.svelte b/ui/src/lib/components/ui/table/table-caption.svelte new file mode 100644 index 0000000000..4696cff571 --- /dev/null +++ b/ui/src/lib/components/ui/table/table-caption.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/ui/src/lib/components/ui/table/table-cell.svelte b/ui/src/lib/components/ui/table/table-cell.svelte new file mode 100644 index 0000000000..a998bf6839 --- /dev/null +++ b/ui/src/lib/components/ui/table/table-cell.svelte @@ -0,0 +1,15 @@ + + + + {@render children?.()} + diff --git a/ui/src/lib/components/ui/table/table-footer.svelte b/ui/src/lib/components/ui/table/table-footer.svelte new file mode 100644 index 0000000000..b9b14ebfac --- /dev/null +++ b/ui/src/lib/components/ui/table/table-footer.svelte @@ -0,0 +1,20 @@ + + +tr]:last:border-b-0", className)} + {...restProps} +> + {@render children?.()} + diff --git a/ui/src/lib/components/ui/table/table-head.svelte b/ui/src/lib/components/ui/table/table-head.svelte new file mode 100644 index 0000000000..267c4e0826 --- /dev/null +++ b/ui/src/lib/components/ui/table/table-head.svelte @@ -0,0 +1,15 @@ + + + + {@render children?.()} + diff --git a/ui/src/lib/components/ui/table/table-header.svelte b/ui/src/lib/components/ui/table/table-header.svelte new file mode 100644 index 0000000000..f47d2597cf --- /dev/null +++ b/ui/src/lib/components/ui/table/table-header.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/ui/src/lib/components/ui/table/table-row.svelte b/ui/src/lib/components/ui/table/table-row.svelte new file mode 100644 index 0000000000..90b4e2a209 --- /dev/null +++ b/ui/src/lib/components/ui/table/table-row.svelte @@ -0,0 +1,15 @@ + + + + {@render children?.()} + diff --git a/ui/src/lib/components/ui/table/table.svelte b/ui/src/lib/components/ui/table/table.svelte new file mode 100644 index 0000000000..d95a02eaef --- /dev/null +++ b/ui/src/lib/components/ui/table/table.svelte @@ -0,0 +1,17 @@ + + +
+ + {@render children?.()} +
+
diff --git a/ui/src/lib/components/ui/tabs/index.ts b/ui/src/lib/components/ui/tabs/index.ts new file mode 100644 index 0000000000..31267e58d1 --- /dev/null +++ b/ui/src/lib/components/ui/tabs/index.ts @@ -0,0 +1,18 @@ +import Root from "./tabs.svelte"; +import Content from "./tabs-content.svelte"; +import List, { tabsListVariants, type TabsListVariant } from "./tabs-list.svelte"; +import Trigger from "./tabs-trigger.svelte"; + +export { + Root, + Content, + List, + Trigger, + tabsListVariants, + type TabsListVariant, + // + Root as Tabs, + Content as TabsContent, + List as TabsList, + Trigger as TabsTrigger, +}; diff --git a/ui/src/lib/components/ui/tabs/tabs-content.svelte b/ui/src/lib/components/ui/tabs/tabs-content.svelte new file mode 100644 index 0000000000..394ab3e820 --- /dev/null +++ b/ui/src/lib/components/ui/tabs/tabs-content.svelte @@ -0,0 +1,17 @@ + + + diff --git a/ui/src/lib/components/ui/tabs/tabs-list.svelte b/ui/src/lib/components/ui/tabs/tabs-list.svelte new file mode 100644 index 0000000000..020507c998 --- /dev/null +++ b/ui/src/lib/components/ui/tabs/tabs-list.svelte @@ -0,0 +1,40 @@ + + + + + diff --git a/ui/src/lib/components/ui/tabs/tabs-trigger.svelte b/ui/src/lib/components/ui/tabs/tabs-trigger.svelte new file mode 100644 index 0000000000..4ae962ac13 --- /dev/null +++ b/ui/src/lib/components/ui/tabs/tabs-trigger.svelte @@ -0,0 +1,23 @@ + + + diff --git a/ui/src/lib/components/ui/tabs/tabs.svelte b/ui/src/lib/components/ui/tabs/tabs.svelte new file mode 100644 index 0000000000..bfb900db1f --- /dev/null +++ b/ui/src/lib/components/ui/tabs/tabs.svelte @@ -0,0 +1,19 @@ + + + diff --git a/ui/src/lib/components/ui/tooltip/index.ts b/ui/src/lib/components/ui/tooltip/index.ts new file mode 100644 index 0000000000..1718604210 --- /dev/null +++ b/ui/src/lib/components/ui/tooltip/index.ts @@ -0,0 +1,19 @@ +import Root from "./tooltip.svelte"; +import Trigger from "./tooltip-trigger.svelte"; +import Content from "./tooltip-content.svelte"; +import Provider from "./tooltip-provider.svelte"; +import Portal from "./tooltip-portal.svelte"; + +export { + Root, + Trigger, + Content, + Provider, + Portal, + // + Root as Tooltip, + Content as TooltipContent, + Trigger as TooltipTrigger, + Provider as TooltipProvider, + Portal as TooltipPortal, +}; diff --git a/ui/src/lib/components/ui/tooltip/tooltip-content.svelte b/ui/src/lib/components/ui/tooltip/tooltip-content.svelte new file mode 100644 index 0000000000..0cf0694973 --- /dev/null +++ b/ui/src/lib/components/ui/tooltip/tooltip-content.svelte @@ -0,0 +1,52 @@ + + + + + {@render children?.()} + + {#snippet child({ props })} +
+ {/snippet} +
+
+
diff --git a/ui/src/lib/components/ui/tooltip/tooltip-portal.svelte b/ui/src/lib/components/ui/tooltip/tooltip-portal.svelte new file mode 100644 index 0000000000..d234f7d7d1 --- /dev/null +++ b/ui/src/lib/components/ui/tooltip/tooltip-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/ui/src/lib/components/ui/tooltip/tooltip-provider.svelte b/ui/src/lib/components/ui/tooltip/tooltip-provider.svelte new file mode 100644 index 0000000000..6dba9a670d --- /dev/null +++ b/ui/src/lib/components/ui/tooltip/tooltip-provider.svelte @@ -0,0 +1,7 @@ + + + diff --git a/ui/src/lib/components/ui/tooltip/tooltip-trigger.svelte b/ui/src/lib/components/ui/tooltip/tooltip-trigger.svelte new file mode 100644 index 0000000000..1acdaa47be --- /dev/null +++ b/ui/src/lib/components/ui/tooltip/tooltip-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/ui/src/lib/components/ui/tooltip/tooltip.svelte b/ui/src/lib/components/ui/tooltip/tooltip.svelte new file mode 100644 index 0000000000..0b0f9cef2a --- /dev/null +++ b/ui/src/lib/components/ui/tooltip/tooltip.svelte @@ -0,0 +1,7 @@ + + + diff --git a/ui/src/lib/config/config-form.svelte b/ui/src/lib/config/config-form.svelte new file mode 100644 index 0000000000..7477af14c9 --- /dev/null +++ b/ui/src/lib/config/config-form.svelte @@ -0,0 +1,178 @@ + + +
+ {#if baseline && overrideCount > 0} +
▲ {overrideCount} per-run override{overrideCount > 1 ? 's' : ''}
+ {/if} + +
+ {#each schema as f (f.key)} + {@const err = errorFor(f)} + {@const ov = isOverride(f)} +
+ +
+
+ {f.label ?? f.key} + {#if f.required}*{/if} + {#if ov}{/if} +
+ {#if f.description}
{f.description}
{/if} +
+ + +
+ {#if f.type === 'bool'} + + {:else if f.type === 'enum'} + + {:else if f.type === 'secret'} +
+
+ + +
+ {#if f.secretSource}{f.secretSource}{/if} +
+ {:else} + set(f, f.type === 'int' ? Number((e.currentTarget as HTMLInputElement).value) : (e.currentTarget as HTMLInputElement).value)} + /> + {/if} + +
+ {#if err} + ⚠ {err} + {:else if values[f.key] === undefined && f.default !== undefined} + default · {String(f.default)} + {/if} + {#if f.type === 'int' && (f.min !== undefined || f.max !== undefined)} + int · {f.min ?? '—'}–{f.max ?? '—'} + {/if} +
+
+
+ {/each} +
+ + +
+ +
+ {#if rawOpen} +
{yaml}
+ {/if} +
diff --git a/ui/src/lib/index.ts b/ui/src/lib/index.ts new file mode 100644 index 0000000000..856f2b6c38 --- /dev/null +++ b/ui/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/ui/src/lib/lineage/column-lineage.svelte b/ui/src/lib/lineage/column-lineage.svelte new file mode 100644 index 0000000000..61089eb816 --- /dev/null +++ b/ui/src/lib/lineage/column-lineage.svelte @@ -0,0 +1,549 @@ + + +
+ +
+
+
+ ⌕ + + {#if colQuery}{/if} +
+
+ {#if colQuery}{matchedColumns.length} of {totalColumns} columns{:else}{totalColumns} columns{/if} +
+
+
+ {#each focalColumns as c (c.name)} + {@const cid = `${focal}.${c.name}`} + {@const lens = incomingLens.get(cid)} + + {:else} +
No columns match.
+ {/each} + {#if truncated} +
+ Showing first {COL_LIST_CAP} of {matchedColumns.length} — search to narrow. +
+ {/if} +
+ + +
+ + +
+
+ {#if selectedCol} + {nameOf(modelOf(selectedCol))}.{colOf(selectedCol)} + {/if} +
+ {#each lenses as l (l)} + {lensLabel[l]} + {/each} + +
+
+
+ + + {#if selectedCol} + + {/if} +
+
+
+ + diff --git a/ui/src/lib/lineage/flow-controller.svelte b/ui/src/lib/lineage/flow-controller.svelte new file mode 100644 index 0000000000..62289b60bf --- /dev/null +++ b/ui/src/lib/lineage/flow-controller.svelte @@ -0,0 +1,23 @@ + diff --git a/ui/src/lib/lineage/graph-draw.ts b/ui/src/lib/lineage/graph-draw.ts new file mode 100644 index 0000000000..2be38a3e7d --- /dev/null +++ b/ui/src/lib/lineage/graph-draw.ts @@ -0,0 +1,96 @@ +import { COL_STEP, NODE_H, NODE_W, ROW_STEP, type GraphLayout, type GraphNode } from './graph-layout'; + +// Far-zoom rendering for large graphs: when the lineage page is zoomed out +// beyond the point where cards are legible, cards are hidden and this canvas +// draws every model as a status bar plus every edge, in the flow's coordinates. + +export type View = { x: number; y: number; zoom: number }; + +export type DrawState = { + layout: GraphLayout; + view: View; + width: number; + height: number; + dpr: number; + /** the page's lit set (selection lineage); everything else is dimmed */ + lit: Set | null; + selected: Set; +}; + +export function hitTest(sx: number, sy: number, layout: GraphLayout, view: View): GraphNode | null { + const wx = (sx - view.x) / view.zoom; + const wy = (sy - view.y) / view.zoom; + const d = Math.floor(wx / COL_STEP); + if (d < 0 || d >= layout.columns.length || wx - d * COL_STEP > NODE_W) return null; + const i = Math.floor((wy - layout.colTop[d]) / ROW_STEP); + const n = layout.columns[d][i]; + return n && wy >= n.y && wy - n.y <= NODE_H ? n : null; +} + +function css(name: string): string { + return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || '#888'; +} + +export function drawFar(ctx: CanvasRenderingContext2D, s: DrawState): void { + const { layout, view } = s; + const k = view.zoom; + const c = { + edge: css('--sb-edge'), + primary: css('--primary'), + ok: css('--sb-success'), + stale: css('--sb-stale'), + fail: css('--sb-error'), + src: css('--sb-layer-source') + }; + const statusColour = (st: string) => (st === 'fail' ? c.fail : st === 'stale' ? c.stale : st === 'src' ? c.src : c.ok); + + ctx.setTransform(s.dpr, 0, 0, s.dpr, 0, 0); + ctx.clearRect(0, 0, s.width, s.height); + ctx.setTransform(s.dpr * k, 0, 0, s.dpr * k, s.dpr * view.x, s.dpr * view.y); + + const left = -view.x / k; + const right = (s.width - view.x) / k; + const top = -view.y / k; + const bottom = (s.height - view.y) / k; + const lit = s.lit; + + const dim = new Path2D(); + const hot = new Path2D(); + for (const [a, b] of layout.edges) { + const p = layout.byId.get(a)!; + const q = layout.byId.get(b)!; + const x1 = p.x + NODE_W; + const x2 = q.x; + if (x2 < left || x1 > right) continue; + const y1 = p.y + NODE_H / 2; + const y2 = q.y + NODE_H / 2; + if (Math.max(y1, y2) < top || Math.min(y1, y2) > bottom) continue; + const path = lit && lit.has(a) && lit.has(b) ? hot : dim; + const mx = (x1 + x2) / 2; + path.moveTo(x1, y1); + path.bezierCurveTo(mx, y1, mx, y2, x2, y2); + } + ctx.lineWidth = 1 / k; + ctx.strokeStyle = c.edge; + ctx.globalAlpha = lit ? 0.12 : 0.5; + ctx.stroke(dim); + ctx.globalAlpha = 1; + ctx.strokeStyle = c.primary; + ctx.lineWidth = 1.5 / k; + ctx.stroke(hot); + + for (const n of layout.nodes) { + if (n.x > right || n.x + NODE_W < left || n.y > bottom || n.y + NODE_H < top) continue; + ctx.globalAlpha = lit && !lit.has(n.id) ? 0.15 : 1; + ctx.fillStyle = statusColour(n.status); + ctx.fillRect(n.x, n.y, NODE_W, NODE_H); + } + ctx.globalAlpha = 1; + + ctx.strokeStyle = c.primary; + ctx.lineWidth = 3 / k; + for (const id of s.selected) { + const n = layout.byId.get(id); + if (n) ctx.strokeRect(n.x - 8 / k, n.y - 8 / k, NODE_W + 16 / k, NODE_H + 16 / k); + } +} diff --git a/ui/src/lib/lineage/graph-layout.ts b/ui/src/lib/lineage/graph-layout.ts new file mode 100644 index 0000000000..d467ac2617 --- /dev/null +++ b/ui/src/lib/lineage/graph-layout.ts @@ -0,0 +1,132 @@ +import type { Edge, Node } from '@xyflow/svelte'; + +// Whole-graph layered layout for large projects. Stand-in for what `sqb compile` +// would emit: columns are dependency depth, and each column is ordered by a few +// barycentre sweeps so connected models sit near each other. Sizes and gaps +// match the lineage page's default cards and dagre spacing. + +export const NODE_W = 236; +export const NODE_H = 61; +export const GAP_X = 76; +export const GAP_Y = 18; +export const COL_STEP = NODE_W + GAP_X; +export const ROW_STEP = NODE_H + GAP_Y; + +export type GraphNode = { + id: string; + name: string; + status: string; + kind: string; + freshness: string; + depth: number; + x: number; + y: number; +}; + +export type GraphLayout = { + nodes: GraphNode[]; + byId: Map; + edges: [string, string][]; + upstream: Map; + downstream: Map; + columns: GraphNode[][]; + colTop: number[]; // world y of each column's first row (columns are top-aligned) + width: number; + height: number; +}; + +export function layoutDag(nodes: Node[], edges: Edge[], sweeps = 6): GraphLayout { + const byId = new Map(); + for (const n of nodes) { + const d = n.data as Record; + byId.set(n.id, { + id: n.id, + name: String(d.name), + status: String(d.status), + kind: String(d.kind), + freshness: String(d.freshness ?? ''), + depth: 0, + x: 0, + y: 0 + }); + } + const upstream = new Map(); + const downstream = new Map(); + for (const e of edges) { + (upstream.get(e.target) ?? upstream.set(e.target, []).get(e.target)!).push(e.source); + (downstream.get(e.source) ?? downstream.set(e.source, []).get(e.source)!).push(e.target); + } + + // depth = longest path from a source (Kahn's topological order) + const indegree = new Map(); + for (const id of byId.keys()) indegree.set(id, upstream.get(id)?.length ?? 0); + const queue = [...byId.keys()].filter((id) => indegree.get(id) === 0); + for (let i = 0; i < queue.length; i++) { + const id = queue[i]; + const depth = byId.get(id)!.depth; + for (const next of downstream.get(id) ?? []) { + const n = byId.get(next)!; + n.depth = Math.max(n.depth, depth + 1); + indegree.set(next, indegree.get(next)! - 1); + if (indegree.get(next) === 0) queue.push(next); + } + } + + const maxDepth = Math.max(0, ...[...byId.values()].map((n) => n.depth)); + const columns: GraphNode[][] = Array.from({ length: maxDepth + 1 }, () => []); + for (const n of [...byId.values()].sort((a, b) => a.name.localeCompare(b.name))) columns[n.depth].push(n); + + // barycentre sweeps: order each column by the average relative position of its neighbours + const rel = new Map(); + const index = () => { + for (const col of columns) col.forEach((n, i) => rel.set(n.id, col.length > 1 ? i / (col.length - 1) : 0.5)); + }; + index(); + for (let s = 0; s < sweeps; s++) { + const down = s % 2 === 0; + const order = down ? columns.slice(1) : columns.slice(0, -1).reverse(); + for (const col of order) { + const key = new Map(); + for (const n of col) { + const ns = (down ? upstream : downstream).get(n.id) ?? []; + key.set(n.id, ns.length ? ns.reduce((sum, id) => sum + rel.get(id)!, 0) / ns.length : rel.get(n.id)!); + } + col.sort((a, b) => key.get(a.id)! - key.get(b.id)!); + col.forEach((n, i) => rel.set(n.id, col.length > 1 ? i / (col.length - 1) : 0.5)); + } + } + + const tallest = Math.max(...columns.map((c) => c.length)); + const colTop = columns.map(() => 0); + columns.forEach((col, d) => + col.forEach((n, i) => { + n.x = d * COL_STEP; + n.y = colTop[d] + i * ROW_STEP; + }) + ); + + return { + nodes: [...byId.values()], + byId, + edges: edges.map((e) => [e.source, e.target]), + upstream, + downstream, + columns, + colTop, + width: columns.length * COL_STEP - GAP_X, + height: tallest * ROW_STEP - GAP_Y + }; +} + +/** Every model reachable from the given ids along `next` (inclusive). */ +export function closure(ids: Iterable, next: Map): Set { + const seen = new Set(); + const stack = [...ids]; + while (stack.length) { + const id = stack.pop()!; + if (seen.has(id)) continue; + seen.add(id); + for (const n of next.get(id) ?? []) stack.push(n); + } + return seen; +} diff --git a/ui/src/lib/lineage/layout.ts b/ui/src/lib/lineage/layout.ts new file mode 100644 index 0000000000..7248382b73 --- /dev/null +++ b/ui/src/lib/lineage/layout.ts @@ -0,0 +1,52 @@ +import dagre from '@dagrejs/dagre'; +import type { Node, Edge } from '@xyflow/svelte'; + +// Fallback dimensions used before a node has been measured by Svelte Flow. +const DEFAULT_WIDTH = 236; +const DEFAULT_HEIGHT = 61; + +export type LayoutOptions = { + /** gap between layers (columns) in LR mode */ + rankSep?: number; + /** gap between nodes within a layer */ + nodeSep?: number; +}; + +/** + * Layered left-to-right layout via dagre. Uses each node's measured size when + * available (so taller nodes — e.g. with extra Node fields enabled — get the + * right spacing and the graph reflows cleanly on toggle). + */ +export function layoutGraph(nodes: Node[], edges: Edge[], opts: LayoutOptions = {}): Node[] { + const g = new dagre.graphlib.Graph(); + g.setGraph({ + rankdir: 'LR', + ranksep: opts.rankSep ?? 76, + nodesep: opts.nodeSep ?? 18, + marginx: 24, + marginy: 24 + }); + g.setDefaultEdgeLabel(() => ({})); + + for (const node of nodes) { + const width = node.measured?.width ?? (node.width as number) ?? DEFAULT_WIDTH; + const height = node.measured?.height ?? (node.height as number) ?? DEFAULT_HEIGHT; + g.setNode(node.id, { width, height }); + } + for (const edge of edges) { + g.setEdge(edge.source, edge.target); + } + + dagre.layout(g); + + return nodes.map((node) => { + const pos = g.node(node.id); + const width = node.measured?.width ?? (node.width as number) ?? DEFAULT_WIDTH; + const height = node.measured?.height ?? (node.height as number) ?? DEFAULT_HEIGHT; + // dagre returns center coordinates; Svelte Flow expects top-left. + return { + ...node, + position: { x: pos.x - width / 2, y: pos.y - height / 2 } + }; + }); +} diff --git a/ui/src/lib/lineage/node-fields.svelte.ts b/ui/src/lib/lineage/node-fields.svelte.ts new file mode 100644 index 0000000000..7478e30f42 --- /dev/null +++ b/ui/src/lib/lineage/node-fields.svelte.ts @@ -0,0 +1,72 @@ +// Node field preferences for the lineage graph ("Node fields" control). +// Global (applies to all nodes). Persisted to localStorage so a user's +// choice of what to show on nodes survives reloads. + +export type NodeFieldKey = + | 'kind' + | 'status' + | 'checks' + | 'freshness' + | 'description' + | 'owner' + | 'tags' + | 'columns'; + +export type NodeField = { key: NodeFieldKey; label: string }; + +export const NODE_FIELDS: NodeField[] = [ + { key: 'kind', label: 'Kind' }, + { key: 'status', label: 'Status' }, + { key: 'checks', label: 'Checks' }, + { key: 'freshness', label: 'Freshness' }, + { key: 'description', label: 'Description' }, + { key: 'owner', label: 'Owner' }, + { key: 'tags', label: 'Tags' }, + { key: 'columns', label: 'Column count' } +]; + +const DEFAULTS: Record = { + kind: true, + status: true, + checks: true, + freshness: false, + description: false, + owner: false, + tags: false, + columns: false +}; + +const STORAGE_KEY = 'sqlbuild.lineage.nodeFields'; + +function load(): Record { + if (typeof localStorage === 'undefined') return { ...DEFAULTS }; + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return { ...DEFAULTS }; + return { ...DEFAULTS, ...JSON.parse(raw) }; + } catch { + return { ...DEFAULTS }; + } +} + +class NodeFieldsState { + value = $state>(load()); + + toggle(key: NodeFieldKey) { + this.value = { ...this.value, [key]: !this.value[key] }; + this.persist(); + } + + reset() { + this.value = { ...DEFAULTS }; + this.persist(); + } + + private persist() { + if (typeof localStorage !== 'undefined') { + localStorage.setItem(STORAGE_KEY, JSON.stringify(this.value)); + } + } +} + +export const nodeFields = new NodeFieldsState(); diff --git a/ui/src/lib/lineage/scoped-lineage.svelte b/ui/src/lib/lineage/scoped-lineage.svelte new file mode 100644 index 0000000000..865d90c6d4 --- /dev/null +++ b/ui/src/lib/lineage/scoped-lineage.svelte @@ -0,0 +1,206 @@ + + +
+ +
+
+ Upstream +
+ + {upDepth} + +
+
+
+ Downstream +
+ + {downDepth} + +
+
+ + + +
+ --select {selectExpr} + +
+
+ + +
+ + + + + +
+
+ + diff --git a/ui/src/lib/lineage/trace.ts b/ui/src/lib/lineage/trace.ts new file mode 100644 index 0000000000..1bf5f89578 --- /dev/null +++ b/ui/src/lib/lineage/trace.ts @@ -0,0 +1,53 @@ +import type { Edge } from '@xyflow/svelte'; + +/** Immediate neighbours (edges touching the node) — used for hover. */ +export function neighbourEdgeIds(nodeId: string, edges: Edge[]): Set { + const ids = new Set(); + for (const e of edges) { + if (e.source === nodeId || e.target === nodeId) ids.add(e.id); + } + return ids; +} + +/** Full upstream + downstream path from a set of nodes — used for selection. */ +export function tracePath( + nodeIds: Set, + edges: Edge[] +): { nodes: Set; edges: Set } { + const outgoing = new Map(); + const incoming = new Map(); + for (const e of edges) { + (outgoing.get(e.source) ?? outgoing.set(e.source, []).get(e.source)!).push(e); + (incoming.get(e.target) ?? incoming.set(e.target, []).get(e.target)!).push(e); + } + + const litNodes = new Set(nodeIds); + const litEdges = new Set(); + + // downstream (follow outgoing) + const downStack = [...nodeIds]; + while (downStack.length) { + const id = downStack.pop()!; + for (const e of outgoing.get(id) ?? []) { + litEdges.add(e.id); + if (!litNodes.has(e.target)) { + litNodes.add(e.target); + downStack.push(e.target); + } + } + } + // upstream (follow incoming) + const upStack = [...nodeIds]; + while (upStack.length) { + const id = upStack.pop()!; + for (const e of incoming.get(id) ?? []) { + litEdges.add(e.id); + if (!litNodes.has(e.source)) { + litNodes.add(e.source); + upStack.push(e.source); + } + } + } + + return { nodes: litNodes, edges: litEdges }; +} diff --git a/ui/src/lib/mock/model-sql.ts b/ui/src/lib/mock/model-sql.ts new file mode 100644 index 0000000000..8f1a257671 --- /dev/null +++ b/ui/src/lib/mock/model-sql.ts @@ -0,0 +1,92 @@ +// Mock model SQL for the prototype (real data comes from the SQLBuild compiler). + +export const FACT_ORDERS_SQL = `MODEL ( + materialized table, + unique_key order_id, + tags [finance, core], + description "One row per order with payment + customer.", + columns ( + order_id (audits [not_null, unique]), + customer_id (audits [not_null]) + ) +); + +WITH orders AS ( + SELECT order_id, customer_id, order_status, total_cents, ordered_at + FROM __ref("stg_orders") +), +payments AS ( + SELECT order_id, payment_method, amount_cents + FROM __ref("stg_payments") + WHERE status = 'success' +), +customers AS ( + SELECT customer_id, first_order_at + FROM __ref("stg_customers") +) + +SELECT + o.order_id, + o.customer_id, + o.order_status, + o.total_cents, + p.payment_method, + o.ordered_at, + o.ordered_at = c.first_order_at AS is_first_order +FROM orders o +LEFT JOIN payments p USING (order_id) +LEFT JOIN customers c USING (customer_id)`; + +export function mockModelSql(name: string, kind: string, parents: string[], description = name): string { + if (name === 'fact_orders') return FACT_ORDERS_SQL; + const materialized = kind.split(' · ')[0]; + const ctes = parents.map((parent, i) => `${i === 0 ? 'WITH ' : ''}${parent.replace(/^(stg|raw)_+/, '')} AS (\n SELECT *\n FROM __ref("${parent}")\n)`); + const first = parents[0]?.replace(/^(stg|raw)_+/, '') ?? 'source'; + return `MODEL (\n materialized ${materialized},\n description "${description}"\n);\n\n${ctes.join(',\n')}\n\nSELECT\n *\nFROM ${first}`; +} + +// ---- drift mock: stored SQL per build in the current target (SQLBuild keeps raw + compiled per build). +// Drift is computed by the backend against what was last built in this target. ---- + +export type DriftKind = 'direct' | 'compiled' | 'upstream'; +export type SqlVersion = { id: string; label: string; detail: string; raw: string; compiled?: string }; + +export const MODEL_DRIFT: Record = { + fact_orders: 'direct', + stg_payments: 'compiled', + dim_customers: 'upstream', + customer_status: 'upstream', + order_status_idx: 'upstream', + hourly_order: 'upstream', + daily_activity: 'upstream', + hourly_ctx: 'upstream' +}; + +const FACT_ORDERS_BUILT = FACT_ORDERS_SQL.replace("\n WHERE status = 'success'", '') + .replace(',\ncustomers AS (\n SELECT customer_id, first_order_at\n FROM __ref("stg_customers")\n)', '') + .replace(' o.ordered_at = c.first_order_at AS is_first_order', ' row_number() OVER (PARTITION BY o.customer_id ORDER BY o.ordered_at) = 1 AS is_first_order') + .replace('\nLEFT JOIN customers c USING (customer_id)', ''); + +const FACT_ORDERS_EARLIER = FACT_ORDERS_SQL.replace("\n WHERE status = 'success'", ''); + +/** + * Versions to compare the current SQL against, newest first. `compiled` overrides + * the default ref substitution when the stored compiled SQL differed. + */ +export function modelVersions(name: string, current: string): SqlVersion[] { + if (name === 'fact_orders') { + return [ + { id: 'built', label: 'Last build', detail: 'run-9d21 · 4m ago', raw: FACT_ORDERS_BUILT }, + { id: 'run-91cd', label: 'Build run-91cd', detail: '1h ago', raw: FACT_ORDERS_BUILT }, + { id: 'run-2b70', label: 'Build run-2b70', detail: '3h ago', raw: FACT_ORDERS_EARLIER } + ]; + } + const builtCompiled = + name === 'stg_payments' + ? current.replace(/__ref\("(\w+)"\)/g, '"waffle_shop"."prod"."$1_v1"').replace(/^MODEL\s*\([\s\S]*?\n\);\s*\n+/, '') + : undefined; + return [ + { id: 'built', label: 'Last build', detail: 'run-9d21 · 4m ago', raw: current, compiled: builtCompiled }, + { id: 'run-91cd', label: 'Build run-91cd', detail: '1h ago', raw: current } + ]; +} diff --git a/ui/src/lib/mock/scale-graph.ts b/ui/src/lib/mock/scale-graph.ts new file mode 100644 index 0000000000..f85ebb9781 --- /dev/null +++ b/ui/src/lib/mock/scale-graph.ts @@ -0,0 +1,158 @@ +import type { Edge, Node } from '@xyflow/svelte'; + +// Synthetic warehouse-shaped lineage for scale testing (?models=N on /lineage). +// Deterministic: the same N always produces the same graph. Models carry the +// same fields as the hand-written lineage mock (kind, icon, checks, drift, +// owner, tags, ...) so the real cards render in full. + +const LAYERS = ['raw', 'stg', 'int', 'int', 'fct', 'dim', 'mart', 'rpt'] as const; +// share of models per layer (sums to 1) +const LAYER_SHARE = [0.12, 0.2, 0.16, 0.12, 0.14, 0.08, 0.12, 0.06]; +const MODELS_PER_DOMAIN = 120; +const DOMAIN_NAMES = [ + 'orders', 'customers', 'payments', 'inventory', 'menu', 'stores', 'staff', 'suppliers', 'delivery', + 'loyalty', 'marketing', 'finance', 'pricing', 'reviews', 'support', 'ops', 'forecasting', 'logistics', + 'events', 'web', 'mobile', 'franchise', 'quality', 'audit' +]; +const OWNERS = ['@analytics', '@finance-data', '@growth', '@platform', '@ops']; +const TAGS = ['core', 'finance', 'pii', 'daily', 'hourly', 'reporting']; + +export const domainName = (i: number) => + DOMAIN_NAMES[i % DOMAIN_NAMES.length] + (i >= DOMAIN_NAMES.length ? `_${Math.floor(i / DOMAIN_NAMES.length) + 1}` : ''); + +type Kind = { kind: string; icon: string }; +const KINDS: Record<(typeof LAYERS)[number], Kind[]> = { + raw: [ + { kind: 'source', icon: 'source' }, + { kind: 'source · merge', icon: 'loader' }, + { kind: 'loader · table', icon: 'loader' }, + { kind: 'seed', icon: 'seed' } + ], + stg: [{ kind: 'view', icon: 'view' }], + int: [ + { kind: 'view', icon: 'view' }, + { kind: 'table', icon: 'table' } + ], + fct: [ + { kind: 'table', icon: 'table' }, + { kind: 'incremental · merge', icon: 'merge' }, + { kind: 'incremental · delete_insert', icon: 'delete_insert' } + ], + dim: [ + { kind: 'table', icon: 'table' }, + { kind: 'incremental · merge', icon: 'merge' } + ], + mart: [ + { kind: 'table', icon: 'table' }, + { kind: 'incremental · delete_insert', icon: 'microbatch' } + ], + rpt: [ + { kind: 'view', icon: 'view' }, + { kind: 'table', icon: 'table' } + ] +}; + +function rng(seed: number) { + let s = seed >>> 0; + return () => { + s = (s + 0x6d2b79f5) >>> 0; + let t = s; + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +export function scaleGraph(total: number): { nodes: Node[]; edges: Edge[] } { + const rand = rng(total); + const pick = (xs: readonly T[]) => xs[Math.floor(rand() * xs.length)]; + const domains = Math.max(1, Math.round(total / MODELS_PER_DOMAIN)); + const byLayer: string[][][] = LAYERS.map(() => Array.from({ length: domains }, () => [])); + const nodes: Node[] = []; + const edges: Edge[] = []; + const downstream = new Map(); + + let made = 0; + LAYERS.forEach((layer, li) => { + const count = li === LAYERS.length - 1 ? total - made : Math.round(total * LAYER_SHARE[li]); + for (let i = 0; i < count; i++) { + const domain = Math.floor(rand() * domains); + const id = `${layer}_${li}_${i}`; + const { kind, icon } = pick(KINDS[layer]); + const name = `${layer}__${domainName(domain)}__m${i}`; + const isSource = kind.startsWith('source'); + const fail = !isSource && rand() < 0.03; + const checkCount = isSource ? 0 : Math.floor(rand() * 5); + const warn = !fail && checkCount > 0 && rand() < 0.06; + nodes.push({ + id, + type: 'asset', + position: { x: 0, y: 0 }, + data: { + name, + kind, + icon, + status: isSource ? 'src' : fail ? 'fail' : 'ok', + freshness: fail ? 'failed' : `${1 + Math.floor(rand() * 59)}m ago`, + ...(fail + ? { checks: '✕ 1', checksColor: 'var(--sb-error)' } + : warn + ? { checks: '! 1', checksColor: 'var(--sb-warning)' } + : checkCount + ? { checks: `✓ ${checkCount}` } + : {}), + ...(isSource + ? {} + : { + columnCount: 3 + Math.floor(rand() * 20), + owner: rand() < 0.6 ? pick(OWNERS) : undefined, + tags: rand() < 0.4 ? [pick(TAGS)] : undefined, + description: rand() < 0.5 ? `${layer} model for ${domainName(domain)}.` : undefined + }) + } + }); + byLayer[li][domain].push(id); + + if (li > 0) { + const parents = 1 + Math.floor(rand() * (layer === 'stg' ? 1.5 : 3.5)); + const seen = new Set(); + for (let p = 0; p < parents; p++) { + // mostly the previous layer, sometimes further back; mostly same domain + const back = 1 + (rand() < 0.7 ? 0 : Math.floor(rand() * Math.min(3, li))); + const pl = li - back; + const pd = rand() < 0.85 ? domain : Math.floor(rand() * domains); + const pool = byLayer[pl][pd].length ? byLayer[pl][pd] : byLayer[pl].flat(); + if (!pool.length) continue; + const src = pool[Math.floor(rand() * pool.length)]; + if (seen.has(src)) continue; + seen.add(src); + edges.push({ id: `${src}-${id}`, source: src, target: id, type: 'bezier' }); + (downstream.get(src) ?? downstream.set(src, []).get(src)!).push(id); + } + } + } + made += count; + }); + + // Drift: a few models changed since the last build in this target (direct SQL + // change, or compiled-only change); everything downstream of them is stale + // with upstream drift. Drift implies stale, never the reverse. + const byId = new Map(nodes.map((n) => [n.id, n])); + const driftSources = nodes.filter((n) => n.data.status !== 'src' && rand() < 0.008); + for (const n of driftSources) n.data = { ...n.data, drift: rand() < 0.75 ? 'direct' : 'compiled' }; + const stack = driftSources.flatMap((n) => downstream.get(n.id) ?? []); + const seen = new Set(); + while (stack.length) { + const id = stack.pop()!; + if (seen.has(id)) continue; + seen.add(id); + const n = byId.get(id)!; + if (!n.data.drift) n.data = { ...n.data, drift: 'upstream' }; + stack.push(...(downstream.get(id) ?? [])); + } + for (const n of nodes) { + if (n.data.drift && n.data.status === 'ok') n.data = { ...n.data, status: 'stale' }; + } + + return { nodes, edges }; +} diff --git a/ui/src/lib/mock/timeline.ts b/ui/src/lib/mock/timeline.ts new file mode 100644 index 0000000000..dfbde2d70a --- /dev/null +++ b/ui/src/lib/mock/timeline.ts @@ -0,0 +1,81 @@ +// Deterministic mock run history for the activity timeline. + +export type TimelineStatus = 'ok' | 'warn' | 'fail' | 'running' | 'scheduled'; +export type TimelineRun = { + id: string; + job: string; + status: TimelineStatus; + start: number; + end: number; + trigger: 'schedule' | 'sensor' | 'manual'; +}; +export type TimelineJob = { name: string }; + +const MIN = 60_000; +export const TIMELINE_NOW = new Date(2026, 4, 31, 12, 8).getTime(); + +type JobSpec = TimelineJob & { + every: number; // minutes between ticks + at: number; // minute offset within the day + dur: number; // typical duration, minutes + trigger: TimelineRun['trigger']; +}; + +const SPECS: JobSpec[] = [ + { name: 'nightly_build', every: 180, at: 0, dur: 24, trigger: 'schedule' }, + { name: 'marts_only', every: 240, at: 30, dur: 14, trigger: 'schedule' }, + { name: 'revenue_refresh', every: 360, at: 75, dur: 38, trigger: 'schedule' }, + { name: 'customer_snapshots', every: 720, at: 150, dur: 52, trigger: 'schedule' }, + { name: 'finance_daily', every: 1440, at: 360, dur: 95, trigger: 'schedule' }, + { name: 'hourly_orders', every: 60, at: 10, dur: 6, trigger: 'schedule' }, + { name: 'load_sources', every: 120, at: 50, dur: 9, trigger: 'sensor' }, + { name: 'load_payments', every: 90, at: 20, dur: 11, trigger: 'sensor' }, + { name: 'load_customers', every: 240, at: 5, dur: 18, trigger: 'schedule' }, + { name: 'sync_crm', every: 360, at: 200, dur: 26, trigger: 'schedule' }, + { name: 'ingest_events', every: 30, at: 3, dur: 3, trigger: 'sensor' }, + { name: 'checks_only', every: 180, at: 100, dur: 7, trigger: 'schedule' }, + { name: 'freshness_checks', every: 60, at: 40, dur: 2, trigger: 'schedule' }, + { name: 'audit_nightly', every: 1440, at: 150, dur: 44, trigger: 'schedule' }, +]; + +export const TIMELINE_JOBS: TimelineJob[] = SPECS.map(({ name }) => ({ name })); + +function rng(seed: number) { + return () => { + seed |= 0; + seed = (seed + 0x6d2b79f5) | 0; + let t = Math.imul(seed ^ (seed >>> 15), 1 | seed); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +function build(): TimelineRun[] { + const runs: TimelineRun[] = []; + const from = TIMELINE_NOW - 8 * 1440 * MIN; + const to = TIMELINE_NOW + 1440 * MIN; + const day0 = new Date(from); + day0.setHours(0, 0, 0, 0); + SPECS.forEach((spec, jobIndex) => { + const rand = rng(jobIndex * 7919 + 17); + for (let t = day0.getTime() + spec.at * MIN; t < to; t += spec.every * MIN) { + if (t < from) continue; + const dur = spec.dur * (0.7 + rand() * 0.7) * MIN; + const id = `run-${Math.floor(rand() * 0xffff).toString(16).padStart(4, '0')}`; + if (t > TIMELINE_NOW) { + runs.push({ id, job: spec.name, status: 'scheduled', start: t, end: t + spec.dur * MIN, trigger: spec.trigger }); + continue; + } + const r = rand(); + const status: TimelineStatus = + t + dur > TIMELINE_NOW ? 'running' : r < 0.035 ? 'fail' : r < 0.07 ? 'warn' : 'ok'; + runs.push({ id, job: spec.name, status, start: t, end: status === 'running' ? TIMELINE_NOW : t + dur, trigger: spec.trigger }); + } + }); + // Anchors that match the rest of the prototype's mock data. + runs.push({ id: 'run-4f2a', job: 'nightly_build', status: 'fail', start: TIMELINE_NOW - 22 * MIN, end: TIMELINE_NOW - 18 * MIN, trigger: 'schedule' }); + runs.push({ id: 'run-91cd', job: 'marts_only', status: 'ok', start: TIMELINE_NOW - 97 * MIN, end: TIMELINE_NOW - 84 * MIN, trigger: 'manual' }); + return runs.sort((a, b) => a.start - b.start); +} + +export const TIMELINE_RUNS = build(); diff --git a/ui/src/lib/run/run-panel.svelte b/ui/src/lib/run/run-panel.svelte new file mode 100644 index 0000000000..0cc40e46a7 --- /dev/null +++ b/ui/src/lib/run/run-panel.svelte @@ -0,0 +1,382 @@ + + +{#if open} + +
+ +
+ Run +
+ {#each verbs as v (v)} + + {/each} +
+ target {target} 🔒 + +
+ +
+ +
+ +
+
Selection
+
+ ⌕ + { + selectTouched = true; + userEdited = false; + }} + placeholder="select: +fact_orders, tag:revenue, group:marts, *" + class="text-foreground placeholder:text-[var(--sb-text-faint)] w-full bg-transparent text-[12px] outline-none" + /> + {#if selectQuery}{/if} +
+
+ + {matched.length} node{matched.length === 1 ? '' : 's'} match + + {selectQuery ? '' : 'whole project'} +
+ {#if matched.length} + +
+ {#each matched.slice(0, SELECT_CAP) as n (n.name)} + {n.name} + {/each} + {#if matched.length > SELECT_CAP} + +{matched.length - SELECT_CAP} more — refine with a query + {/if} +
+ {/if} +
+ + +
+ Provider config + durable config → +
+ {#if relevantProviders.length === 0} +
This selection uses no providers — nothing to configure. Models build against the project target.
+ {:else} +
+ {#each relevantProviders as p (p)} +
+
{p}
+ {#if cfgValues[p]} + [f.key, f.default]))} + /> + {/if} +
+ {/each} +
+ {/if} +
+ + +
+
Flags · {verb}
+
+ {#each flagsByVerb[verb] as f (f.flag)} + + {/each} +
+
+ Type -- for flags, or a name / tag: / group: for selection. The command below is the source of truth. +
+
+
+ + +
+ {#if acOpen && suggestions.length} +
+ {#each suggestions as s, i (s.value)} + + {/each} +
+ {/if} +
+ $ + { + userEdited = true; + refreshToken(); + }} + onkeydown={onKeydown} + onclick={refreshToken} + onblur={() => setTimeout(() => (acOpen = false), 120)} + spellcheck="false" + class="text-foreground flex-1 bg-transparent text-[12.5px] outline-none" + placeholder="sqb build --select +fact_orders --target snowflake_prod" + /> + +
+ {#if overrideArgs.length} +
+ overrides: {overrideArgs.join(' ')}
+ {/if} + {#if cfgErrors > 0}
Fix {cfgErrors} config field{cfgErrors > 1 ? 's' : ''} to run
{/if} +
+
+{/if} + + diff --git a/ui/src/lib/sql/diff.ts b/ui/src/lib/sql/diff.ts new file mode 100644 index 0000000000..d0cfac4f90 --- /dev/null +++ b/ui/src/lib/sql/diff.ts @@ -0,0 +1,64 @@ +// Pure line diff (LCS) with context folding for the SQL drift view. + +export type DiffOp = 'same' | 'add' | 'del'; +export type DiffLine = { op: DiffOp; a?: number; b?: number; text: string }; +export type DiffRow = DiffLine | { op: 'fold'; count: number; key: string }; + +export function diffLines(before: string, after: string): DiffLine[] { + const a = before.split('\n'); + const b = after.split('\n'); + const n = a.length; + const m = b.length; + const lcs: number[][] = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0)); + for (let i = n - 1; i >= 0; i--) { + for (let j = m - 1; j >= 0; j--) { + lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]); + } + } + const out: DiffLine[] = []; + let i = 0; + let j = 0; + while (i < n && j < m) { + if (a[i] === b[j]) { + out.push({ op: 'same', a: i + 1, b: j + 1, text: b[j] }); + i++; + j++; + } else if (lcs[i + 1][j] >= lcs[i][j + 1]) { + out.push({ op: 'del', a: i + 1, text: a[i] }); + i++; + } else { + out.push({ op: 'add', b: j + 1, text: b[j] }); + j++; + } + } + while (i < n) out.push({ op: 'del', a: ++i, text: a[i - 1] }); + while (j < m) out.push({ op: 'add', b: ++j, text: b[j - 1] }); + return out; +} + +/** Collapse runs of unchanged lines, keeping `context` lines around each change. */ +export function foldDiff(lines: DiffLine[], context = 3): DiffRow[] { + const keep = lines.map(() => false); + lines.forEach((l, idx) => { + if (l.op === 'same') return; + for (let k = Math.max(0, idx - context); k <= Math.min(lines.length - 1, idx + context); k++) keep[k] = true; + }); + const rows: DiffRow[] = []; + let run = 0; + lines.forEach((l, idx) => { + if (keep[idx]) { + if (run) rows.push({ op: 'fold', count: run, key: `f${idx}` }); + run = 0; + rows.push(l); + } else run++; + }); + if (run) rows.push({ op: 'fold', count: run, key: 'fend' }); + return rows; +} + +export function diffStats(lines: DiffLine[]) { + return { + added: lines.filter((l) => l.op === 'add').length, + removed: lines.filter((l) => l.op === 'del').length + }; +} diff --git a/ui/src/lib/sql/highlight.ts b/ui/src/lib/sql/highlight.ts new file mode 100644 index 0000000000..8dfa720634 --- /dev/null +++ b/ui/src/lib/sql/highlight.ts @@ -0,0 +1,52 @@ +// SQLBuild-aware syntax highlighting. +// +// SQLBuild SQL is a superset of SQL: a MODEL(...) config header (materialization, +// tags, description, columns + audits DSL) followed by a SELECT body, with +// __ref()/__source() reference functions. No stock grammar knows the header, so +// we layer a small SQLBuild grammar on top of Prism's base SQL grammar. +// +// Output is class-based (.token.*), themed via CSS variables in layout.css so it +// adapts to light/dark automatically. + +import Prism from 'prismjs'; +import 'prismjs/components/prism-sql'; + +let registered = false; + +export function ensureSqlbuildGrammar() { + if (registered) return; + registered = true; + + // clone Prism's base SQL grammar as the body language + const base = Prism.languages.sql; + + const sqlbuild = Prism.languages.extend('sql', {}); + // must exist before insertBefore('sqlbuild', ...) can extend it + Prism.languages.sqlbuild = sqlbuild; + + // SQLBuild reference functions: __ref('x'), __source('a','b') -> function color + Prism.languages.insertBefore('sqlbuild', 'function', { + 'sb-ref': { + pattern: /\b__(?:ref|source|var|env)\b(?=\s*\()/, + alias: 'function' + } + }); + + // MODEL config-header keywords (the DSL before SELECT) + Prism.languages.insertBefore('sqlbuild', 'keyword', { + 'sb-config': { + // MODEL / materialization kinds / config props / audit names + pattern: + /\b(?:MODEL|materialized|view|table|incremental|ephemeral|seed|source|loader|tags|description|columns|audits|owner|tags|cursor|unique_key|grain|cluster_by|partition_by|strategy|merge|delete_insert|append|microbatch|not_null|unique|accepted_values|relationships|expression_is_true|freshness)\b/i, + alias: 'keyword' + } + }); + + // register under both names so `language-sqlbuild` and `language-sql` both work + void base; +} + +export function highlightSqlbuild(code: string): string { + ensureSqlbuildGrammar(); + return Prism.highlight(code, Prism.languages.sqlbuild, 'sqlbuild'); +} diff --git a/ui/src/lib/sql/outline.ts b/ui/src/lib/sql/outline.ts new file mode 100644 index 0000000000..83089b8467 --- /dev/null +++ b/ui/src/lib/sql/outline.ts @@ -0,0 +1,72 @@ +// Pure helpers for the SQL viewer: structural outline, compiled rendering and +// line-level highlighting with clickable __ref() links. + +import { highlightSqlbuild } from './highlight'; + +export type OutlineItem = { label: string; line: number }; +export type SqlOutline = { + config: { key: string; value: string }[]; + refs: OutlineItem[]; + steps: OutlineItem[]; + finalSelect?: number; + columns: string[]; +}; + +const REF_RE = /__ref\(\s*"(\w+)"\s*\)/g; + +export function compileRefs(raw: string, database = 'waffle_shop', schema = 'prod'): string { + const withoutHeader = raw.replace(/^MODEL\s*\([\s\S]*?\n\);\s*\n+/, ''); + return withoutHeader.replace(REF_RE, (_m, name: string) => `"${database}"."${schema}"."${name}"`); +} + +export function outlineSql(raw: string): SqlOutline { + const lines = raw.split('\n'); + const config: { key: string; value: string }[] = []; + const header = raw.match(/^MODEL\s*\(([\s\S]*?)\n\);/); + if (header) { + for (const key of ['materialized', 'strategy', 'unique_key', 'tags', 'owner']) { + const m = header[1].match(new RegExp(`\\b${key}\\s+(\\[[^\\]]*\\]|[^,\\n]+)`)); + if (m) config.push({ key, value: m[1].replace(/[[\]"]/g, '').replace(/,\s*/g, ', ').trim() }); + } + } + + const refs: OutlineItem[] = []; + const seen = new Set(); + const steps: OutlineItem[] = []; + let finalSelect: number | undefined; + lines.forEach((line, i) => { + for (const m of line.matchAll(REF_RE)) { + if (!seen.has(m[1])) { + seen.add(m[1]); + refs.push({ label: m[1], line: i + 1 }); + } + } + const cte = line.match(/^\s*(?:WITH\s+|,\s*)?(\w+)\s+AS\s*\($/i); + if (cte) steps.push({ label: cte[1], line: i + 1 }); + if (/^SELECT\b/i.test(line)) finalSelect = i + 1; + }); + + const columns: string[] = []; + if (finalSelect) { + for (const line of lines.slice(finalSelect)) { + if (/^\s*FROM\b/i.test(line)) break; + const col = line.trim().replace(/,$/, ''); + if (!col) continue; + const alias = col.match(/\bAS\s+(\w+)$/i)?.[1] ?? col.split('.').pop()?.split(/\s+/).pop(); + if (alias && /^\w+$/.test(alias)) columns.push(alias); + } + } + return { config, refs, steps, finalSelect, columns }; +} + +/** Highlight SQL and split into per-line HTML; raw refs become clickable links. */ +export function highlightLines(code: string, linkRefs: boolean): string[] { + let html = highlightSqlbuild(code); + if (linkRefs) { + html = html.replace( + /(__ref<\/span>\(<\/span>)"(\w+)"<\/span>/g, + '$1' + ); + } + return html.split('\n'); +} diff --git a/ui/src/lib/theme.svelte.ts b/ui/src/lib/theme.svelte.ts new file mode 100644 index 0000000000..2b38f68d48 --- /dev/null +++ b/ui/src/lib/theme.svelte.ts @@ -0,0 +1,21 @@ +import { browser } from '$app/environment'; + +type Theme = 'dark' | 'light'; + +function read(): Theme { + if (!browser) return 'dark'; + return (localStorage.getItem('sb-theme') as Theme) ?? 'dark'; +} + +export const theme = $state<{ value: Theme }>({ value: read() }); + +function apply(t: Theme) { + if (!browser) return; + document.documentElement.classList.toggle('dark', t === 'dark'); + localStorage.setItem('sb-theme', t); +} + +export function toggleTheme() { + theme.value = theme.value === 'dark' ? 'light' : 'dark'; + apply(theme.value); +} diff --git a/ui/src/lib/utils.ts b/ui/src/lib/utils.ts new file mode 100644 index 0000000000..55b3a918e7 --- /dev/null +++ b/ui/src/lib/utils.ts @@ -0,0 +1,13 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChild = T extends { child?: any } ? Omit : T; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChildren = T extends { children?: any } ? Omit : T; +export type WithoutChildrenOrChild = WithoutChildren>; +export type WithElementRef = T & { ref?: U | null }; diff --git a/ui/src/routes/+layout.svelte b/ui/src/routes/+layout.svelte new file mode 100644 index 0000000000..eeffb95c29 --- /dev/null +++ b/ui/src/routes/+layout.svelte @@ -0,0 +1,17 @@ + + + + + + +
+ +
+ {@render children()} +
+
diff --git a/ui/src/routes/+layout.ts b/ui/src/routes/+layout.ts new file mode 100644 index 0000000000..3cf2c06326 --- /dev/null +++ b/ui/src/routes/+layout.ts @@ -0,0 +1,5 @@ +// SPA mode: no SSR, no prerender. All data is fetched client-side, so the build +// is a static bundle served by the SQLBuild server. +export const ssr = false; +export const prerender = false; +export const trailingSlash = 'never'; diff --git a/ui/src/routes/+page.svelte b/ui/src/routes/+page.svelte new file mode 100644 index 0000000000..c0a5d42038 --- /dev/null +++ b/ui/src/routes/+page.svelte @@ -0,0 +1,257 @@ + + + + + + +
+
+
+
+
+ {#if issues.length}{:else}{/if} +
+
+ {issues.length ? `Production needs attention` : 'Production is healthy'} + {issues.length ? `${issues.length} issues · last build failed 18 minutes ago` : 'Last build completed 18 minutes ago'} +
+
+ 40 current + {#if !healthy} + 2 stale + 1 failed check + {/if} +
+ View latest run → +
+ {#if issues.length} +
+ {#each issues as item (item.title)} + {@const Glyph = item.icon} + + + {item.title} + {item.detail} + {item.when} + {item.action} → + + {/each} +
+ {/if} +
+ +
+ {#each stats as stat (stat.label)} +
+ {stat.label} +
{stat.value}{stat.detail}
+ {stat.note} +
+ {/each} +
+ + + +
+
+

Activity

Runs and scheduled ticks by job
+ Open full timeline → +
+ +
+ +
+
+
+

Warehouse freshness

42 models · production
+ Open catalog → +
+
+
+
+ 33 fresh + 3 stale + 4 warning + 2 failed +
+
+ {#each [['Staging', 12, 12], ['Intermediate', 9, 10], ['Marts', 12, 14], ['Sources', 5, 6]] as [layer, fresh, total] (layer)} +
+ {layer} +
+ {fresh}/{total} +
+ {/each} +
+
+
+
+
+

Recent changes

Deploys and definition changes
+ View runs → +
+
+ {#each changes as c (c.title)} + {@const Glyph = changeIcon[c.kind as keyof typeof changeIcon]} + + + {c.title}{c.detail} + {c.who} · {c.when} + + {/each} +
+
+
+ +
+
+ + + diff --git a/ui/src/routes/catalog/+page.svelte b/ui/src/routes/catalog/+page.svelte new file mode 100644 index 0000000000..e901946889 --- /dev/null +++ b/ui/src/routes/catalog/+page.svelte @@ -0,0 +1,195 @@ + + + + + + + + + {#snippet filters()} + ({ value: t, label: typeLabel[t] }))} /> + + {/snippet} + + +
+ + + + + + + + + + + + + {#each filtered as m (m.name)} + {@const Glyph = typeIcon[m.nodeType]} + goto(`/catalog/${m.name}`)}> + + + + + + + + {:else} + + {/each} + +
NameKindTagsStatusChecksLast built
+
+ + +
+
{m.kind} + {#if m.tags.length} +
{#each m.tags as t (t)}{t}{/each}
+ {:else}—{/if} +
{m.checks}
No nodes match the filter.
+
diff --git a/ui/src/routes/catalog/[name]/+page.svelte b/ui/src/routes/catalog/[name]/+page.svelte new file mode 100644 index 0000000000..49d3ce6e24 --- /dev/null +++ b/ui/src/routes/catalog/[name]/+page.svelte @@ -0,0 +1,772 @@ + + + + + + + +
+
+ +
+ +

{name}

+ + +
+
+ {nodeType}{isModel ? ' · table' : ''}marts@analyticsBuilt 4m ago +
+
+ {#each tabs as [t, lbl] (t)} + + {/each} +
+
+ +
+ {#if tab === 'overview'} +
+
+
+
+ + + run-9d21 → +
+
+ {#each facts as [k, v] (k)}
{k}{v}
{/each} +
+
+ {#each recent as st, i (i)}{/each} +
+
Last {recent.length} buildsnow
+ {#if drift} + + {/if} +
+ +
+

Description

+

{description}

+
+ + {#if hasSql} +
+ +
+ {:else if isPython || nodeType === 'udf'} +
+ setTab('code')} /> +
+ {/if} + + {#if isModel} +
+

Checks

{checks.length} of {checks.length} passing
+
+ {#each checks as c (c.name)} +
{c.name}{c.phase}
+ {/each} +
+
+ {/if} + + {#if hasColumns} +
+

Columns

+
+ {#each columns.slice(0, 5) as c (c.name)} +
{c.name}{c.desc}{c.type.toLowerCase()}
+ {/each} +
+
+ {/if} + +
+
+

Upstream {upstream.length}

+ {#each upstream as u (u)}{u}{/each} +
+
+

Downstream {downstream.length}

+ {#each downstream as d (d)}{d}{/each} +
+
+
+ + +
+ {:else if tab === 'columns'} + + + + {#each columns as c (c.name)} + + + + + + + {/each} + +
ColumnTypeDescriptionAudits
{c.name}{c.type.toLowerCase()}{c.desc}
{#each c.flags as f (f)}{f}{:else}—{/each}
+ {:else if tab === 'sql'} + (window.location.href = `/catalog/${m}?tab=sql`)} /> + {:else if tab === 'code'} +
+ {:else if tab === 'lineage'} + + {:else if tab === 'collineage'} + + {:else if tab === 'definition'} +
+ {:else if tab === 'runs'} + + + + {#each modelRuns as r (r.id)} + (window.location.href = `/runs/${r.id}`)}> + + + + + + + {/each} + +
RunJobRowsDurationStarted
+
{r.id}
+ {#if r.detail}
{r.detail}
{/if} +
e.stopPropagation()}>{r.job}{r.rows}{r.duration}
+ {/if} +
+
+ + diff --git a/ui/src/routes/checks/+page.svelte b/ui/src/routes/checks/+page.svelte new file mode 100644 index 0000000000..c258cc1d6b --- /dev/null +++ b/ui/src/routes/checks/+page.svelte @@ -0,0 +1,132 @@ + + + + + + + + + {#snippet filters()} + ({ value: f, label: resultFilterLabel[f], count: f === 'all' ? counts.total : counts[f] }))} + /> + + + {/snippet} + + +
+ + + + + + + + + + + + + + + {#each filtered as c (c.name + c.model)} + goto(`/runs/${c.lastRunId}`)}> + + + + + + + + + + {:else} + + {/each} + +
CheckResultModelTypeSeverityPhaseDetailWhen
+
+ + {c.name} +
+
{#if c.model === '—'}Standalone{:else} e.stopPropagation()}>{c.model}{/if}{c.type}{c.severity}{c.phase}{c.detail}
No checks match the filter.
+
+ + diff --git a/ui/src/routes/jobs/+page.svelte b/ui/src/routes/jobs/+page.svelte new file mode 100644 index 0000000000..0456e6b119 --- /dev/null +++ b/ui/src/routes/jobs/+page.svelte @@ -0,0 +1,135 @@ + + + + + + + + j.enabled).length, label: 'scheduled' }, + { tone: 'var(--sb-error)', count: jobs.filter((j) => j.lastStatus === 'fail').length, label: 'failing' } + ]} +> + {#snippet filters()} + + + {/snippet} + + +
+ + + + + + + + + + + + + {#each filtered as j (j.name)} + goto(`/jobs/${j.name}`)}> + + + + + + + + {:else} + + {/each} + +
JobSelectorNodesScheduleLast runWhen
+
+ + +
+
{j.select}{j.nodes} + {#if j.schedule === 'manual'} + Manual + {:else} +
+ + {j.schedule} +
+ {/if} +
+ {#if j.lastStatus === 'never'} + Never run + {:else} + + {/if} +
No jobs match the filter.
+
diff --git a/ui/src/routes/jobs/[name]/+page.svelte b/ui/src/routes/jobs/[name]/+page.svelte new file mode 100644 index 0000000000..f6ab7cc67e --- /dev/null +++ b/ui/src/routes/jobs/[name]/+page.svelte @@ -0,0 +1,181 @@ + + + + + + + +
+ +
+ ← Jobs + {name} + --select {def.select} + · {resolved.length} nodes · {def.schedule} +
+ +
+ +
+

{def.desc}

+ + +
+
Recent runs
+ + + {#each runs as r (r.id)} + (window.location.href = `/runs/${r.id}`)}> + + + + + + + + {/each} + +
{r.id}{r.nodes}{r.dur}{r.when}{r.by}
+
+ + +
+
Selected nodes · {resolved.length}
+
+ {#each resolved as n (n.name)} + + + {n.name} + + + {/each} +
+
+
+ + +
+
Definition
+
+ {#each [['Selector', def.select], ['Schedule', def.schedule], ['Environment', def.environment], ['Threads', String(def.threads)], ['On failure', def.onFailure], ['Owner', def.owner]] as [k, v] (k)} +
+ {k} + {v} +
+ {/each} +
+
+
+ Triggers · {jobTriggers.length} + all → +
+ {#if jobTriggers.length === 0} +
No trigger — run manually.
+ {:else} +
+ {#each jobTriggers as t (t.name)} +
+ {t.type === 'schedule' ? '◷' : '⚡'} +
+
{t.name}
+
{t.cadence}
+
+ +
+ {/each} +
+ {/if} +
+
+
+
+ + diff --git a/ui/src/routes/layout.css b/ui/src/routes/layout.css new file mode 100644 index 0000000000..67084487b3 --- /dev/null +++ b/ui/src/routes/layout.css @@ -0,0 +1,456 @@ +@import 'tailwindcss'; +@import "tw-animate-css"; +@import "shadcn-svelte/tailwind.css"; +@import "@fontsource-variable/inter"; +@import "@fontsource-variable/space-grotesk"; +@import "@fontsource/ibm-plex-mono/400.css"; +@import "@fontsource/ibm-plex-mono/500.css"; + +@custom-variant dark (&:is(.dark *)); +@plugin '@tailwindcss/typography'; + +/* Refined SQLBuild light theme: crisp white chrome over a restrained cool-grey canvas. */ +:root { + /* experiment: panels are outlined in light mode, filled (borderless) in dark mode */ + /* light: the card edge is the strongest line on the page; lines inside cards stay quieter */ + --panel-border: rgba(28, 32, 38, 0.2); + --panel-shadow: none; + --inner-line: rgba(28, 32, 38, 0.055); + --node-border: var(--border-strong); + --radius: 2px; /* experiment: boxy corners */ + + --background: #ffffff; + --foreground: #171a1f; + --card: #ffffff; + --card-foreground: #171a1f; + --popover: #ffffff; + --popover-foreground: #171a1f; + --primary: #2677d9; + --primary-foreground: #ffffff; + --secondary: #f6f7f9; + --secondary-foreground: #171a1f; + --muted: #f6f7f9; + --muted-foreground: #626a76; + --accent: #f3f5f7; + --accent-foreground: #171a1f; + --destructive: #c74757; + --border: rgba(31, 41, 55, 0.12); + --input: rgba(31, 41, 55, 0.18); + --ring: #2677d9; + --chart-1: #2677d9; + --chart-2: #16825c; + --chart-3: #7756b9; + --chart-4: #aa6c16; + --chart-5: #16825c; + --sidebar: #ffffff; + --sidebar-foreground: #626a76; + --sidebar-primary: #2677d9; + --sidebar-primary-foreground: #ffffff; + --sidebar-accent: rgba(38, 119, 217, 0.10); + --sidebar-accent-foreground: #171a1f; + --sidebar-border: rgba(28, 32, 38, 0.10); + --sidebar-ring: #2677d9; + + /* SQLBuild semantic + structural tokens (light) */ + --sb-success: #16825c; + --sb-warning: #aa6c16; + --sb-error: #c74757; + --sb-running: #2677d9; + --sb-stale: #7756b9; + --sb-secondary: #16826f; + --sb-text-faint: #98a0ad; + --sb-surface-low: #f7f8fa; + --sb-surface-high: #ffffff; + --sb-inset: #fbfcfe; + --sb-hover: #f5f7fa; + --sb-layer-source: #8b9098; + --sb-layer-staging: #16826f; + --sb-layer-intermediate: #aa6c16; + --sb-layer-marts: #2677d9; + --sb-elev: 0 1px 2px rgba(16,24,40,0.035), 0 5px 16px rgba(16,24,40,0.045); + + /* structural border + graph tokens (light) */ + --border-subtle: rgba(28, 32, 38, 0.075); + --border-strong: rgba(28, 32, 38, 0.22); + --sb-edge: rgba(28, 32, 38, 0.24); + --sb-edge-dim: rgba(28, 32, 38, 0.08); + --sb-grid: rgba(28, 32, 38, 0.075); + --sb-mono: #464b53; + --sb-mono-err: #aa3445; + + /* SQL syntax tokens (light) */ + --syn-keyword: #1a56c4; + --syn-string: #0a7a3f; + --syn-number: #9a6700; + --syn-comment: #8a909c; + --syn-function: #7c3aed; + --syn-operator: #3a3f4a; + --syn-punct: #5b616e; +} + +/* Neutral operational dark theme: no warm tint, with SQLBuild blue reserved for action. */ +.dark { + --panel-border: transparent; + --panel-shadow: none; + --inner-line: rgba(176, 184, 198, 0.09); + --node-border: rgba(176, 184, 198, 0.1); + --background: #0d0f12; + --foreground: #e9ebef; + --card: #171a1f; + --card-foreground: #e9ebef; + --popover: #191c21; + --popover-foreground: #e9ebef; + --primary: #3788e8; + --primary-foreground: #ffffff; + --secondary: #20242a; + --secondary-foreground: #e9ebef; + --muted: #191c21; + --muted-foreground: #9ba1ac; + --accent: #20242a; + --accent-foreground: #e9ebef; + --destructive: #eb6475; + --border: rgba(176, 184, 198, 0.14); + --input: rgba(176, 184, 198, 0.22); + --ring: #3788e8; + --chart-1: #3788e8; + --chart-2: #4dd6c4; + --chart-3: #a78bfa; + --chart-4: #f0c267; + --chart-5: #54cf8e; + --sidebar: #080a0d; + --sidebar-foreground: #9ba1ac; + --sidebar-primary: #3788e8; + --sidebar-primary-foreground: #ffffff; + --sidebar-accent: rgba(55, 136, 232, 0.14); + --sidebar-accent-foreground: #e9ebef; + --sidebar-border: rgba(176, 184, 198, 0.12); + --sidebar-ring: #69aaf6; + + /* SQLBuild semantic + structural tokens (used by custom widgets) */ + --sb-success: #54cf8e; + --sb-warning: #f0c267; + --sb-error: #f47089; + --sb-running: #69aaf6; + --sb-stale: #a78bfa; + --sb-secondary: #4dd6c4; + --sb-text-faint: #626975; + --sb-surface-low: #080a0d; + --sb-surface-high: #20242a; + --sb-inset: #090b0e; + --sb-hover: #20242a; + --sb-layer-source: #59627a; + --sb-layer-staging: #4dd6c4; + --sb-layer-intermediate: #f0c267; + --sb-layer-marts: #3788e8; + --sb-elev: 0 12px 32px rgba(0,0,0,0.3), 0 0 0 1px rgba(176,184,198,0.04), inset 0 1px 0 rgba(255,255,255,0.035); + + /* structural border + graph tokens (dark) */ + --border-subtle: rgba(176, 184, 198, 0.09); + --border-strong: rgba(176, 184, 198, 0.28); + --sb-edge: rgba(176, 184, 198, 0.28); + --sb-edge-dim: rgba(176, 184, 198, 0.09); + --sb-grid: rgba(176, 184, 198, 0.09); + --sb-mono: #b6bbc4; + --sb-mono-err: #d6a6ae; + + /* SQL syntax tokens (dark) */ + --syn-keyword: #5aa9e6; + --syn-string: #79c98a; + --syn-number: #e3b341; + --syn-comment: #5c606b; + --syn-function: #c79bf0; + --syn-operator: #aeb3bd; + --syn-punct: #8b8f9b; +} + +@theme inline { + --font-sans: 'Inter Variable', sans-serif; + --font-display: 'Space Grotesk Variable', 'Inter Variable', sans-serif; + --font-mono: 'IBM Plex Mono', ui-monospace, monospace; + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --color-foreground: var(--foreground); + --color-background: var(--background); + --radius-sm: calc(var(--radius) * 0.6); + --radius-md: calc(var(--radius) * 0.8); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) * 1.4); + --radius-2xl: calc(var(--radius) * 1.8); + --radius-3xl: calc(var(--radius) * 2.2); + --radius-4xl: calc(var(--radius) * 2.6); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } + html { + @apply font-sans; + } +} + +/* Shared list-table treatment (Catalog/Runs/Jobs/Triggers/Checks/Providers). + Dagster-like: row dividers only, muted sentence-case header, one text scale, + mono only for genuinely code-y values (.code / ). */ +.sb-list { + width: 100%; + font-family: var(--font-sans); + font-size: 12.5px; + border-collapse: separate !important; + border-spacing: 0; +} +.sb-list thead th { + position: sticky; + top: 0; + z-index: 5; + height: 36px; + padding: 0 16px; + border-bottom: 1px solid var(--border); + background: var(--background); + color: var(--muted-foreground); + font-size: 12px; + font-weight: 500; + text-align: left; + white-space: nowrap; +} +.sb-list tbody td { + padding: 10px 16px; + border-bottom: 1px solid var(--border); + vertical-align: middle; +} +.sb-list th:first-child, +.sb-list td:first-child { + padding-left: 20px; +} +.sb-list th:last-child, +.sb-list td:last-child { + padding-right: 20px; +} +.sb-list tbody tr { + cursor: pointer; +} +.sb-list tbody tr:hover { + background: var(--sb-hover); +} +.sb-list .code, +.sb-list code { + font-family: var(--font-mono); + font-size: 12px; +} +.sb-list .num { + text-align: right; + font-variant-numeric: tabular-nums; +} +.sb-list .muted { + color: var(--muted-foreground); +} +.sb-list .cell-name { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} +.sb-list .cell-name > svg { + flex: 0 0 auto; + color: var(--muted-foreground); +} +.sb-list .cell-name a, +.sb-list .cell-name strong { + color: var(--foreground); + font-size: 13px; + font-weight: 500; +} +.sb-list .cell-name a:hover { + color: var(--primary); +} +.sb-list .cell-desc { + margin-top: 1px; + overflow: hidden; + color: var(--muted-foreground); + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; +} +.sb-list .link { + color: var(--primary); +} +.sb-list .link:hover { + text-decoration: underline; +} +.sb-list .empty { + padding: 48px 20px; + color: var(--muted-foreground); + text-align: center; + cursor: default; +} +.sb-list-wrap { + min-height: 0; + flex: 1; + overflow: auto; +} +/* table pill (Dagster-style): wrap discrete values so cells read as objects */ +.sb-tag { + display: inline-flex; + align-items: center; + gap: 5px; + background: var(--sb-hover); + border: 0; + border-radius: 2px; + padding: 2px 7px; + font-size: 11.5px; + line-height: 1.45; + color: var(--muted-foreground); + white-space: nowrap; +} +.sb-tag .sb-tag-ico { + color: var(--sb-text-faint); +} +/* status label: coloured dot + plain label (no tinted pill) */ +.sb-status { + display: inline-flex; + align-items: center; + gap: 7px; + color: var(--foreground); + font-size: 12.5px; + font-weight: 500; + white-space: nowrap; + text-transform: capitalize; +} +.sb-status > span:first-child { + width: 8px !important; + height: 8px !important; + border-radius: 50% !important; + background: var(--tone) !important; +} +/* styled checkbox (replaces the chunky native accent-color box) */ +.sb-check { + appearance: none; + -webkit-appearance: none; + width: 14px; + height: 14px; + border: 1.5px solid var(--border-strong); + border-radius: 2px; + background: transparent; + cursor: pointer; + position: relative; + vertical-align: middle; + transition: + background 0.12s, + border-color 0.12s; +} +.sb-check:hover { + border-color: var(--muted-foreground); +} +.sb-check:checked { + background: var(--primary); + border-color: var(--primary); +} +.sb-check:checked::after { + content: ''; + position: absolute; + left: 4px; + top: 1px; + width: 4px; + height: 8px; + border: solid #fff; + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} +/* paused/disabled row: dim the row content (toggle keeps its own colour) */ +.sb-row-off td { + opacity: 0.6; +} +.sb-toggle { + position: relative; + width: 28px; + height: 16px; + flex: 0 0 28px; + border: 0; + border-radius: 999px; + background: var(--border-strong); + transition: background 0.15s; +} +.sb-toggle.on { + background: var(--sb-success); +} +.sb-toggle::after { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 12px; + height: 12px; + border-radius: 50%; + background: #fff; + transition: left 0.15s; +} +.sb-toggle.on::after { + left: 14px; +} +/* Prism token theme — mapped to SQLBuild syntax tokens (works light + dark) */ +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: var(--syn-comment); + font-style: italic; +} +.token.keyword { + color: var(--syn-keyword); +} +.token.string, +.token.char, +.token.attr-value { + color: var(--syn-string); +} +.token.number, +.token.boolean { + color: var(--syn-number); +} +.token.function, +.token.sb-ref { + color: var(--syn-function); +} +.token.operator, +.token.entity, +.token.url { + color: var(--syn-operator); +} +.token.punctuation { + color: var(--syn-punct); +} +.token.sb-config { + color: var(--syn-keyword); + font-weight: 500; +} + diff --git a/ui/src/routes/lineage/+page.svelte b/ui/src/routes/lineage/+page.svelte new file mode 100644 index 0000000000..7c737929b9 --- /dev/null +++ b/ui/src/routes/lineage/+page.svelte @@ -0,0 +1,1579 @@ + + + + + + + + + + +
+
+
+ +
+ + {#if modelQuery.trim() && !browserOpen} +
+ {#each browserNodes.slice(0, 8) as node (node.id)} + + {:else} +
No matching models
+ {/each} +
+ {/if} +
+
+ {#each ['Kind', 'Group', 'Status'] as f (f)} + + {/each} +
+ + + + +
+ {#each [['ok', 'var(--sb-success)'], ['warn', 'var(--sb-warning)'], ['stale', 'var(--sb-stale)'], ['fail', 'var(--sb-error)']] as [st, c] (st)} + {@const n = nodes.filter((x) => x.data.status === st).length} + {#if n}{n}{/if} + {/each} +
+ +
+ +
+ {#if browserOpen} + + {/if} + +
+ (hoveredId = node.id)} + onnodemouseleave={() => (hoveredId = null)} + > + + + + + {#if scaleMode} + +
{scaleModels.toLocaleString()} models · {far ? 'zoomed out: status overview' : 'cards'} · zoom {Math.round(viewport.zoom * 100)}%
+ {/if} +
+ + {#if sqlOpen && hasSql} +
+ (sqlOpen = false)} /> +
+ {/if} + + + {#if inspectorOpen} +
+ + + +
+ {/if} +
+
+
+ + diff --git a/ui/src/routes/providers/+page.svelte b/ui/src/routes/providers/+page.svelte new file mode 100644 index 0000000000..3b1cc8e65c --- /dev/null +++ b/ui/src/routes/providers/+page.svelte @@ -0,0 +1,102 @@ + + + + + + + + p.status === 'error').length, label: 'error' }]} +> + {#snippet filters()} + + + {/snippet} + + +
+ + + + + + + + + + + + {#each filtered as p (p.id)} + {@const Glyph = kindIcon[p.kind]} + goto(`/providers/${p.id}`)}> + + + + + + + {:else} + + {/each} + +
ProviderKindTypeStatusUsed by
+
+ + +
+
{p.kind}{p.type}{p.uses} {p.uses === 1 ? 'node' : 'nodes'}
No providers match the filter.
+
diff --git a/ui/src/routes/providers/[id]/+page.svelte b/ui/src/routes/providers/[id]/+page.svelte new file mode 100644 index 0000000000..2815252e42 --- /dev/null +++ b/ui/src/routes/providers/[id]/+page.svelte @@ -0,0 +1,120 @@ + + + + + + + +
+ +
+ ← Providers + {def.name} + {def.kind} · {def.type} + + {def.status} + +
+ +
+ +
+
+ {#each [['config', 'Configuration'], ['uses', `Used by · ${usedByModels.length + usedByJobs.length}`]] as [t, lbl] (t)} + + {/each} +
+ + {#if tab === 'config'} +
+
+ 🔒 + Read-only — defined in your project config. Override per run from the lineage Run panel. +
+ +
+ {:else} +
+
+
Models · {usedByModels.length}
+
+ {#each usedByModels as m (m)} + {m} + {/each} +
+
+
+
Jobs · {usedByJobs.length}
+
+ {#each usedByJobs as j (j)} + {j} + {/each} +
+
+
+ {/if} +
+ + +
+
Provider
+
+ {#each [['Kind', def.kind], ['Type', def.type], ['Status', def.status]] as [k, v] (k)} +
+ {k} + {v} +
+ {/each} +
+
+
Description
+

{def.desc}

+
+
+
+
diff --git a/ui/src/routes/runs/+page.svelte b/ui/src/routes/runs/+page.svelte new file mode 100644 index 0000000000..aaa798bf13 --- /dev/null +++ b/ui/src/routes/runs/+page.svelte @@ -0,0 +1,148 @@ + + + + + + + r.status === 'running').length, label: 'running' }, + { tone: 'var(--sb-error)', count: runs.filter((r) => r.status === 'fail').length, label: 'failed' } + ]} +> + {#snippet filters()} + + + + + + {#if view === 'list'}{/if} + {/snippet} + + +{#if view === 'timeline'} +
+{:else} +
+ + + + + + + + + + + + + + + {#each sortedRuns as r (r.id)} + goto(`/runs/${r.id}`)}> + + + + + + + + + + {/each} + +
RunJobTriggerEnvNodesStartedDurationBy
+ + e.stopPropagation()}>{r.job}{r.trigger}{r.env}{r.nodes}{r.duration}{r.by}
+
+{/if} + + diff --git a/ui/src/routes/runs/[id]/+page.svelte b/ui/src/routes/runs/[id]/+page.svelte new file mode 100644 index 0000000000..a4a9f127da --- /dev/null +++ b/ui/src/routes/runs/[id]/+page.svelte @@ -0,0 +1,832 @@ + + + + + + + +
+ +
+ ← Runs + {runId} + + failed + + nightly_build · schedule · prod · a3f19c2 +
+ 11:46:09 · 1.42s · @scheduler + · + 8 ok + + + 3 skip +
+
+ + +
+ +
+
+ Execution + + {#if topView === 'timeline'} + +
+ + + {#if ganttQuery} + + {/if} +
+ {#if statusFilter.size || ganttQuery} + {visibleExec.length}/{exec.length} steps + {/if} + + +
+
+ + + +
+ +
+ {:else} +
+ {/if} + +
+ {#each [['timeline', 'Timeline'], ['graph', 'Graph']] as [v, lbl] (v)} + + {/each} +
+
+ + +
+
+ {#if topView === 'timeline'} + +
+ +
+ +
+
+ +
+ {#each axisTicks as tk (tk)} + + {/each} +
+ + +
+
Step
+
+ {#each axisTicks as tk (tk)} + {fmtTick(tk)} + {/each} +
+
+ + + + + +
+ {#each visibleExec as e (e.id)} +
selectStep(e.name)} + > +
+ {#if selectedStep === e.name}{/if} + {e.name} +
+
+ {#if e.status === 'skip'} + skipped (upstream failed) + {:else} + {@const endX = xOf(e.start + e.dur)} +
+ + {#if endX > trackW - 56} + {e.dur.toFixed(2)}s + {:else} + {e.dur.toFixed(2)}s + {/if} + {/if} +
+
+ {/each} + {#if visibleExec.length === 0} +
No steps match the current filter.
+ {/if} +
+
+
+
+ {:else} +
+ + + + +
+ {/if} +
+ + +
+ {#each railOrder as s (s)} + {#if statusCounts[s]} + {@const active = statusFilter.has(s)} + + {/if} + {/each} + {#if statusFilter.size} + + {/if} +
+
+
+ + + + + +
+
+ {#each ['events', 'logs', 'stdout', 'stderr'] as tab (tab)} + + {/each} + + {#if bottomTab === 'events'} +
+ +
+ + + {#if query} + + {/if} +
+ + +
+ + {#if levelsOpen} + +
+ {#each allLevels as lv (lv)} + + {/each} +
+ {/if} +
+
+ {/if} +
+ +
+ {#if bottomTab === 'events'} + + + + {#each ['Time', 'Step', 'Event', 'Info'] as h (h)} + + {/each} + + + + {#each filteredEvents as ev (ev.t + ev.step)} + + + + + + + {:else} + + + + {/each} + +
{h}
{ev.t}{ev.step} + {eventLabel[ev.kind]} + {ev.info}
No events match the current filter.
+ {:else if bottomTab === 'logs'} +
+ {#each logs as l (l.t + l.text)} +
+ {l.t} + {l.text} + {#if l.tag} {l.tag}{/if} +
+ {/each} +
+ {:else if bottomTab === 'stdout'} +
{stdoutText}
+ {:else} +
{stderrText}
+ {/if} +
+
+
+
+ + diff --git a/ui/src/routes/triggers/+page.svelte b/ui/src/routes/triggers/+page.svelte new file mode 100644 index 0000000000..975ae6362f --- /dev/null +++ b/ui/src/routes/triggers/+page.svelte @@ -0,0 +1,189 @@ + + + + + + + + + {#snippet filters()} + + + {/snippet} + {#snippet actions()} + {#if selected.size} + {selected.size} selected + + + {/if} + {/snippet} + + +
+ + + + + + + + + + + + + + {#each filtered as t (t.name)} + + + + + + + + + + {:else} + + {/each} + +
TriggerTypeJobNext tickLast tickLast run
toggleSel(t.name)} class="sb-check" aria-label="Select {t.name}" /> +
+ +
+ {t.name} +
{t.cadence}
+
+
+
+ + {#if t.type === 'schedule'}{:else}{/if} + {t.type === 'schedule' ? 'Schedule' : 'Sensor'} + + {t.job}{t.type === 'schedule' && t.next ? (t.enabled ? t.next : 'Paused') : '—'}{t.lastTick} + {#if t.lastRunId} + + + + + {:else} + — + {/if} +
No triggers match the filter.
+
+ + diff --git a/ui/static/favicon.png b/ui/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..97e37653b5aa119c35b0ecd944dc9219d9d31250 GIT binary patch literal 6618 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

j!daP9BLa*%(oz;q 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 z%0A;lb!+B8C(F|9qC(+MPQ;P_%UZnv1-1(~K1nrDF6_+YgJ*b0&sFZTV5=OQ@JN#| zX7t6IYQEln0vc^IZXl~uYeF#MuVU2ZIVGxPXxsChPs07Iu5AyRw;RbNA6DOI!a#I@ zlA+lL1)`0uGxJXEM^`1_bT%pQgFEjn>I&%S%OR=MUhc6u_eGB`ho~W%Klyh9{<{%q zLfr@M@^ri~zwJ!CNqqf^Awd&v{gHm5@uy{FgFUtdm)cc%#aS;(XQi8{(eOdx0l(D;p7^W0L9|L#Cpq?8^kTNoZ4&7F#m1%jlvrbRR7Wr@JlQ|Gb_7UDD@ zyR?p1h*e8spwU(^1|@l2%B|8Y?wjiD2)?-xy)c(z52>f<2Ooem*ukb8tbrSEWyuL% zd6<4!{Jco`u@+v`L<gGV(axK=jeAXxD(=!+YaC0!S}P=ohz2% zY1ekK)aD>C1MX5&nppj_Bjxt=gr;nwn-@PHidLxC@G!W%#3{6f6&dh}OD#G`6orx} zS`!!vjk{^|`}05nh&a?C)8kuKG|}viKS2PZfyBfo<1NOTP{h<+$a7T1C?9)?M_a5q zx0^0OLb6ip#l6KkT&W|+xI~k z43rYl5G&mcykV~Up-sEskteP$3Zn3nr-A-6%KJWZb;11ol#HZeq33G*N}0yT{?SfO zg}qNktS~L^Th4M;KeXlgN~|m7(sDJNf7CN?B{I_#Ul0#%gb$>Ngh?<4>qSQu zsb--+o3Un(OmS8q;E_yB_@g6>1~@kSe=VDUllpnpbuPejF2Hr`RNz!}hJiAhh0Jg; z;3Y@aLOGv?R5c+Lvz1H$A|8RL%g04$F~LqAY(iQ_P@ugaca)&4Sw$O!+~T3|4h_t; z57cDo-@0Zn(0_yf%e>-158g;H(R=_D)eq|RC^mlSj=mFE<)(+DaNX$-D}6k)Sot*` zGtxV`(4Ee`JU25ghfD!U4xg;?5zsya=CM`t!7jiNR;!*c@c5t3zbNhX1#P&lev7ef zzQED*=>ar3YSF*T;{d*cApb-c^aI>;%5BLL*|5q^E?F!&F08-_jd5v3OUy*tE``|_ z>{YEJPov$=r<7Lp9MMN&u2^L|#rvjJxae4&4w4#7zKM6i2eBGGAcPI9CAakw>HKfs z-3J+k9}1R69%Q+mo&NNYu@Ug8zwK~2bPbCt=8vyhtXCAYjZh8B2vEVlym-;w+!~KI z*4%e@8FtnTkVgEa>0qh-lSKk9z~MA3GrCgKfn@u2yi6+J(+8jknAPguZmmK?7J*!1 zUo^w$rd&$Zm|H9w{a36r!+|3^W85eMX;eXD7_Pti`yLeVqJ*IbfDCtI5=ltfdk9#9R;pb?`k zJY3c0lC8GjzQjzLz35;&3bkrs79IZ>q!E!;= ziceE7vjv-IRE-7%@_}w;5V>-t`ed6EeRva;%c2P`kzFSw?`2637yZmX5#`@YXwHt< z<II+~94oX2I7!NYG9l*(M56cU*JNrOQq{2|u$wR_LLVg$pqk9?L8 zdtd_X@VM?>Tw8sXK!v#AC;OeKpL|K3WzRZXkwu!R@J9Y%{$vd>UV9S1ffJ&a?ed>` zNE{xcyt(+%`Y|DgbLG1WNpZTCvuJXC#s-Mc-*a;jukGHEqzweo{I__=Klic#5>{AR z>i;1!)u+nNdgb9y1@3dJo?{)X# z;-<<+BSG~)!xbe8wptIg2j~xnRJFYl2wwI6WUYn0459!^w3y1m4#3M4G;SAAp=Vp$ zB?#>G7wo@136-$&6Z_DDB~XzxfdY~k#3X8oSib61$ue|pZSf%FIr>?GF0>!$;E6no zR2ooRdV4V0polTZ8C^7%ptU+Trz`4uaL2?yN+%!7+jR%+*adO zbRSSKP#ISGpO`jS5=*-h>blKUc(7sFo`b|^TSmQ%8aw?adGchl{Z1^I_$=DF2<43zCbaogsdYZYG#6ab^og}1Qp1kNzh?I zFx4dU$)SQ2&X@n%wfewZM|0ebA;%6Mn^D-mWRMI1chb6fdZ%zToyki!K?*$IL!pK5 z_^jF=X#V$QBSVN&NOxRsGI|2jYzwGb7VTr`!NvzDfU8?4$<-{?_X*dY6CXf8@Afs(0P<7HgFDrGt555Ll+pds0yY&g z|LwqjGPVM-o7Qin=RR*9_$2Q8U?%Nzj4}rENeuG5qxevG!M>){}z%Dvk1T|$(%_r`nW9^wIHGIaqwFrJ*uV@LzpRbN(yb`j~e zBNVYsc2+lNsRad9qjyz|I}VHnZQI%k={t%ARHc~rMWa8ltS6;3B!aqXt<;C9kjHv0ca@_he1RRoBx0t0Gm?(i1ojX8SjYAcDu9(&ctzvGpzv!Q!>E90jJgNJ zHieHtoqtIaQ5bec;%(o9OkFAsrj%k~48k1>oS|y|{VbKL-kUdqmZ(?eXr$iz7_k~= zoKAZ8kNFvF)Ebss7vJD4KUBC@1vB=5iJ@V>te``ogMuXtZG}VqxHJv1q`WS)0l_l_ z#%;HMQ*M73NNtU3F{EW&{cFSaJHM8Tsb49-R~Le`dbEmH?fwFi!A1}4p3DAaJ=^LeIax| z+3BRLzs?c9x0bS3L(g8|aqD)Qt3Px3{=q4XF1O2ymurUe?WcM0leOl)ecpe9l-!y! zqeAr7vz9aR*CXLqe=S2#VpG^b+{M_DJ@P`QC_|$9TyBLxD9wD>i>MtSA(P<1V-jO= zk5dMt;bmiVaN~_8MQ+m{PoS4DwyVNaAMk0GY_39)AZ@C(G^B@nVH$NoUfrExZ@iXd{1R(N82x$;jw6J?20oH%8 z8392#rlt<~c{Aeu#!*$aKTNh~;A=Z35S0VechT?yiyQS7H+s`AR~6>Y3GKav<0EZ4 zSK(BQR#fz6oTL^b@D|)LpVOt3)$&NB7-x{DdbVbpn0`Sz*?lz68wK=wd{<9xuJ3Mh zwat1iXtP5nNS#=@m#nsuT$uioiU5)k4B7`lmjfgo@w5ufh!aA0*zV09zuA5O5eWI9 zJ)|OaR)C$gGf&)O@(|rxIQnG4NM=W*Z>36OAi`-|SXUBe01V&kycKuIXI&37Gz)Pv z(~hq{6;kGnpqau+llbz!kh$DtyW^4gLIXjC}ONUm{9p+4cN1&L1_08@FUZRLRr1V`Tyl7=# z#_vBfC?oYX{REcl%cNNmUUw^wNe6fTb0e6Ve4<{vKNg-ZEm^kKa9`knosmC02#Wpt z4=m(?WKj|7@Yv~4IIGYR$#wL0Um_S$*F;C&lhuukBDN9pYn=l0X*{i{bX$%EqQ!7) zRC77Aa+5ZgI)jJJ99VA|Rt?6Gj2tOs^!(y-q#!XuRN+JT+)dE>hk&8FAN|4teP za1zdZRpYa4`^~NH`Q^11cvl`92_}}@NOub1vpP?js>4;_ty2*NynDeVy)X<)pS}y$ zNKY<@m=A@Lct(!t3SB$N_h`d15?#&A+_+1R(`NMsPLN12i(^HhM4)VGphaOgxF^^+ z`{lN)v^Xo=dG?cK(&^s+bOoZ&JO3x>JK^3r#U>oI)jN1(NPvmdcgsDHcZjJm@rYj} zHRLkj-pkErQF#ZUNR?oICoj*Huz!#)qRjE>E7~EVsPdl?Qo#%Y2Iv+XFea23y!r{l zV=FluD zl6M&<|9*T2YTNv1u`WxK!UGtX<+M%4=czh6yfKCc^)f9 zco2t^QrDSwgD*i?CV( z_x$zCo@y($|MP-H%|wME0sY0aszwJ)(^es9;>?plTto=0f?p6tUy4%9UXV`&Q3wS5 zzA}7LHg1r2Bvhm*0TqXiqKaBu+J+<&)kdmrI;Ni?kLf68#vJ~cM~m2Qy2s@6i(xX) zonDH$D6a^vXjmi|0QO0%ma*jKj{k!5AKjBsJ}7_!k+tX1NyGZ@5Rw2eB6h?H8Gz0i zrMvLi1ZuMtgT;|qzlcWpV#NezS`*q+YXTmf7E@|Ea>_GUygQz#br=|AfslAmGqU(? z`YYPjbzJ9lwqDRu#4e$y`Tw0n*_dV<9gu>L`&-XvKKhZgBtAH%By`xo=C`d}#{hvf zAN793z*|xChrbB3q0yTlr=8@=uZrO&NYN8ze<3cXfTGxA#2?2+8p4eknH!DrNTTRe zouKR6+q`A1Ztf+UkbI!NNs9Pe<7xW;{3yE_<|K&Z+ctvt^@WsOk2BBbq4MF2y)Q7yZa+-{zBips-TDOq7V$Q!dmT>p9%7fG5r`*{+tfdJy z{|IB6q`0R;MFF(OY-76p;tJjRp?YEg`v+A<(+H&W!xjE{0Aw13<-H>*(L6-4Y|{j=ngBrLCiol~$Ron)}BXgKy<5OQ`+ zp!{T)a=o_KwsyB!E<*3)U24|5w6xJokU*o<^~hsU1JDO51^)Tw;jSFk&S;Vs17HN4 z@3h0}b=VUJ^&o-MAVomU9v^Q^Ra93VMYnIS0c!v$H?@y#TJgWBWGpi;WPD`MLKNFp zm0_`s=%|R`E^bjJC@`4Hv?I&PfC34X(HlvLzwN_kqei*+FD{>MJ)aQ>+ATW!kSgdx z7puF$bS5G#Hn)9cM<4IV(mt7uF*oiKVsEv%{*+PewT&-e|7^A9ESL8k&d63X5R$IY z2oDxnoIFmNY<0t^yZ0hhpPO(bg=Jv66su`G^lvNCLPiZRy4pqC?wo_O-yKDRhk>sC z0cKUeq9Dj?BQKSu?->4mqDUG$M@66d!Bo;lIM1~B^Uy<-2pI$l*(!-RMON>xO$zn7 z*__P<7(Ly8Zr@24j{^3$-wT|{*`CL5&u8q#EjAF8XkZVH-rP-hsdT@levWXAlmxmw z-1Mk&nt%n(TPohrf;-dZ$(9C<5dpv@v(ambHX;jLP#Wv%hCx#K^guC(jq$gwU<8MG zhhN_O0`BW>C{%~A^M04O zwySx}s5^(mNI&zHxQL22AX?07NQbPpQhosigA1y{1_WY51@O2oRhX*u{JvcctEj5> za{bc<01toTbLf5LMXJ?GI|{857v<_kXT5c*DTGdoFIoPR?HXeVIb)!70h!l0*V_HG z=Nk0$6MfLQIwAsXv=OUsd_2DPgTuRIbvt?Fzjug%A_yBrzAG;mVdd`1;r+Tp`>XCH zEQI8JV5z)WM{YcOv-6)5KU`(zc^44+kQQAKPnG(^(ziD2_Fym+6jcIhh6|i=e^h^W zP()4MCAB=BaPm>mKO^9j05+QZ_@rSqZsOKEyrb$hz$`b)h!uo^GM97=m%?Smp7ac_GQ8vJPcTryE&81<9!blWsy|nOC4n{fn za=fAYeJ6o9d8+jyJ!}QW?pl+P^Xug%ybrr< zm5~2-5+004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv0RI600RN!9r;`8x00(qQO+^Rl1px&L368JmlmGyL07*naRCwC#y?20Q*HtI{ zJNLd!lmcz4zSTJ?GqWY1Qhr z=z^%5mj*=A*H;}&C_UWGKO;aA|0e$dhyYSV3J_t}nQghv73)slaQQ1|cRjlQiSLrY zs*EbjB7jB0goP5>NO8`Y@=^ZhLN?7XP=DQ{udn-O?R+=N+?cSC33tVb7g`_go2287 z4m21}C*YZ=vIixXD|_IXs|J^)AGFWbsIKa_L?)Vf9J{_7wd^yW0oDq)xgMD zRV%3iRK`;y8m=|HM@hVVgH|Y zD`b{6yMrX@@?I$ofYVE;vF0CKNt>$|9F@VW!Lp@mY_m|+?xlK#?{-tt`QT*tG{3xD zpq5$&yM+&P-HIsrOpzjh#5j&am65eOE?sfqt1h_Vot2GiTQ0X|TOvk@A|fyg0A^LS zb;W8No8Ei-SDi<{zUPiFdb86+3U-Z2eBTD@3PCJ`-GKTX_$NQuolBj&eK)Bs=)A!0 zz>i^L*KpDttWrJ=%!gsA(1KtCDg60Dq#**k!|He0z3pAXr^C>x2D3))8|e?I5^ccl zA_}#6a@#FUUkYz0-E#e5>-#Gh&uJPq*68?YKk$x?FMaF!E3S~LHVw~dIs}jqg)K`# zBMTI{uF;j6swrj3frp+t^z~01_|`|f=_78-wW?JijS%rtCv4E^g%=9VGv4i!%fs0? zz36Pad#k;3UXLspPNoeH_3H_kp=HhPd}cBXxMUG&SitTA_s}_jUFz<&MJFn|?n&}U z;#*0lvmli?X~1rmrIoTH_Hv zTSua@i@sfbrHgk4U5PTVyX2e>`vJR>>2$$E-z(QAMFg;MT8`tWYIXC)FFW&%KfB@T zt1Bz3ty$A*7#2~!7q9p!BtA~0DS!}!AqpT85lu=AYGXFL-oA(Ld*%~=cI?5gH>akm z)ta(vP+%bf3KYG|Xg5KF*gH*KueiISVQ!km0K3abcVIMUy!*leyAjPNcv#l#0$3;| zMfZgsDMw0$2zrT7xDlvp$cV2xMvXV)XUq=d6r3i`uF>b5n3MO}4wCe1a zoN?_NH(hd}XX~k1hakdAYYQyV?y_*78Rc3ufYl;k6}m(SL4*{Hfjwf`RdwKrr=R}p z-#737>am?q>58T85hxOYh!1;-qOvagF@!V-u`qaFqG4{HA;i1G%&wuL#Ra=dx0#l~ z?z~fB=NKSEhHhWl+$t$c2kcV&Yw7Z{qOXR-jbX`w-7d?V+nSb+n@hp|Gd51kYk5!` z&%buVweQ*S%2(OrV<2fXT?5fth0$?UI%z4(GYqC1;o>L&0|{VAJdY|96}w8UBPSmF zmk-q+{KA39zH14Tu4&5>QX=p}^HT91wKlFWLp5{;PeX`zha46Re!p60TE@Erpt*VO z4ts>s`J;sZyC=Dn?L_mf*_}`OP~m*pqqFgVHk1uvFQOjq4l|uRV7K>Z45@A81Yea4 z<2fyl$JT8*^VM%#d+~LrU38^mSL)5C=NUyr!QL%sxzgNJ@-I*ah9u38986jWU%=Ei*zFP@4tlZN$4LeQdux8ao)l_%t3C5P?M4xW;p6WPHsz zSD*QY_pUwf#kI|ov-OtiIwWE#6@(H?21uZGc_4Q}L^9q(NsU7SX5=AGFHL#-b0W5(_fsp|CU3~JYKae=o*xY z%HtnPQLHNq3G659KWx+u*zHL333lgcwboZn2Kmu^(Y$FZ7OR#$ zfZgP(Y>*m*KDbbneQ~Kmg)9LCHjeADQKM_Oy!bUM&c1HTH7^_4FzGbiX0v5kT0>K? z-7G%FER232S=3x|fsSCe1b*gY=LP{B0>mA9Hz*j7m1T*oE9~t1-uDiF?L)h7|AKH6 zWb0~GKml}?La8`Z<7r-K8SDZ=QvKwv9wY|Bt)WuSPQ9eWC0ANQm~u4mTrEP zAOp&4%p?lC5d0AUB*fwvwyKs98XwUop5Ar%uFpO5rBAdDJ}sWn)f$u~0J9(r{M(jo z!!yDLLxaP+CqLN5oE%(A@a{6$oi|#rvAXz5jX_kQuN=!_+E7mC(|hUzpDBlg7Q`R6 zD5J%9jvC}fk>P3CWw2YC(0~dww>@gUVwOmt6hYW{twyUczGm~9^R8Nd>Gc~gziwjl znudo~3kF6jB0tV;s0Srd61o#mGI2`glBsY<&2G_kHycPOqXUlL69@xB~0JT zvwA0>7KnEuR6G`Z$ zAS48$U|$$=eg#>$TkgqFq`oa)!?jhqO0Z3>_#rU`nqNiAVf41|X_KztQ&Mn7BF#M!_8MGu|~ zrTpu=)qP!U6na4br4$=Oqy$tMwaKF9)bz6-{mbL`-g0Q?Gs-j+X;rB)AOVvjDBaV} z*#5TB4iPQdNL%cR&ynV~6{F|cFLib;0L@=dUNGhlvDA>Fh5drnV-9+R#jy|%D4ia* zP7Ep1MR)6+c8jD%9~GU{xh z>?zx}tChx7{pe%&&pv#|?yr5cb@(|&M0QP-3Xh~{gLg|re4FN(TeTLF7PyEf8FcH# z_SR?BPcn|pA1%seuVF6y;EL8g8wOjCHVTFAoRF999>#9L?(ibrIn;G%BH6W7>n>5G zi{aGlCR0nn#V9U;fCvgiB<#7yX-aK;%9VFI&|Rk3W#8d1}SrgwkgpP&Ey zU%AJgXU|xb5zo*r z%<3cJWqD(DbXFcdT9_!AOF{(qL`zz&crwx48)8w`V@pFi^i!u~n4<1CUC)~*mYY7RE(r^sJ;(OgK5Sl}M2SQzmu0;d8WiC;Ydm}M!ND+~^PP3_Mqnj_k zX5y^tw!QofV_Q#QZO_&n_KXk|Kq3SXXXE2iwi*rOK#&)RaLW3aVedD7)f+gVtqT0{w- zD@70yHjZOlYi#oL>u%a|-4CC7HKq#$}O+vSTw*)`P8c2B_sHUnTS{&!dQ#<$F`iXtFeG;<=oo1_A852@~ z2>YJ}5!%xA?oc>E4tRG_KPnP;ZTfOjBRfE9gP{v^Y`;ZofQWoJDP!E|V>9_l%$&ud z4mz?SpcgRMO)_9Unq^_NybIB>PKHfsc^YD{J20d>=-TwTR9`f(+dd;ribNRfIF8F0 z->_}R8{V_&s@JYPkR6 z^B+opUKIc$1_O#Jc4btb*mH3Du{)mr)L+$iJ?zvQcC|*@76`}g&c%V)x&XT!YB|U8 zu<(smh$K4t=q))}JXo5imB}(KQCbGO3p*6%ZNLA#SK?&zUWjN|!EXN!3|$8enIrW) zb_RBWT81^fu2>_jD-kIOyRPH9u~o10N%Uh<02C1$qqGK5b-dE5HxJ+Uz2|TJ$kgL^Oz+!c zTNd;P6j&gBtl|PLwgs}CAn%}t#A3`F1qB#vT$|%X=~5B zZtKf#+HlELv~py2y5YGFfhn_XBnlPnQtbP3%Jq+(U|^ns%XZ~av3@!JpF`|s}TF=f@1UE?4|8PZ6$ zw21AMyThnr=$)iQ-TgxF*VuZS?&8rP)Q5ADXoD1oOINgZ*om{iBV-Uh4AF=4 z26p>w?fG(J=u}XmK%vO&wVIAwU9)-pmDg{0(VI@a?rK<-X47+9O@FJHh9VInqAucw z&4sA&MtoOt@Vbji_egVeYfYC)fD8l<)70S8jX3JKNiBAr&TTtb&NXuY%nU*O}%V4*A%0%wXKb4*l zMqi+6kRA&IX3kP1>G)(J2g$JeFyu|7()|QGc(9u(OI)7BBO)PTujP1z(G90>eZ`NQ z^6Ixvp1rBn-tPaW@OwjgC!LLH-573$vOo8_v}-YSyl z0tGo1L{$hwvjqWq1F-A&eG(Cn*0wb|fg?Nj@4Dqf2fz7g_vjweYUvU8ZGfL-Vjr8^aV@Z2&aS@lCtHyMt%vVA47{nz=LB#X{lTIr}j5 zRnqot*rrqVUcZF^yCEqeMKGSzXf)Nx_?q)ydd3@nZ1qJiTD^7k)Rg14TnKAY2?lG=q}SISoou>*wP8sYz##0a1jLsdsK)mz7QK6B#xw?6Zgf1cX? z*l4W+HKLSaKqRO}oLxHg-kf{74jFoKNWf)UrWXj!1KvG(WWc~{(iiX@>~^DpsdY~d z>K5#FdUXJ_IAFIwDvNK{R%6){*e$G2nv}2vJI{*k+ky_PFH`IGrU+g9Aw=@L$Xos6{X zwp&QU5IB6UFUD0FyrTVNlJi~R-33Crm?PMo|B|-ZI@O`n{eay}J``pjWvhZXxMJL< z<9U_w@hz9XdgCRpIqg+9s5Rr$({8KP(h9x821i>Au#zI(qaP&5+`j@>_(346`T;6-$lV#~*t7$TvTJ_`6?f z9eBo>nX+pmtZX5$4_p1{4PgqtJ4NKQQBs$Zc3cL#%d|{OndZlB7fV+X3{Sa-S?NAJ zTp9>1vghvL)VlLs(oXpd+KnNRO?+~hBHu07aXr^wwPy9X*Pipn_pH6-#rC?f#&Oqe zwFCfExO)kRKF(m*Hrw>2j|Lb}K!)L<4*m?8t#ZBEjuJXuX_E&PdLr#-YYc_nrF>!%I;0@H=ngyE!XhMt!J;hWytTGY-c_#1{H=eOO-TJEh_oG>-;24 zRWhv3kB3C`2p{bFo6=YSfW-)r>oJXuh*n3R*m?LHpWQT87iG@SN*!99c8gXn2j5BMc}a0Wp%uP0A#apaZ~5zFy+PS;CQS z_mlP<&0;>OQ*j4>z(%%Z3R;Saz7hxtL_`E60)YsT5fM@@lvUA|1$ExBTV) z2X0ejh%9AQ7y_s;p)`_~!g6JKkT2CqDGirtnNDsR+|;<}lDi)o{^AY;-W`4}^+8=Q zHW!3ykYKk28ZJb=<|BumHM?c1P$CGC5Q*!!4ac!pZP;?fYfgLF+qPYOG1-;rmNK48 zP<~h|5dnxe_RY=u;pSWy+DA_)1-tFgY@c9v(Wz@K^xR6F96@a~;FM7&hk+vqkPz&K zD;WkL#3U@_$LnP_0@9Wx_2%J+?w)%18;8F2Z_e?(n#ih*Fj4q%p>}#1>@L$Xo%}SQ z*)hLsYN%;xloG&Vgydbqy85oE$GG`je@UINPq_jMO9Si{=F=P4O&GI-w8-B^L%(6G7~~_}K+7+R4YO#`WY@GeHGAx#+Yf#B-=@BI3z{w~RnRMZ<#B1bmIE>@ z(=weTH0-jtUpi@o(!#}ieZf$VEbZHwA-(gwyQoeJSg(1f9M~<=kr9a8hC1b(jW2!2 zwu>&_c*gm(VpXHbj_Uy&95MhA@z41UP)-1eC;_VyiDwbGloX*PEJH6bL#8FR+`uDGBhL1iSFh!UG9{U26gS6%vB5WfXZzv85+gsx>Rj?eBT@ z(TDEWBcm>Res@WCmNw-YJlO4ZYu_?0(=rW&mY5*!L?^KrnU9t_iP=>~y|b|#?B?A* z@)ld_fyYb7lOf7aLcm#2cB@kb2>=nfuDj|*Z@=`H|I=C{vnRY0$D1xIr9eVHgaLsl z6)820q}pk#S@JqGf)A|IhhbrTchGWJM5VgJp`>EBj6z2QDOx@BQCDj7AcMULiS>oR zkE$#J5fUE+d&+ZJ#%*hr#cK?Y9XPsv^SRScJ=JK=T9pce#dn-p*uj!F0lWZ(?=mgZ zBGN*S`DN;t2Gvq8WEnf)t}bmayYO3Bs+4jh@MAm`OyGKCWV9_i$X^RsUUO>gj{P{} z*#0At!4D73=gl8^>Sqnjd)C>t1v4$cbkk zf7sT#GuEVJM#XjQo1ueBZJCy7nO?|ratn}w)b8YF(L7KdWA*Z3FYKOs)69G0wlPxY zu|e#{P`ulT;2;0nnwoyePxHlZL-Y7(#X9|r&8KW!yL#2AFpFoH0)utlwKVBLAXz+_ zS{!C$U6s_S>_(fdo7BCwcuDDZgT;dstsUm<{gKKhrxGvT(B=mn3tLirPX-DwZp&a4 za@q>IV6~jR%AT~8)`~?0Z~#|$#8#6Q{m>iV{Jo^jiX~+iHsXV7U>c}gg{^- zCXJeH%;=2kJ@m-F#~wMfdE4aHEo)C%H)fL<79%MSPTzQ)M7Tw~i{Q7!Z{#slaW!(r z<`Puf1FwntHS;e5Nq1|t?kiJ#dPOEU2S9$K=CQS}N0-oxL4im)49AnTwp_4IU9y@s zjoY>&X7X5E_FpH4XhN&e9A7hW)oWgN&uw4N20BZ+7Yr)qT6c_E_TOEmWm;fbc4jD~ z;gm&7W0&_yMj56ju}~DhDzmX$dbV8;q`xh4#|XT5ff($z(Sru4Lwvslc%BHLlq<69 z*(1B2IkNt=Ri|%WeaiY3HQVxx=NXZB4MD*sruK+8bzgcDbptm%FNU{h9~65HNc-2i zOs$X+ImSdzAw_Ql`_PDl$OySiEVN}5m#)Qyt7&!B0<#Bl*dK#|?~4*x!irF>Rg@xL zdy0>wiHf9XdHW8g<;$5fgwxD2Ez`+LOF#rKAW7oKa0;U3_F4o=`4Up;&E*LUKw7h# zWky1MM+A^C41@)uL=i~&UYPrLo!I}((dy*rhIQj-pSf=BSQX5k2tXX6qaZ~PVxZlf z5;o7EYoO^aoqN51HGZown>sx)~OzJ%*vpmuZ=n zDWXN&qbkKq`GUgD=kj}LQO5ixonBiyv=GYc?)zQh^{$idtL#f&ts4}=R}4}Fc8lv3 zp%_33Sr8xP2srdu5QtW=RBOuJedxgRyN_+&yz>0*8&-~1v?3$$7)*XR2#Q=Af*oT8 z0dO2VszdFM1~1NE{nR`H@#G-(3fstNq6jSgIVwO z-WVrK^ToBX+Pxs@ZcCQ1A3K%8P`Yv4F;Sq=it+H=Ge`FAJ25&|IsLRXTi35xF6}3 z$wJytiyRfEmw24gQvq}j%0%k;vec^)Lv_06KUDF1UF zX?X>{7_2Xm*P@nLdbteETz!x(zOy^s1^QhFyvNE8Jud#fj^3cLBQT zVe_ru4ShO#Dt|Y1ma>P1`-my|EZ(6;L=0*%jG=Wkb;&ARvB6qx5dkh!!zED)07R63 zf`o`BsagWU6Eof?o|*mpv#sf)Ezv?+$O1eg2x9eSHYSP)7uO&N2zRnjcVR@NpT{pG z>a|BKQRla18UXd)_HDMsmnAk|BC3xmXc;`snL27V`rI%r<@bj{zk9NDn=r;eSRbqW zt?U4kIPPg?t3YZ;Qx0O04`PS~1dLalsmAk%r}jQ|pt^eOq6@cdTQf0YE05Wj6!H;C z6d^ogAUvdPkn??4y1MznyMlpjXlSo}F3|KBAl=Aufr5n%kpVe|MhWMw#08V;+*Nv9 zfdR(|lVTw#B7ufLm_#ZXRh#O}w;yhP@|os0_qxp!j%cBZ5CkmY*2d&E)P(1EP+A`H z@isU8E_HdYU`@=g+|>)H#+Gl+XZ^q8BujB$!dO)p%_H^i{rlq&tQy&I z?xwBlR;(JW2#aB`#9Q%$Pyiv6%OS&snX&xc|Gjsab~FT*u>N`iGz8?8>lCmgUE$aH!Lg7S(pUoTG?8-K|jCZ@FB1UzAjfyAW{sqfCHP*sg z0|zyIci?*JK?mkp$v1MF?_&ydkQAZil61>V&5RgO0Wbm(EG6V8Ko?0mSQH=nWg zj8i5zO^#Y1kHz1cmh|O>Z&c6r%gEz*PQ2|T6ngG0Ay7}JKDYMo(FT`@(;`sxlV^`hc!3(lzXH+?FhIuw8DPK9$Q ze1UFesJq!9sC`#$-qx;ruo7}c()#+PT*;*MEmKZgX03uRk_aBJ#?cDQHj-&0j`L~8|wltV<#N#{n zH9xhpdG8)}9Y8TGAwl4YkVO@OAWU=0k0Ee%@q>M^|FK>|Q)(A7t3+!cUBTSU7lYtT zH+sI|Y=E|m>)v7_nZIZ=>n&*oc)s{W_3UYy8-AlpA2d)s)I6bvyt*Q~S?pxEw3oB< zN>cvAr`|S=7gz~Og-It6-AM(~OF6ovB{HP5#mp;%+CVL<@UxP+c@V6p!dFBfs8|3F zKRUp`IWBOX*3U_lF4Eaea$nsRP_ zuKA^X?!m)`*$5GAfnji5%))JVh3nt&V7=5UOJVrUjk-pLCpl_K{$0K!QGgT^4yH zXEsoD@XBd>-4=nBM`P$bJ~CXQ@>HZ<3eiJmA%H*?yK&5E9-ck){P9(5$1XZ=%f{8? zwxv8HzIQ`f! z5D5-wlde-)dNlW$r?-CHorlg6uvB+3YfIZVf|^EtHJHN!nU(_Rn;(#~B&yz?I!~H& zPQNR621)$ewxk^mI+A#88Qb$)y8C)UK|hsjh*Ypnv~WblO~1gm5+ z7zoYX`>Mr^0?(fOW^Wt(xpz< zD`zo~Hfs`+wBr6Gp@HErs#}UDiBS#vIhHcxHZLwkBrCK`0%53YxCdIGh zhGR-j56H!Bp{Yczo4c2B)ah2}Mju$6-=(PebH(X)6c>S@%#Zjo*fJIDo3bqtnmyKh z;I3T{SDxRtW9`}7)@@upVg#NM6B=U$lI^Yu|Gl(kyR?t>46-QqqsP8hgauQ-%lOo} zWFhMH_k}4%2IMdt#+ahZSK`V|)@fsU#8QUgI4nv@~`T*sjDG%lZ<~`1i=81zMT(5*5A!^;h788GhN5p)+OXS`pv0*=RKF zs{VmjynOBCn*Z^;zkBMbr}%tj0TDc72z2`$xBvEU{Pxq&Jag>$(UFlcQpC)ZhdpV^ zk1XmnZ@HEC~Uxk5av?o zJ`?gMs`+3Vs6*w79!ue!KAm@w{4^LN5>0f0zlRWssD>d0oH`;GPtpaG)?}5GB9GDZ z5V$J@FxLp6AtM?}@xG?{>T``x>~x>p?-}+)0Sd6-AMhLcd`humx~#a8{gw4+@I#OF z#Dzto0qV6PK^Ae-ZxiynHkiQd86!f%iikx5MSV~R5s|Qu2Wv$ZvTRFf70-Dg^mN;RAwg5cZ7F4yM`OsGSC*QH=UgQzxCoUenF0sk9s*G$mSWIzj~v`% z_F8MkH*H;Y;r2}{sj;98?!+D5r?94Jwo(Le zeT^=Z2e2orEq>7kebFjx8Lw0ovA|_AzA=WN!2}V*ieOt%gc;Y|e#HIcuEsZ>=lYZ< zwgQ-J62majz)X~uVl7vG775p9(RqlIa%BM%|XZBmTwGNIHXMWi4A*mH>) zU{4XmGoELy-Nswa^FU%)B#ambf-uB@4cLY8$hc%&Qg&5~9DLH$j}sJ=)}$@VW>Nwo zrC5@%CR%f4&0?W0a~N77spUu+oBU_~5!6b=^ITz9gFU4@QHmX-)X2FnGaI%w>nFr@ z1h!}`5(L8&F<>D!9DdeN(>kX_H;gMXD zei=lhJY-cuGtifYJyuC9Q(K~F@`Q+xQjJz~Y^3%p@Bi=b{jr}|zjpG(iD^<4v_z;Z z@Np;`nXS(PvUb&)Kloq2d+xbA{@btr=1jdYIyTD2pnO|ES~QU3w8qCrx1PFvc4k&s zj4>iYiin6vDMggf{6x>uy-LUMUI| z5B`y^p<*YLa{(lZ92^7?VPPS_u2fr2^U%Q~G1!&dn9kXv2SHLk{*%PXNjHK)er*{Q zTgY~2yyx!U_xw}GR;?XB@0|6UCdX|{83vogBqUNMs7SK+DVbYb^{&Oa6km2iu4x{( z)%6Ci1QSrgIvGt`iiAW!E>l(HtPyj;7Hh}K%F3#Oz!RD=04WBPfJ8I_!kS=N;sMW0 zyI0{!$3{9{M1TbJP0LLN{_o<*j`GbGc^~>^v5;pqO^aBXQ1e)Wao=bZj z7sdTv5UA7@ND&mXcr7uWqigo+jh-59SQGY(-{4wRQ+KfS0yDWuj6m674i=w*2Z(~- z>)BTcGil{2kM=yMp1s%B#D;4#2c5^iqr4U=sOAij7F~f-zAil|qHw{aIxOuF@-RGJ zTRtJ)!%}UWbrD-tpVKf5g7_{C5<2Ag>1qvj zTVqx1eu|#EA0pPOb>^wt<=MM!tyJ?krl*AfWocbu1%a>#3C9cdY;%{$h56x-1!FwV zpsGnIhVy}jgL3WZPuRqaAlwZIU?ONssZM=-D0)SXLY;+YJf~jwr#^pS(!M1q_p^%$ zAT*`q;cZ$$m%r)%sg8`=mgN^uDIM+Z7hz^etb!A^)!ulKs_ZiC&5^`C##fFiGM;BC z`kN2_?Tt6Rso8ASXX{EUO0G>JzeyjGC<3U_Y%zQ9d+&QMx!}V0y!XBP_aCZQI$p^n zwLQ6?TB}B*e&ZY8{Er|0$C;TK+p;{*^WTW}S4iZS?0-!6Mh$}?+zxZFLKioD$w5rK&CFCYEzc^6*jw3;E$i3rR=^%Nz&KNMmF?61~k zY<&C=KJX{M`77^FX?D{^2`es{vEK(t`Bh%vBrJysh#wa~C`MVeigC@+=Z@aK|F~Vz zn@?YR#@2P4CP&4vE5g1nDB>hDtS_!l-x9pYUf4OYa4t%{FYUxsa6Ut@4w683O(Y

IBw*=L(Q9?bH1_1o1Qf+LW-do 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 literal 0 HcmV?d00001 diff --git a/ui/static/robots.txt b/ui/static/robots.txt new file mode 100644 index 0000000000..b6dd6670cb --- /dev/null +++ b/ui/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/ui/svelte.config.js b/ui/svelte.config.js new file mode 100644 index 0000000000..e6eca9a156 --- /dev/null +++ b/ui/svelte.config.js @@ -0,0 +1,21 @@ +import adapter from '@sveltejs/adapter-static'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + compilerOptions: { + // Force runes mode for the project, except for libraries. Can be removed in svelte 6. + runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true) + }, + kit: { + // SPA mode: static build with client-side routing. There is no Node + // server in production; the SQLBuild server serves the built assets. + adapter: adapter({ + fallback: 'index.html', + pages: 'build', + assets: 'build', + precompress: false + }) + } +}; + +export default config; diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 0000000000..2c2ed3c4d8 --- /dev/null +++ b/ui/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/ui/vite.config.ts b/ui/vite.config.ts new file mode 100644 index 0000000000..56f40c7228 --- /dev/null +++ b/ui/vite.config.ts @@ -0,0 +1,5 @@ +import tailwindcss from '@tailwindcss/vite'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ plugins: [tailwindcss(), sveltekit()] }); From fb97d0946a2b421e4adee41653d4ed92b9def484 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Thu, 29 Oct 2026 06:58:14 +0000 Subject: [PATCH 02/62] fix(ui): resolve the prototype's type-check errors Wire the lineage hover handlers to the current event names, declare the run gantt element refs as state and add Node types for the generated tsconfig. --- ui/package-lock.json | 59 ++++++++++++++++++++++++++-- ui/package.json | 1 + ui/src/routes/lineage/+page.svelte | 4 +- ui/src/routes/runs/[id]/+page.svelte | 15 +++---- 4 files changed, 67 insertions(+), 12 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index 596a6dcf23..4e8ee175a3 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -23,6 +23,7 @@ "@sveltejs/vite-plugin-svelte": "^7.0.0", "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.2.2", + "@types/node": "^20.19.43", "@types/prismjs": "^1.26.6", "bits-ui": "^2.18.1", "clsx": "^2.1.1", @@ -53,9 +54,9 @@ "license": "MIT" }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", "dev": true, "license": "MIT", "optional": true, @@ -444,6 +445,40 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@rolldown/binding-win32-arm64-msvc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", @@ -952,6 +987,17 @@ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "license": "MIT" }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/@types/prismjs": { "version": "1.26.6", "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", @@ -2119,6 +2165,13 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/ui/package.json b/ui/package.json index 7d6a17a08b..4d3b604f97 100644 --- a/ui/package.json +++ b/ui/package.json @@ -20,6 +20,7 @@ "@sveltejs/vite-plugin-svelte": "^7.0.0", "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.2.2", + "@types/node": "^20.19.43", "@types/prismjs": "^1.26.6", "bits-ui": "^2.18.1", "clsx": "^2.1.1", diff --git a/ui/src/routes/lineage/+page.svelte b/ui/src/routes/lineage/+page.svelte index 7c737929b9..c8f3fca849 100644 --- a/ui/src/routes/lineage/+page.svelte +++ b/ui/src/routes/lineage/+page.svelte @@ -618,8 +618,8 @@ multiSelectionKey={['Shift', 'Meta', 'Control']} selectionKey="Shift" proOptions={{ hideAttribution: true }} - onnodemouseenter={(_e, node) => (hoveredId = node.id)} - onnodemouseleave={() => (hoveredId = null)} + onnodepointerenter={({ node }) => (hoveredId = node.id)} + onnodepointerleave={() => (hoveredId = null)} > diff --git a/ui/src/routes/runs/[id]/+page.svelte b/ui/src/routes/runs/[id]/+page.svelte index a4a9f127da..4692f05766 100644 --- a/ui/src/routes/runs/[id]/+page.svelte +++ b/ui/src/routes/runs/[id]/+page.svelte @@ -53,7 +53,8 @@ // We measure a dedicated zero-content element pinned to the pane, so its size can // NEVER be influenced by the scaled timeline content (which would feed back). let viewportW = $state(900); - let measureEl: HTMLDivElement; + let measureEl = $state(); + let gutterW = $state(250); // available timeline width = viewport minus gutter minus right pad const trackBaseW = $derived(Math.max(120, viewportW - gutterW - 24)); @@ -91,7 +92,7 @@ } function resetZoom() { zoom = 1; - if (timelineEl) timelineEl.scrollLeft = 0; + if (ganttEl) ganttEl.scrollLeft = 0; } // ---- step selection + gantt-side filtering (scales to 100s of models) ---- @@ -156,7 +157,7 @@ const GUTTER_PAD = 24 + 12 + 8 + 16; let gutterDragging = $state(false); - let ganttEl: HTMLDivElement; + let ganttEl = $state(); // auto-fit the gutter to the longest step name function autoFitGutter(): number { @@ -170,17 +171,17 @@ return Math.min(GUTTER_AUTOFIT_CAP, Math.max(GUTTER_MIN, Math.ceil(widest) + GUTTER_PAD)); } - let gutterW = $state(250); onMount(() => { gutterW = autoFitGutter(); // measure a dedicated zero-content element (never the scrollable content), // so zoom can't feed back into the measurement and grow without bound. - if (measureEl) { + const el = measureEl; + if (el) { const measure = () => { - viewportW = measureEl.clientWidth; + viewportW = el.clientWidth; }; const ro = new ResizeObserver(measure); - ro.observe(measureEl); + ro.observe(el); measure(); return () => ro.disconnect(); } From d5114dae41da25f181de47fca85e50a805e0ec46 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:05:55 +0000 Subject: [PATCH 03/62] feat(ui): add a typed API client layer with a mock implementation Pages and the shell read through an ApiClient interface shaped like the server API, with changed-signal subscriptions for live refetches. A mock client provides neutral example data, a coalescing warehouse sync and per-target trigger state. --- ui/src/lib/api/capabilities.ts | 26 ++ ui/src/lib/api/client.ts | 64 ++++ ui/src/lib/api/index.ts | 25 ++ ui/src/lib/api/mock/fixtures.ts | 220 +++++++++++ ui/src/lib/api/mock/mock-client.ts | 294 +++++++++++++++ ui/src/lib/api/mock/timeline.ts | 84 +++++ ui/src/lib/api/signals.ts | 58 +++ ui/src/lib/api/types.ts | 351 ++++++++++++++++++ .../components/timeline/run-timeline.svelte | 108 +++--- ui/src/lib/format/time.ts | 50 +++ ui/src/lib/mock/timeline.ts | 81 ---- ui/src/lib/state/app.svelte.ts | 168 +++++++++ ui/src/lib/state/resource.svelte.ts | 74 ++++ ui/src/lib/status.ts | 94 +++++ .../{providers => connections}/+page.svelte | 0 .../[id]/+page.svelte | 0 16 files changed, 1567 insertions(+), 130 deletions(-) create mode 100644 ui/src/lib/api/capabilities.ts create mode 100644 ui/src/lib/api/client.ts create mode 100644 ui/src/lib/api/index.ts create mode 100644 ui/src/lib/api/mock/fixtures.ts create mode 100644 ui/src/lib/api/mock/mock-client.ts create mode 100644 ui/src/lib/api/mock/timeline.ts create mode 100644 ui/src/lib/api/signals.ts create mode 100644 ui/src/lib/api/types.ts create mode 100644 ui/src/lib/format/time.ts delete mode 100644 ui/src/lib/mock/timeline.ts create mode 100644 ui/src/lib/state/app.svelte.ts create mode 100644 ui/src/lib/state/resource.svelte.ts create mode 100644 ui/src/lib/status.ts rename ui/src/routes/{providers => connections}/+page.svelte (100%) rename ui/src/routes/{providers => connections}/[id]/+page.svelte (100%) diff --git a/ui/src/lib/api/capabilities.ts b/ui/src/lib/api/capabilities.ts new file mode 100644 index 0000000000..1eb890b733 --- /dev/null +++ b/ui/src/lib/api/capabilities.ts @@ -0,0 +1,26 @@ +import type { Capability } from './types'; + +/** + * Whether the current principal holds `capability`. + * + * Only used to hide or disable UI; the server remains the real check. + */ +export function hasCapability(capabilities: readonly Capability[] | undefined, capability: Capability): boolean { + return capabilities?.includes(capability) ?? false; +} + +/** Whether the current principal holds any of `required`. */ +export function hasAnyCapability( + capabilities: readonly Capability[] | undefined, + required: readonly Capability[] +): boolean { + return required.some((capability) => hasCapability(capabilities, capability)); +} + +export const ADMIN_CAPABILITIES: readonly Capability[] = [ + 'location.manage', + 'user.manage', + 'role.assign', + 'token.manage', + 'audit.view' +]; diff --git a/ui/src/lib/api/client.ts b/ui/src/lib/api/client.ts new file mode 100644 index 0000000000..77c88f1765 --- /dev/null +++ b/ui/src/lib/api/client.ts @@ -0,0 +1,64 @@ +import type { + Bootstrap, + Check, + Connection, + Job, + NavCounts, + Overview, + PlatformEvent, + Run, + RunListQuery, + Scope, + Timeline, + Trigger, + TriggerState, + WarehouseSync, + WarehouseSyncRequestResult +} from './types'; +import type { ChangeSubscription } from './signals'; + +/** + * Everything the UI reads from or asks of the server. + * + * Pages depend on this interface only. The mock implementation stands in until + * the server's OpenAPI client exists; that client is wrapped to implement the + * same interface. + */ +export interface ApiClient { + bootstrap(): Promise; + + navCounts(scope: Scope): Promise; + overview(scope: Scope): Promise; + + listRuns(scope: Scope, query?: RunListQuery): Promise; + getRun(scope: Scope, runId: string): Promise; + timeline(scope: Scope, window: { since: string; until: string }): Promise; + + listJobs(scope: Scope): Promise; + getJob(scope: Scope, key: string): Promise; + + listTriggers(scope: Scope): Promise; + setTriggerState(scope: Scope, name: string, state: TriggerState): Promise; + + listChecks(scope: Scope): Promise; + listConnections(scope: Scope): Promise; + + listPlatformEvents(query: { activeOnly: boolean }): Promise; + + getWarehouseSync(scope: Scope): Promise; + /** Coalescing: merges into a running sync, with at most one follow-up queued. */ + requestWarehouseSync(scope: Scope): Promise; + + /** Live "changed" signals (SSE in the server client); consumers refetch. */ + changes: ChangeSubscription; +} + +export class ApiError extends Error { + constructor( + message: string, + readonly status: number + ) { + super(message); + this.name = 'ApiError'; + } +} diff --git a/ui/src/lib/api/index.ts b/ui/src/lib/api/index.ts new file mode 100644 index 0000000000..c50701a275 --- /dev/null +++ b/ui/src/lib/api/index.ts @@ -0,0 +1,25 @@ +import type { ApiClient } from './client'; +import { MockApiClient, type MockOptions } from './mock/mock-client'; +import type { BuiltInRole } from './types'; + +export type { ApiClient } from './client'; +export { ApiError } from './client'; +export * from './types'; +export * from './signals'; +export { hasCapability, hasAnyCapability, ADMIN_CAPABILITIES } from './capabilities'; + +const ROLES: readonly BuiltInRole[] = ['viewer', 'operator', 'developer', 'admin']; +const ALERTS: readonly NonNullable[] = ['default', 'none', 'all']; + +/** + * The client the app uses. Until the server API exists this is the mock; + * `?as=` and `?alerts=none|all` on first load adjust the mock for demos. + */ +export function createClient(search: URLSearchParams): ApiClient { + const role = search.get('as'); + const alerts = search.get('alerts'); + return new MockApiClient({ + role: ROLES.find((r) => r === role), + alerts: ALERTS.find((a) => a === alerts) + }); +} diff --git a/ui/src/lib/api/mock/fixtures.ts b/ui/src/lib/api/mock/fixtures.ts new file mode 100644 index 0000000000..a3774d1869 --- /dev/null +++ b/ui/src/lib/api/mock/fixtures.ts @@ -0,0 +1,220 @@ +// Neutral example data for the mock client: a fictional waffle shop's orders, +// customers and payments. Times are relative to `now` so ages stay realistic. + +import type { + BuiltInRole, + Capability, + Change, + Check, + Connection, + Job, + Location, + NodeCounts, + PlatformEvent, + Principal, + Run, + RunKind, + RunStatus, + Snapshot, + Trigger +} from '../types'; +import { CAPABILITIES } from '../types'; + +const MIN = 60_000; + +export const SCHEDULER: Principal = { kind: 'scheduler', name: 'scheduler', displayName: 'Scheduler' }; +export const SENSOR_DAEMON: Principal = { kind: 'sensor', name: 'sensors', displayName: 'Sensor daemon' }; +export const CI_ACCOUNT: Principal = { kind: 'service_account', name: 'svc-ci', displayName: 'CI service account' }; +export const DEMO_USER: Principal = { kind: 'user', name: 'demo', displayName: 'Demo User' }; + +const OPERATE: Capability[] = ['view', 'view.sql', 'job.run', 'run.cancel', 'run.rerun', 'trigger.toggle', 'sensor.reset_cursor']; +const DEVELOP: Capability[] = [...OPERATE, 'view.evidence', 'run.rewrite_history', 'run.adhoc', 'connection.test', 'plan.preview']; + +/** Mock-only approximation of the built-in roles; the server owns the real grants. */ +export const ROLE_CAPABILITIES: Record = { + viewer: ['view'], + operator: OPERATE, + developer: DEVELOP, + admin: [...CAPABILITIES] +}; + +export const LOCATION_SHOP = '0d9c6a52-3f1e-4b7a-9c43-5b8e2f61a7d4'; +export const LOCATION_MARKETING = '7a41e0c8-62b5-4d19-8f3a-c1e95d0b2e66'; + +function counts(partial: Partial): NodeCounts { + return { success: 0, warn: 0, failed: 0, skipped: 0, running: 0, pending: 0, ...partial }; +} + +export function createFixtures(now: number) { + const ago = (minutes: number) => new Date(now - minutes * MIN).toISOString(); + const ahead = (minutes: number) => new Date(now + minutes * MIN).toISOString(); + + const snapshot = (generation: number, revision: string, minutesAgo: number, dirty = false): Snapshot => ({ + id: `snap-${revision}`, + generation, + gitRevision: revision, + gitDirty: dirty, + gitAuthor: dirty ? 'Demo User' : 'Release automation', + publishedAt: ago(minutesAgo), + sqbVersion: '0.185.0' + }); + + const idleSync = (minutesAgo: number) => ({ + state: 'idle' as const, + lastSyncedAt: ago(minutesAgo), + runningSince: null, + followUpQueued: false, + lastError: null + }); + + const locations: Location[] = [ + { + id: LOCATION_SHOP, + name: 'waffle-shop', + health: 'healthy', + lastSeenAt: ago(0.2), + targets: [ + { name: 'prod', adapter: 'snowflake', isDefault: true, snapshot: snapshot(42, '7d2e41a', 190), warehouseSync: idleSync(4) }, + { name: 'staging', adapter: 'snowflake', isDefault: false, snapshot: snapshot(57, 'b81c09e', 35), warehouseSync: idleSync(11) }, + { name: 'dev', adapter: 'duckdb', isDefault: false, snapshot: snapshot(311, 'b81c09e', 2, true), warehouseSync: idleSync(1) } + ] + }, + { + id: LOCATION_MARKETING, + name: 'marketing-analytics', + health: 'falling_back', + lastSeenAt: ago(0.5), + targets: [ + { name: 'prod', adapter: 'postgres', isDefault: true, snapshot: snapshot(18, '3e5f7aa', 1440), warehouseSync: idleSync(26) } + ] + } + ]; + + type RunSeed = { + id: string; + kind: RunKind; + status: RunStatus; + job: string | null; + trigger?: string; + by: Principal; + queued: number; + started?: number; + ended?: number; + nodes: Partial; + reason?: string; + rerunOf?: string; + }; + const runSeeds: RunSeed[] = [ + { id: 'run-9d21', kind: 'schedule', status: 'running', job: 'nightly_build', trigger: 'nightly_build', by: SCHEDULER, queued: 3, started: 2.5, nodes: { success: 4, running: 2, pending: 10 } }, + { id: 'run-5e10', kind: 'job', status: 'queued', job: 'revenue_refresh', by: DEMO_USER, queued: 1, nodes: { pending: 4 }, reason: 'Waiting for run-9d21: overlapping write set' }, + { id: 'run-6a90', kind: 'schedule', status: 'unresponsive', job: 'activity_microbatch', trigger: 'activity_microbatch', by: SCHEDULER, queued: 9, started: 8.8, nodes: { success: 2, running: 1, pending: 2 }, reason: 'No heartbeat for 95s' }, + { id: 'run-4f2a', kind: 'schedule', status: 'partial', job: 'nightly_build', trigger: 'nightly_build', by: SCHEDULER, queued: 22, started: 22, ended: 18, nodes: { success: 8, warn: 1, failed: 1, skipped: 3 }, reason: '1 node failed; 3 dependents skipped' }, + { id: 'run-91cd', kind: 'job', status: 'success', job: 'marts_only', by: DEMO_USER, queued: 97, started: 97, ended: 84, nodes: { success: 6 } }, + { id: 'run-3b22', kind: 'adhoc', status: 'success', job: null, by: DEMO_USER, queued: 120, started: 120, ended: 118, nodes: { success: 2 } }, + { id: 'run-2b70', kind: 'schedule', status: 'success', job: 'nightly_build', trigger: 'nightly_build', by: SCHEDULER, queued: 188, started: 188, ended: 176, nodes: { success: 16 } }, + { id: 'run-88e1', kind: 'sensor', status: 'success', job: 'load_sources', trigger: 'on_raw_orders_landed', by: SENSOR_DAEMON, queued: 206, started: 206, ended: 205, nodes: { success: 3 } }, + { id: 'run-1a05', kind: 'schedule', status: 'warn', job: 'activity_microbatch', trigger: 'activity_microbatch', by: SCHEDULER, queued: 248, started: 248, ended: 244, nodes: { success: 4, warn: 1 } }, + { id: 'run-77b3', kind: 'group', status: 'success', job: 'nightly_all', trigger: 'nightly_all', by: SCHEDULER, queued: 368, started: 368, ended: 340, nodes: { success: 19 } }, + { id: 'run-0c7d', kind: 'rerun', status: 'cancelled', job: 'nightly_build', by: CI_ACCOUNT, queued: 600, started: 600, ended: 597, nodes: { success: 3, skipped: 13 }, rerunOf: 'run-77b3', reason: 'Cancelled by svc-ci' }, + { id: 'run-c40e', kind: 'job', status: 'success', job: 'checks_only', by: CI_ACCOUNT, queued: 1080, started: 1080, ended: 1079, nodes: { success: 14 } }, + { id: 'run-e3f8', kind: 'schedule', status: 'presumed_failed', job: 'activity_microbatch', trigger: 'activity_microbatch', by: SCHEDULER, queued: 1500, started: 1500, ended: 1490, nodes: { success: 1, failed: 1, skipped: 3 }, reason: 'Worker lost; no heartbeat after 5m' }, + { id: 'run-a7c1', kind: 'schedule', status: 'failed', job: 'revenue_refresh', trigger: 'revenue_refresh', by: SCHEDULER, queued: 1620, started: 1620, ended: 1617, nodes: { failed: 1, skipped: 3 }, reason: 'daily_revenue failed: warehouse error' } + ]; + + const runs: Run[] = runSeeds.map((s) => ({ + id: s.id, + kind: s.kind, + status: s.status, + statusReason: s.reason ?? null, + jobKey: s.job, + parentRunId: null, + triggerName: s.trigger ?? null, + rerunOfRunId: s.rerunOf ?? null, + requestedBy: s.by, + locationId: LOCATION_SHOP, + target: 'prod', + snapshotId: 'snap-7d2e41a', + gitRevision: '7d2e41a', + queuedAt: ago(s.queued), + startedAt: s.started === undefined ? null : ago(s.started), + endedAt: s.ended === undefined ? null : ago(s.ended), + nodeCounts: counts(s.nodes) + })); + + const lastRunOf = (job: string) => { + const run = runs.find((r) => r.jobKey === job); + return run ? { id: run.id, status: run.status, startedAt: run.startedAt, endedAt: run.endedAt } : null; + }; + + const jobBase = { + overlap: 'coalesce' as const, + onFailure: 'skip_dependents' as const, + concurrencyGroups: [] as string[], + members: [] as string[] + }; + const jobs: Job[] = [ + { ...jobBase, key: 'nightly_build', kind: 'job', description: 'Full project build', select: '+', nodeCount: 16, concurrencyGroups: ['target:prod'], sourceFile: 'orchestration/jobs.py', triggerNames: ['nightly_build'], lastRun: lastRunOf('nightly_build') }, + { ...jobBase, key: 'marts_only', kind: 'job', description: 'Marts and their upstreams', select: '+marts', nodeCount: 9, sourceFile: 'orchestration/jobs.py', triggerNames: ['marts_hourly'], lastRun: lastRunOf('marts_only') }, + { ...jobBase, key: 'load_sources', kind: 'job', description: 'Ingest loaders and sources', select: 'tag:ingest', nodeCount: 3, overlap: 'queue', sourceFile: 'orchestration/jobs.py', triggerNames: ['on_raw_orders_landed', 'on_payments_freshness'], lastRun: lastRunOf('load_sources') }, + { ...jobBase, key: 'checks_only', kind: 'job', description: 'Run audits across all models', select: '* --checks-only', nodeCount: 14, sourceFile: 'orchestration/jobs.py', triggerNames: [], lastRun: lastRunOf('checks_only') }, + { ...jobBase, key: 'activity_microbatch', kind: 'job', description: 'Incremental activity models', select: '+hourly_activity_ctx', nodeCount: 5, sourceFile: 'orchestration/activity.py', triggerNames: ['activity_microbatch'], lastRun: lastRunOf('activity_microbatch') }, + { ...jobBase, key: 'revenue_refresh', kind: 'job', description: 'Daily revenue and downstream reporting', select: 'daily_revenue+', nodeCount: 4, onFailure: 'stop_group', sourceFile: 'orchestration/finance.py', triggerNames: ['revenue_refresh'], lastRun: lastRunOf('revenue_refresh') }, + { ...jobBase, key: 'nightly_all', kind: 'group', description: 'Sources, then the full build', select: null, members: ['load_sources', 'nightly_build'], nodeCount: 19, onFailure: 'skip_downstream_jobs', sourceFile: 'orchestration/jobs.py', triggerNames: ['nightly_all'], lastRun: lastRunOf('nightly_all') } + ]; + + const triggerRun = (id: string | null) => { + const run = id ? runs.find((r) => r.id === id) : undefined; + return run ? { id: run.id, status: run.status, startedAt: run.startedAt, endedAt: run.endedAt } : null; + }; + const triggers: Trigger[] = [ + { name: 'nightly_build', type: 'schedule', description: 'Nightly full build', jobKey: 'nightly_build', cadence: 'Daily at 02:00 Europe/London', cron: '0 2 * * *', timezone: 'Europe/London', defaultState: 'running', state: 'running', nextTickAt: ahead(13 * 60), lastTick: { id: 'tick-1', scheduledFor: ago(3), outcome: 'requested', runId: 'run-9d21', coveredByRunId: null, reason: null }, lastRun: triggerRun('run-9d21'), sourceFile: 'orchestration/jobs.py' }, + { name: 'nightly_all', type: 'schedule', description: 'Sources then the full build', jobKey: 'nightly_all', cadence: 'Daily at 01:00 Europe/London', cron: '0 1 * * *', timezone: 'Europe/London', defaultState: 'running', state: 'running', nextTickAt: ahead(12 * 60), lastTick: { id: 'tick-2', scheduledFor: ago(368), outcome: 'requested', runId: 'run-77b3', coveredByRunId: null, reason: null }, lastRun: triggerRun('run-77b3'), sourceFile: 'orchestration/jobs.py' }, + { name: 'activity_microbatch', type: 'schedule', description: 'Activity models every 15 minutes', jobKey: 'activity_microbatch', cadence: 'Every 15 minutes', cron: '*/15 * * * *', timezone: 'UTC', defaultState: 'running', state: 'running', nextTickAt: ago(12), lastTick: { id: 'tick-3', scheduledFor: ago(24), outcome: 'covered', runId: null, coveredByRunId: 'run-6a90', reason: 'Coalesced into the waiting run' }, lastRun: triggerRun('run-6a90'), sourceFile: 'orchestration/activity.py' }, + { name: 'revenue_refresh', type: 'schedule', description: 'Daily revenue refresh', jobKey: 'revenue_refresh', cadence: 'Daily at 03:00 UTC', cron: '0 3 * * *', timezone: 'UTC', defaultState: 'running', state: 'paused', nextTickAt: null, lastTick: { id: 'tick-4', scheduledFor: ago(1620), outcome: 'requested', runId: 'run-a7c1', coveredByRunId: null, reason: null }, lastRun: triggerRun('run-a7c1'), sourceFile: 'orchestration/finance.py' }, + { name: 'marts_hourly', type: 'schedule', description: 'Hourly marts refresh', jobKey: 'marts_only', cadence: 'Hourly at :00', cron: '0 * * * *', timezone: 'UTC', defaultState: 'paused', state: 'paused', nextTickAt: null, lastTick: null, lastRun: null, sourceFile: 'orchestration/jobs.py' }, + { name: 'on_raw_orders_landed', type: 'sensor', description: 'New raw orders landed', jobKey: 'load_sources', cadence: 'When raw__orders has new data', cron: null, timezone: null, defaultState: 'running', state: 'running', nextTickAt: null, lastTick: { id: 'tick-5', scheduledFor: ago(1), outcome: 'skipped', runId: null, coveredByRunId: null, reason: 'No new data' }, lastRun: triggerRun('run-88e1'), sourceFile: 'orchestration/sensors.py' }, + { name: 'on_payments_freshness', type: 'sensor', description: 'Payments source is stale', jobKey: 'load_sources', cadence: 'When raw__payments is stale for more than 1h', cron: null, timezone: null, defaultState: 'running', state: 'running', nextTickAt: null, lastTick: { id: 'tick-6', scheduledFor: ago(2), outcome: 'deferred', runId: null, coveredByRunId: null, reason: 'Location unavailable; retrying' }, lastRun: null, sourceFile: 'orchestration/sensors.py' } + ]; + + const checks: Check[] = [ + { name: 'not_null(order_id)', node: 'fact_orders', checkType: 'not_null', severity: 'error', runScopePhase: 'final', result: 'pass', detail: '0 rows', lastRunId: 'run-4f2a', evaluatedAt: ago(18) }, + { name: 'unique(order_id)', node: 'fact_orders', checkType: 'unique', severity: 'error', runScopePhase: 'final', result: 'pass', detail: '0 rows', lastRunId: 'run-4f2a', evaluatedAt: ago(18) }, + { name: 'relationships(customer_id → dim_customers)', node: 'fact_orders', checkType: 'relationships', severity: 'error', runScopePhase: 'final', result: 'pass', detail: '0 orphans', lastRunId: 'run-4f2a', evaluatedAt: ago(18) }, + { name: 'not_null(customer_id)', node: 'stg_customers', checkType: 'not_null', severity: 'error', runScopePhase: 'final', result: 'pass', detail: '0 rows', lastRunId: 'run-4f2a', evaluatedAt: ago(18) }, + { name: 'not_null(payment_id)', node: 'stg_payments', checkType: 'not_null', severity: 'warn', runScopePhase: 'final', result: 'warn', detail: '2 nulls', lastRunId: 'run-4f2a', evaluatedAt: ago(18) }, + { name: 'accepted_values(order_status)', node: 'stg_orders', checkType: 'accepted_values', severity: 'error', runScopePhase: 'final', result: 'pass', detail: '0 rows', lastRunId: 'run-4f2a', evaluatedAt: ago(18) }, + { name: 'expression_is_true(activity_hour IS NOT NULL)', node: 'hourly_activity_ctx', checkType: 'expression_is_true', severity: 'error', runScopePhase: 'delta_and_final', result: 'fail', detail: '7 rows · blocked promotion', lastRunId: 'run-4f2a', evaluatedAt: ago(18) }, + { name: 'unique(activity_hour)', node: 'daily_activity', checkType: 'unique', severity: 'error', runScopePhase: 'final', result: 'pass', detail: '0 rows', lastRunId: 'run-2b70', evaluatedAt: ago(176) }, + { name: 'revenue reconciles to source', node: null, checkType: 'standalone', severity: 'error', runScopePhase: 'final', result: 'pass', detail: 'within 0.1%', lastRunId: 'run-c40e', evaluatedAt: ago(1079) } + ]; + + const connections: Connection[] = [ + { id: 'snowflake_prod', name: 'snowflake · prod', kind: 'warehouse', type: 'snowflake', status: 'ok', usedBy: 16, summary: 'Production warehouse for the prod target.' }, + { id: 'duckdb_dev', name: 'duckdb · dev', kind: 'warehouse', type: 'duckdb', status: 'ok', usedBy: 16, summary: 'Local DuckDB file for the dev target.' }, + { id: 'orders_api', name: 'orders_api', kind: 'provider', type: 'rest', status: 'ok', usedBy: 1, summary: 'Source loader connection for raw__orders ingestion.' }, + { id: 's3_archive', name: 's3_archive', kind: 'provider', type: 's3', status: 'untested', usedBy: 2, summary: 'Object storage bucket for CSV exports and backups.' }, + { id: 'chat_alerts', name: 'chat_alerts', kind: 'channel', type: 'webhook', status: 'ok', usedBy: 1, summary: 'Posts run-failure alerts to the team channel.' }, + { id: 'warehouse_eu', name: 'warehouse · eu', kind: 'warehouse', type: 'bigquery', status: 'error', usedBy: 0, summary: 'Service account credentials expired.' } + ]; + + const platformEvents: PlatformEvent[] = [ + { id: 'pe-1', kind: 'location_broken', severity: 'warning', occurredAt: ago(48), active: true, locationId: LOCATION_MARKETING, subject: 'marketing-analytics', message: 'The latest image failed to start; running the last good image.' }, + { id: 'pe-2', kind: 'tick_overdue', severity: 'warning', occurredAt: ago(12), active: true, locationId: LOCATION_SHOP, subject: 'activity_microbatch', message: 'Tick is 12 minutes overdue.' }, + { id: 'pe-3', kind: 'heartbeat', severity: 'info', occurredAt: ago(1), active: false, locationId: null, subject: null, message: 'Server heartbeat.' } + ]; + const extraPlatformEvents: PlatformEvent[] = [ + { id: 'pe-4', kind: 'scheduler_no_leader', severity: 'critical', occurredAt: ago(2), active: true, locationId: null, subject: null, message: 'No scheduler holds the leader lease; ticks are not being evaluated.' }, + { id: 'pe-5', kind: 'launch_failed', severity: 'critical', occurredAt: ago(6), active: true, locationId: LOCATION_SHOP, subject: 'revenue_refresh', message: 'Three consecutive launches failed: image pull error.' } + ]; + + const changes: Change[] = [ + { id: 'ch-1', kind: 'macro', title: 'Macro cents_to_dollars updated', detail: 'Used by 3 models · stg_payments, fact_orders, daily_revenue', actor: CI_ACCOUNT, at: ago(300), href: '/catalog/stg_payments?tab=sql' }, + { id: 'ch-2', kind: 'model_added', title: 'New model customer_status', detail: 'marts · view', actor: CI_ACCOUNT, at: ago(1440), href: '/catalog/customer_status' }, + { id: 'ch-3', kind: 'column_added', title: 'Column added dim_customers.lifetime_value', detail: 'decimal(18,2)', actor: CI_ACCOUNT, at: ago(1500), href: '/catalog/dim_customers?tab=columns' }, + { id: 'ch-4', kind: 'snapshot', title: 'Snapshot 7d2e41a published to prod', detail: '3 models changed · fact_orders, stg_payments, dim_customers', actor: CI_ACCOUNT, at: ago(4320), href: '/runs/run-77b3' } + ]; + + return { locations, runs, jobs, triggers, checks, connections, platformEvents, extraPlatformEvents, changes }; +} + +export type Fixtures = ReturnType; diff --git a/ui/src/lib/api/mock/mock-client.ts b/ui/src/lib/api/mock/mock-client.ts new file mode 100644 index 0000000000..f009984545 --- /dev/null +++ b/ui/src/lib/api/mock/mock-client.ts @@ -0,0 +1,294 @@ +import type { ApiClient } from '../client'; +import { ApiError } from '../client'; +import { LocalChangeBus } from '../signals'; +import type { + Bootstrap, + BuiltInRole, + Capability, + Check, + Connection, + CurrentUser, + Job, + NavCounts, + Overview, + PlatformEvent, + Run, + RunListQuery, + Scope, + Timeline, + TimelineBar, + Trigger, + TriggerState, + WarehouseSync, + WarehouseSyncRequestResult +} from '../types'; +import { createFixtures, DEMO_USER, ROLE_CAPABILITIES, type Fixtures } from './fixtures'; +import { buildTimelineBars, TIMELINE_JOB_KEYS } from './timeline'; + +export type MockOptions = { + role?: BuiltInRole; + /** `default` shows two warnings; `none` is all clear; `all` adds critical events. */ + alerts?: 'default' | 'none' | 'all'; + /** Simulated request latency in milliseconds. */ + latencyMs?: number; + /** How long a simulated warehouse sync takes. */ + syncDurationMs?: number; +}; + +const ACTIVE_RUN = new Set(['queued', 'launched', 'running', 'unresponsive']); + +/** In-memory `ApiClient` with neutral example data. Mutations persist per page load. */ +export class MockApiClient implements ApiClient { + readonly changes = new LocalChangeBus(); + readonly #options: Required; + readonly #fixtures: Fixtures; + readonly #createdAt = Date.now(); + /** Trigger state per scope, so pausing in one target doesn't leak into another. */ + readonly #triggers = new Map(); + readonly #syncs = new Map(); + readonly #syncTimers = new Map>(); + #timelineBars: TimelineBar[] | null = null; + + constructor(options: MockOptions = {}) { + this.#options = { + role: options.role ?? 'admin', + alerts: options.alerts ?? 'default', + latencyMs: options.latencyMs ?? 120, + syncDurationMs: options.syncDurationMs ?? 8000 + }; + this.#fixtures = createFixtures(this.#createdAt); + } + + async bootstrap(): Promise { + const user: CurrentUser = { ...DEMO_USER, kind: 'user', role: this.#options.role }; + return this.#reply({ + serverVersion: '0.185.0', + authMode: 'disabled', + user, + capabilities: ROLE_CAPABILITIES[this.#options.role], + locations: this.#fixtures.locations.map((location) => ({ + ...location, + targets: location.targets.map((target) => ({ + ...target, + warehouseSync: this.#sync({ locationId: location.id, target: target.name }) + })) + })) + }); + } + + async navCounts(scope: Scope): Promise { + this.#location(scope); + const triggers = this.#triggersFor(scope); + return this.#reply({ + catalogNodes: 42, + failingChecks: this.#fixtures.checks.filter((c) => c.result !== 'pass').length, + activeRuns: this.#fixtures.runs.filter((r) => ACTIVE_RUN.has(r.status)).length, + pausedTriggers: triggers.filter((t) => t.state === 'paused').length, + activeAlerts: this.#platformEvents().filter((e) => e.active).length, + pendingApprovals: 1 + }); + } + + async overview(scope: Scope): Promise { + this.#location(scope); + const { runs, checks, changes } = this.#fixtures; + const dayAgo = this.#createdAt - 24 * 3600_000; + const recent = runs.filter((r) => r.startedAt && Date.parse(r.startedAt) >= dayAgo); + const succeeded = recent.filter((r) => r.status === 'success' || r.status === 'warn').length; + const failingChecks = checks.filter((c) => c.result === 'fail').length; + const latest = runs.find((r) => r.endedAt) ?? null; + return this.#reply({ + metrics: [ + { label: 'Models', value: '42', detail: '7 views', note: '+3 this week', tone: 'success' }, + { label: 'Runs · 24h', value: String(recent.length), detail: `${succeeded} successful`, note: `${Math.round((succeeded / Math.max(1, recent.length)) * 100)}% success` }, + { label: 'Checks passing', value: String(checks.filter((c) => c.result === 'pass').length), detail: `of ${checks.length}`, note: `${checks.length - checks.filter((c) => c.result === 'pass').length} need attention`, tone: 'warning' }, + { label: 'Stale', value: '2', detail: 'of 42', note: '4.8% of project', tone: 'stale' } + ], + attention: [ + { id: 'att-1', tone: 'error', title: 'nightly_build partially failed', detail: 'hourly_activity_ctx · expression_is_true', since: '18 minutes ago', href: '/runs/run-4f2a', action: 'View run' }, + { id: 'att-2', tone: 'stale', title: '2 models are stale', detail: 'daily_revenue · customer_status', since: 'changed since the last build', href: '/catalog', action: 'View catalog' }, + { id: 'att-3', tone: 'warning', title: 'Source freshness warning', detail: 'raw.payments is 23m behind', since: 'threshold 15 minutes', href: '/checks', action: 'View check' } + ], + currentNodes: 40, + staleNodes: 2, + failingChecks, + latestRun: latest && { id: latest.id, status: latest.status, endedAt: latest.endedAt }, + recentChanges: [...changes].sort((a, b) => b.at.localeCompare(a.at)) + }); + } + + async listRuns(scope: Scope, query: RunListQuery = {}): Promise { + this.#location(scope); + let runs = this.#fixtures.runs.map((run) => ({ ...run, locationId: scope.locationId, target: scope.target })); + if (query.jobKey) runs = runs.filter((r) => r.jobKey === query.jobKey); + if (query.statuses?.length) runs = runs.filter((r) => query.statuses!.includes(r.status)); + if (query.limit) runs = runs.slice(0, query.limit); + return this.#reply(runs); + } + + async getRun(scope: Scope, runId: string): Promise { + const runs = await this.listRuns(scope); + return runs.find((r) => r.id === runId) ?? null; + } + + async timeline(scope: Scope, window: { since: string; until: string }): Promise { + this.#location(scope); + this.#timelineBars ??= [ + ...buildTimelineBars(this.#createdAt), + ...this.#fixtures.runs + .filter((r) => r.jobKey && r.startedAt) + .map( + (r): TimelineBar => ({ + runId: r.id, + jobKey: r.jobKey!, + kind: r.kind, + status: r.status, + start: r.startedAt!, + end: r.endedAt ?? new Date(this.#createdAt).toISOString() + }) + ) + ]; + const bars = this.#timelineBars.filter((b) => b.end > window.since && b.start < window.until); + const jobKeys = [...new Set([...TIMELINE_JOB_KEYS, ...this.#fixtures.jobs.map((j) => j.key)])]; + return this.#reply({ now: new Date(this.#createdAt).toISOString(), jobKeys, bars }); + } + + async listJobs(scope: Scope): Promise { + this.#location(scope); + return this.#reply(structuredClone(this.#fixtures.jobs)); + } + + async getJob(scope: Scope, key: string): Promise { + const jobs = await this.listJobs(scope); + return jobs.find((j) => j.key === key) ?? null; + } + + async listTriggers(scope: Scope): Promise { + this.#location(scope); + return this.#reply(structuredClone(this.#triggersFor(scope))); + } + + async setTriggerState(scope: Scope, name: string, state: TriggerState): Promise { + this.#require('trigger.toggle'); + const triggers = this.#triggersFor(scope); + const trigger = triggers.find((t) => t.name === name); + if (!trigger) throw new ApiError(`Unknown trigger ${name}`, 404); + trigger.state = state; + trigger.nextTickAt = state === 'paused' || !trigger.cron ? null : new Date(Date.now() + 15 * 60_000).toISOString(); + this.changes.emit('triggers'); + return this.#reply(structuredClone(trigger)); + } + + async listChecks(scope: Scope): Promise { + this.#location(scope); + return this.#reply(structuredClone(this.#fixtures.checks)); + } + + async listConnections(scope: Scope): Promise { + this.#location(scope); + return this.#reply(structuredClone(this.#fixtures.connections)); + } + + async listPlatformEvents(query: { activeOnly: boolean }): Promise { + const events = this.#platformEvents(); + return this.#reply(query.activeOnly ? events.filter((e) => e.active) : events); + } + + async getWarehouseSync(scope: Scope): Promise { + this.#location(scope); + return this.#reply({ ...this.#sync(scope) }); + } + + async requestWarehouseSync(scope: Scope): Promise { + this.#location(scope); + const key = scopeKey(scope); + const sync = this.#sync(scope); + if (sync.state === 'running') { + const outcome = sync.followUpQueued ? 'merged' : 'follow_up_queued'; + sync.followUpQueued = true; + this.changes.emit(`warehouse_sync:${key}`); + return this.#reply({ outcome, sync: { ...sync } }); + } + this.#startSync(key, sync); + return this.#reply({ outcome: 'started', sync: { ...sync } }); + } + + #startSync(key: string, sync: WarehouseSync) { + sync.state = 'running'; + sync.runningSince = new Date().toISOString(); + sync.lastError = null; + this.changes.emit(`warehouse_sync:${key}`); + clearTimeout(this.#syncTimers.get(key)); + this.#syncTimers.set( + key, + setTimeout(() => { + sync.state = 'idle'; + sync.lastSyncedAt = new Date().toISOString(); + sync.runningSince = null; + if (sync.followUpQueued) { + sync.followUpQueued = false; + this.#startSync(key, sync); + } else { + this.changes.emit(`warehouse_sync:${key}`); + } + }, this.#options.syncDurationMs) + ); + } + + #sync(scope: Scope): WarehouseSync { + const key = scopeKey(scope); + let sync = this.#syncs.get(key); + if (!sync) { + const target = this.#location(scope).targets.find((t) => t.name === scope.target)!; + sync = { ...target.warehouseSync }; + this.#syncs.set(key, sync); + } + return sync; + } + + #triggersFor(scope: Scope): Trigger[] { + const key = scopeKey(scope); + let triggers = this.#triggers.get(key); + if (!triggers) { + triggers = structuredClone(this.#fixtures.triggers); + // Development targets start with every trigger paused. + if (scope.target === 'dev') { + for (const t of triggers) { + t.state = 'paused'; + t.nextTickAt = null; + } + } + this.#triggers.set(key, triggers); + } + return triggers; + } + + #platformEvents(): PlatformEvent[] { + const { platformEvents, extraPlatformEvents } = this.#fixtures; + if (this.#options.alerts === 'none') return platformEvents.filter((e) => !e.active); + if (this.#options.alerts === 'all') return [...extraPlatformEvents, ...platformEvents]; + return platformEvents; + } + + #location(scope: Scope) { + const location = this.#fixtures.locations.find((l) => l.id === scope.locationId); + if (!location || !location.targets.some((t) => t.name === scope.target)) { + throw new ApiError(`Unknown location or target: ${scope.locationId} / ${scope.target}`, 404); + } + return location; + } + + #require(capability: Capability) { + if (!ROLE_CAPABILITIES[this.#options.role].includes(capability)) { + throw new ApiError(`Missing permission: ${capability}`, 403); + } + } + + #reply(value: T): Promise { + return new Promise((resolve) => setTimeout(() => resolve(value), this.#options.latencyMs)); + } +} + +function scopeKey(scope: Scope): string { + return `${scope.locationId}/${scope.target}`; +} diff --git a/ui/src/lib/api/mock/timeline.ts b/ui/src/lib/api/mock/timeline.ts new file mode 100644 index 0000000000..5ad6aeda06 --- /dev/null +++ b/ui/src/lib/api/mock/timeline.ts @@ -0,0 +1,84 @@ +// Deterministic mock run history for the activity timeline. + +import type { RunKind, RunStatus, TimelineBar } from '../types'; + +const MIN = 60_000; + +type JobSpec = { + key: string; + every: number; // minutes between ticks + at: number; // minute offset within the day + dur: number; // typical duration, minutes + kind: RunKind; +}; + +const SPECS: JobSpec[] = [ + { key: 'nightly_build', every: 180, at: 0, dur: 24, kind: 'schedule' }, + { key: 'marts_only', every: 240, at: 30, dur: 14, kind: 'schedule' }, + { key: 'revenue_refresh', every: 360, at: 75, dur: 38, kind: 'schedule' }, + { key: 'customer_snapshots', every: 720, at: 150, dur: 52, kind: 'schedule' }, + { key: 'finance_daily', every: 1440, at: 360, dur: 95, kind: 'schedule' }, + { key: 'hourly_orders', every: 60, at: 10, dur: 6, kind: 'schedule' }, + { key: 'load_sources', every: 120, at: 50, dur: 9, kind: 'sensor' }, + { key: 'load_payments', every: 90, at: 20, dur: 11, kind: 'sensor' }, + { key: 'load_customers', every: 240, at: 5, dur: 18, kind: 'schedule' }, + { key: 'sync_crm', every: 360, at: 200, dur: 26, kind: 'schedule' }, + { key: 'ingest_events', every: 30, at: 3, dur: 3, kind: 'sensor' }, + { key: 'checks_only', every: 180, at: 100, dur: 7, kind: 'schedule' }, + { key: 'freshness_checks', every: 60, at: 40, dur: 2, kind: 'schedule' }, + { key: 'audit_nightly', every: 1440, at: 150, dur: 44, kind: 'schedule' } +]; + +export const TIMELINE_JOB_KEYS = SPECS.map(({ key }) => key); + +function rng(seed: number) { + return () => { + seed |= 0; + seed = (seed + 0x6d2b79f5) | 0; + let t = Math.imul(seed ^ (seed >>> 15), 1 | seed); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +function pickStatus(r: number): RunStatus { + if (r < 0.02) return 'failed'; + if (r < 0.035) return 'partial'; + if (r < 0.07) return 'warn'; + return 'success'; +} + +/** Runs from eight days before `now` plus projected ticks for the next day. */ +export function buildTimelineBars(now: number): TimelineBar[] { + const bars: TimelineBar[] = []; + const from = now - 8 * 1440 * MIN; + const to = now + 1440 * MIN; + const day0 = new Date(from); + day0.setHours(0, 0, 0, 0); + const iso = (t: number) => new Date(t).toISOString(); + SPECS.forEach((spec, jobIndex) => { + const rand = rng(jobIndex * 7919 + 17); + for (let t = day0.getTime() + spec.at * MIN; t < to; t += spec.every * MIN) { + if (t < from) continue; + const dur = spec.dur * (0.7 + rand() * 0.7) * MIN; + const id = `run-${Math.floor(rand() * 0xffff) + .toString(16) + .padStart(4, '0')}`; + if (t > now) { + bars.push({ runId: null, jobKey: spec.key, kind: spec.kind, status: 'projected', start: iso(t), end: iso(t + spec.dur * MIN) }); + continue; + } + const r = rand(); + const running = t + dur > now; + bars.push({ + runId: id, + jobKey: spec.key, + kind: spec.kind, + status: running ? 'running' : pickStatus(r), + start: iso(t), + end: iso(running ? now : t + dur) + }); + } + }); + return bars.sort((a, b) => a.start.localeCompare(b.start)); +} diff --git a/ui/src/lib/api/signals.ts b/ui/src/lib/api/signals.ts new file mode 100644 index 0000000000..9146bad7d3 --- /dev/null +++ b/ui/src/lib/api/signals.ts @@ -0,0 +1,58 @@ +// "Changed" signals: payload-free notifications that something changed on the +// server. Clients refetch through the normal endpoints when one arrives. + +export type ChangedChannel = + | 'runs' + | `run:${string}` + | 'ticks' + | 'triggers' + | 'jobs' + | 'checks' + | 'platform_events' + | `snapshot:${string}` + | `node_status:${string}` + | `warehouse_sync:${string}`; + +export type ChangedSignal = { channel: ChangedChannel; at: string }; + +export type ConnectionState = 'connecting' | 'live' | 'reconnecting' | 'offline'; + +export interface ChangeSubscription { + /** Calls `handler` for every signal; returns an unsubscribe function. */ + subscribe(handler: (signal: ChangedSignal) => void): () => void; + /** Calls `handler` with the current connection state and on every change. */ + onConnectionState(handler: (state: ConnectionState) => void): () => void; +} + +/** Whether a signal on `channel` should refresh a view listening to `topics`. */ +export function channelMatches(channel: ChangedChannel, topics: readonly string[]): boolean { + return topics.some((topic) => channel === topic || channel.startsWith(`${topic}:`)); +} + +/** In-process broadcaster used by the mock client (and by tests). */ +export class LocalChangeBus implements ChangeSubscription { + #handlers = new Set<(signal: ChangedSignal) => void>(); + #stateHandlers = new Set<(state: ConnectionState) => void>(); + #state: ConnectionState = 'live'; + + subscribe(handler: (signal: ChangedSignal) => void): () => void { + this.#handlers.add(handler); + return () => this.#handlers.delete(handler); + } + + onConnectionState(handler: (state: ConnectionState) => void): () => void { + this.#stateHandlers.add(handler); + handler(this.#state); + return () => this.#stateHandlers.delete(handler); + } + + emit(channel: ChangedChannel): void { + const signal = { channel, at: new Date().toISOString() }; + for (const handler of this.#handlers) handler(signal); + } + + setState(state: ConnectionState): void { + this.#state = state; + for (const handler of this.#stateHandlers) handler(state); + } +} diff --git a/ui/src/lib/api/types.ts b/ui/src/lib/api/types.ts new file mode 100644 index 0000000000..a4044abe6b --- /dev/null +++ b/ui/src/lib/api/types.ts @@ -0,0 +1,351 @@ +// Wire types for the SQLBuild server API. +// +// These mirror the server's resource shapes so that a client generated from the +// OpenAPI schema can replace them later. Timestamps are ISO-8601 strings and IDs +// are opaque strings. + +export type Iso = string; + +// ---- identity, roles and capabilities ---- + +export type BuiltInRole = 'viewer' | 'operator' | 'developer' | 'admin'; + +export const CAPABILITIES = [ + 'view', + 'view.sql', + 'view.evidence', + 'job.run', + 'run.cancel', + 'run.rerun', + 'run.rewrite_history', + 'trigger.toggle', + 'sensor.reset_cursor', + 'run.adhoc', + 'connection.test', + 'plan.preview', + 'location.manage', + 'user.manage', + 'role.assign', + 'token.manage', + 'audit.view' +] as const; +export type Capability = (typeof CAPABILITIES)[number]; + +export type PrincipalKind = 'user' | 'service_account' | 'scheduler' | 'sensor' | 'system'; +export type Principal = { + kind: PrincipalKind; + /** Stable handle, e.g. a user name, service account or `scheduler`. */ + name: string; + displayName: string; +}; + +export type CurrentUser = Principal & { + kind: 'user' | 'service_account'; + role: BuiltInRole; +}; + +export type AuthMode = 'oidc' | 'trusted_proxy' | 'password' | 'disabled'; + +// ---- locations, targets and snapshots ---- + +export type Snapshot = { + id: string; + /** Increases per (location, target); an older compile never replaces a newer one. */ + generation: number; + gitRevision: string; + gitDirty: boolean; + gitAuthor: string; + publishedAt: Iso; + sqbVersion: string; +}; + +export type WarehouseSyncState = 'idle' | 'running' | 'failed'; +export type WarehouseSync = { + state: WarehouseSyncState; + lastSyncedAt: Iso | null; + /** Start of the sync in progress, when `state` is `running`. */ + runningSince: Iso | null; + /** A follow-up sync is queued behind the running one (at most one). */ + followUpQueued: boolean; + lastError: string | null; +}; + +export type WarehouseSyncRequestOutcome = + /** No sync was running; a new one started. */ + | 'started' + /** A sync was running; one follow-up sync was queued behind it. */ + | 'follow_up_queued' + /** A sync was running and a follow-up was already queued; the request merged into it. */ + | 'merged'; + +export type WarehouseSyncRequestResult = { + outcome: WarehouseSyncRequestOutcome; + sync: WarehouseSync; +}; + +export type Target = { + name: string; + adapter: string; + isDefault: boolean; + snapshot: Snapshot | null; + warehouseSync: WarehouseSync; +}; + +export type LocationHealth = 'healthy' | 'degraded' | 'falling_back' | 'broken'; +export type Location = { + /** Opaque server-generated UUID; the name is a mutable label. */ + id: string; + name: string; + health: LocationHealth; + lastSeenAt: Iso | null; + targets: Target[]; +}; + +export type Scope = { locationId: string; target: string }; + +// ---- bootstrap ---- + +export type Bootstrap = { + serverVersion: string; + authMode: AuthMode; + user: CurrentUser; + capabilities: Capability[]; + locations: Location[]; +}; + +// ---- runs ---- + +export const RUN_STATUSES = [ + 'queued', + 'launched', + 'running', + 'unresponsive', + 'success', + 'warn', + 'partial', + 'failed', + 'cancelled', + 'presumed_failed' +] as const; +export type RunStatus = (typeof RUN_STATUSES)[number]; + +export type RunKind = 'job' | 'group' | 'adhoc' | 'rerun' | 'sensor' | 'schedule'; + +export type NodeCounts = { + success: number; + warn: number; + failed: number; + skipped: number; + running: number; + pending: number; +}; + +export type Run = { + id: string; + kind: RunKind; + status: RunStatus; + statusReason: string | null; + /** Job or group key; null for ad-hoc runs. */ + jobKey: string | null; + parentRunId: string | null; + triggerName: string | null; + rerunOfRunId: string | null; + requestedBy: Principal; + locationId: string; + target: string; + snapshotId: string; + gitRevision: string; + queuedAt: Iso; + startedAt: Iso | null; + endedAt: Iso | null; + nodeCounts: NodeCounts; +}; + +export type RunListQuery = { + jobKey?: string; + statuses?: RunStatus[]; + since?: Iso; + until?: Iso; + limit?: number; +}; + +// ---- jobs, groups and triggers ---- + +export type OverlapPolicy = 'coalesce' | 'queue' | 'allow'; +export type OnFailure = 'skip_dependents' | 'skip_downstream_jobs' | 'stop_group'; +export type JobKind = 'job' | 'group'; + +export type Job = { + key: string; + kind: JobKind; + description: string; + /** Selector for jobs; null for groups. */ + select: string | null; + /** Member jobs for groups. */ + members: string[]; + nodeCount: number; + overlap: OverlapPolicy; + onFailure: OnFailure; + concurrencyGroups: string[]; + sourceFile: string; + triggerNames: string[]; + lastRun: Pick | null; +}; + +export type TriggerState = 'running' | 'paused'; +export type TriggerType = 'schedule' | 'sensor'; +export type TickOutcome = 'requested' | 'skipped' | 'covered' | 'failed' | 'deferred'; + +export type Tick = { + id: string; + scheduledFor: Iso; + outcome: TickOutcome; + runId: string | null; + coveredByRunId: string | null; + reason: string | null; +}; + +export type Trigger = { + name: string; + type: TriggerType; + description: string; + /** Job or group this trigger launches. */ + jobKey: string; + /** Human-readable cadence or condition. */ + cadence: string; + cron: string | null; + timezone: string | null; + /** State declared in code. */ + defaultState: TriggerState; + /** Effective runtime state, after any override. */ + state: TriggerState; + nextTickAt: Iso | null; + lastTick: Tick | null; + lastRun: Pick | null; + sourceFile: string; +}; + +// ---- checks ---- + +export type CheckResult = 'pass' | 'warn' | 'fail'; +export type CheckSeverity = 'error' | 'warn'; +export type RunScopePhase = 'delta_and_final' | 'final'; +export type Check = { + name: string; + /** Attached node; null for standalone checks. */ + node: string | null; + checkType: string; + severity: CheckSeverity; + runScopePhase: RunScopePhase; + result: CheckResult; + detail: string; + lastRunId: string; + evaluatedAt: Iso; +}; + +// ---- connections ---- + +export type ConnectionKind = 'warehouse' | 'provider' | 'channel'; +export type ConnectionStatus = 'ok' | 'error' | 'untested'; +export type Connection = { + id: string; + name: string; + kind: ConnectionKind; + type: string; + status: ConnectionStatus; + usedBy: number; + summary: string; +}; + +// ---- platform events ---- + +export type PlatformEventKind = + | 'tick_overdue' + | 'scheduler_no_leader' + | 'location_broken' + | 'launch_failed' + | 'heartbeat'; +export type PlatformEventSeverity = 'info' | 'warning' | 'critical'; +export type PlatformEvent = { + id: string; + kind: PlatformEventKind; + severity: PlatformEventSeverity; + occurredAt: Iso; + /** Still unresolved; only active events surface in the banner. */ + active: boolean; + locationId: string | null; + subject: string | null; + message: string; +}; + +// ---- overview and change feed ---- + +export type ChangeKind = 'snapshot' | 'macro' | 'model_added' | 'column_added'; +export type Change = { + id: string; + kind: ChangeKind; + title: string; + detail: string; + actor: Principal; + at: Iso; + href: string; +}; + +export type AttentionTone = 'error' | 'warning' | 'stale'; +export type AttentionItem = { + id: string; + tone: AttentionTone; + title: string; + detail: string; + since: string; + href: string; + action: string; +}; + +export type OverviewMetric = { + label: string; + value: string; + detail: string; + note: string; + tone?: 'success' | 'warning' | 'stale'; +}; + +export type Overview = { + metrics: OverviewMetric[]; + attention: AttentionItem[]; + currentNodes: number; + staleNodes: number; + failingChecks: number; + latestRun: Pick | null; + recentChanges: Change[]; +}; + +// ---- navigation ---- + +export type NavCounts = { + catalogNodes: number; + failingChecks: number; + activeRuns: number; + pausedTriggers: number; + activeAlerts: number; + pendingApprovals: number; +}; + +// ---- timeline ---- + +export type TimelineBar = { + /** Null for projected (future) ticks. */ + runId: string | null; + jobKey: string; + kind: RunKind; + status: RunStatus | 'projected'; + start: Iso; + /** For running runs, the time the window was read; for projected ticks, start plus typical duration. */ + end: Iso; +}; + +export type Timeline = { + now: Iso; + jobKeys: string[]; + bars: TimelineBar[]; +}; diff --git a/ui/src/lib/components/timeline/run-timeline.svelte b/ui/src/lib/components/timeline/run-timeline.svelte index 8ad84b57e5..ca3fc6f4a1 100644 --- a/ui/src/lib/components/timeline/run-timeline.svelte +++ b/ui/src/lib/components/timeline/run-timeline.svelte @@ -3,13 +3,9 @@ import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'; import SearchIcon from '@lucide/svelte/icons/search'; import type { Snippet } from 'svelte'; - import { - TIMELINE_JOBS, - TIMELINE_NOW, - TIMELINE_RUNS, - type TimelineRun, - type TimelineStatus - } from '$lib/mock/timeline'; + import type { TimelineBar } from '$lib/api'; + import { createResource } from '$lib/state/resource.svelte'; + import { runKindLabel, runStatusLabel, runStatusRank, runStatusTone } from '$lib/status'; let { query = $bindable(''), @@ -46,12 +42,35 @@ let windowKey = $state(initialWindow); let pan = $state(0); // number of half-windows moved into the past + // The server's clock at load; the window is anchored to it so history and + // projected ticks line up. + let anchor = $state(Date.now()); const span = $derived(WINDOWS[windowKey].span); // keep a little future on screen so upcoming ticks are visible - const end = $derived(TIMELINE_NOW + span * 0.08 - pan * (span / 2)); + const end = $derived(anchor + span * 0.08 - pan * (span / 2)); const start = $derived(end - span); const pct = (t: number) => ((t - start) / span) * 100; - const nowPct = $derived(pct(TIMELINE_NOW)); + + const timeline = createResource( + (client, scope) => + client.timeline(scope, { since: new Date(start).toISOString(), until: new Date(end).toISOString() }), + ['runs', 'ticks'] + ); + const now = $derived(timeline.data ? Date.parse(timeline.data.now) : anchor); + $effect(() => { + if (timeline.data) anchor = Date.parse(timeline.data.now); + }); + const nowPct = $derived(pct(now)); + + type Bar = TimelineBar & { startMs: number; endMs: number; key: string }; + const bars = $derived( + (timeline.data?.bars ?? []).map((b) => ({ + ...b, + startMs: Date.parse(b.start), + endMs: Date.parse(b.end), + key: `${b.runId ?? 'tick'}-${b.jobKey}-${b.start}` + })) + ); const ticks = $derived.by(() => { const step = WINDOWS[windowKey].tick; @@ -88,51 +107,40 @@ return out; }); - const visibleRuns = $derived(TIMELINE_RUNS.filter((r) => r.end > start && r.start < end)); + const visibleRuns = $derived(bars.filter((r) => r.endMs > start && r.startMs < end)); const runsByJob = $derived.by(() => { - const m = new Map(); - for (const r of visibleRuns) m.set(r.job, [...(m.get(r.job) ?? []), r]); + const m = new Map(); + for (const r of visibleRuns) m.set(r.jobKey, [...(m.get(r.jobKey) ?? []), r]); return m; }); const counts = $derived({ running: visibleRuns.filter((r) => r.status === 'running').length, - ok: visibleRuns.filter((r) => r.status === 'ok').length, + ok: visibleRuns.filter((r) => r.status === 'success').length, warn: visibleRuns.filter((r) => r.status === 'warn').length, - fail: visibleRuns.filter((r) => r.status === 'fail').length + fail: visibleRuns.filter((r) => r.status === 'failed' || r.status === 'partial' || r.status === 'presumed_failed').length }); // jobs with problems in the window first, then alphabetical - const RANK: Partial> = { fail: 3, running: 2, warn: 1 }; const severity = (job: string) => - Math.max(0, ...(runsByJob.get(job) ?? []).map((r) => RANK[r.status] ?? 0)); + Math.max(0, ...(runsByJob.get(job) ?? []).map((r) => (r.status === 'projected' ? 0 : runStatusRank(r.status)))); const q = $derived(query.trim().toLowerCase()); const jobs = $derived( - TIMELINE_JOBS.filter((j) => !q || j.name.includes(q)).sort( - (a, b) => severity(b.name) - severity(a.name) || a.name.localeCompare(b.name) - ) + (timeline.data?.jobKeys ?? []) + .filter((j) => !q || j.includes(q)) + .sort((a, b) => severity(b) - severity(a) || a.localeCompare(b)) + .map((name) => ({ name })) ); - const tone: Record = { - ok: 'var(--sb-success)', - warn: 'var(--sb-warning)', - fail: 'var(--sb-error)', - running: 'var(--sb-running)', - scheduled: 'transparent' - }; - const label: Record = { - ok: 'Success', - warn: 'Warning', - fail: 'Failed', - running: 'Running', - scheduled: 'Scheduled' - }; + const tone = (r: Bar) => (r.status === 'projected' ? 'transparent' : runStatusTone[r.status]); + const label = (r: Bar) => (r.status === 'projected' ? 'Scheduled' : runStatusLabel[r.status]); + const barClass = (r: Bar) => (r.status === 'projected' ? 'scheduled' : r.status === 'success' ? 'ok' : r.status); let innerW = $state(1600); - let hover = $state<{ run: TimelineRun; x: number; y: number } | null>(null); + let hover = $state<{ run: Bar; x: number; y: number } | null>(null); const clock = (t: number) => new Date(t).toLocaleString('en-GB', { weekday: 'short', day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }); - function duration(r: TimelineRun) { - const m = Math.round((r.end - r.start) / MIN); + function duration(r: Bar) { + const m = Math.round((r.endMs - r.startMs) / MIN); return m < 60 ? `${m}m` : `${Math.floor(m / 60)}h ${m % 60}m`; } @@ -189,24 +197,26 @@

{job.name}
- {#each runsByJob.get(job.name) ?? [] as r (r.id + r.start)} - {@const left = Math.max(0, pct(r.start))} + {#each runsByJob.get(job.name) ?? [] as r (r.key)} + {@const left = Math.max(0, pct(r.startMs))} (hover = { run: r, x: e.clientX, y: e.clientY })} onmousemove={(e) => (hover = { run: r, x: e.clientX, y: e.clientY })} onmouseleave={() => (hover = null)} - aria-label="{r.job} {label[r.status]} {clock(r.start)}" + aria-label="{r.jobKey} {label(r)} {clock(r.startMs)}" > {/each}
{:else} -
No jobs match “{query}”.
+
+ {#if timeline.error}Couldn't load the timeline: {timeline.error}{:else if !timeline.data}Loading…{:else if query}No jobs match “{query}”.{:else}No runs in this window.{/if} +
{/each} @@ -217,12 +227,12 @@ {#if hover} {@const r = hover.run}
-
{label[r.status]}{r.job}
+
{label(r)}{r.jobKey}
- {#if r.status !== 'scheduled'}
Run
{r.id}
{/if} -
{r.status === 'scheduled' ? 'Next tick' : 'Started'}
{clock(r.start)}
-
Duration
{r.status === 'scheduled' ? `~${duration(r)} typical` : r.status === 'running' ? `${duration(r)} so far` : duration(r)}
-
Trigger
{r.trigger}
+ {#if r.runId}
Run
{r.runId}
{/if} +
{r.status === 'projected' ? 'Next tick' : 'Started'}
{clock(r.startMs)}
+
Duration
{r.status === 'projected' ? `~${duration(r)} typical` : r.status === 'running' ? `${duration(r)} so far` : duration(r)}
+
Kind
{runKindLabel[r.kind]}
{/if} diff --git a/ui/src/lib/format/time.ts b/ui/src/lib/format/time.ts new file mode 100644 index 0000000000..0534e7f9c8 --- /dev/null +++ b/ui/src/lib/format/time.ts @@ -0,0 +1,50 @@ +const SECOND = 1000; +const MINUTE = 60 * SECOND; +const HOUR = 60 * MINUTE; +const DAY = 24 * HOUR; + +/** Compact age, e.g. `just now`, `42s ago`, `4m ago`, `3h ago`, `2d ago`. */ +export function formatAgo(iso: string | null | undefined, now: number): string { + if (!iso) return '—'; + const diff = now - Date.parse(iso); + if (diff < 0) return formatIn(iso, now); + if (diff < 5 * SECOND) return 'just now'; + if (diff < MINUTE) return `${Math.floor(diff / SECOND)}s ago`; + if (diff < HOUR) return `${Math.floor(diff / MINUTE)}m ago`; + if (diff < DAY) return `${Math.floor(diff / HOUR)}h ago`; + return `${Math.floor(diff / DAY)}d ago`; +} + +/** Compact time until a future instant, e.g. `in 6m`. */ +export function formatIn(iso: string | null | undefined, now: number): string { + if (!iso) return '—'; + const diff = Date.parse(iso) - now; + if (diff <= 0) return 'now'; + if (diff < MINUTE) return `in ${Math.ceil(diff / SECOND)}s`; + if (diff < HOUR) return `in ${Math.round(diff / MINUTE)}m`; + if (diff < DAY) return `in ${Math.round(diff / HOUR)}h`; + return `in ${Math.round(diff / DAY)}d`; +} + +/** Elapsed seconds since an instant, floored at zero. */ +export function secondsSince(iso: string | null | undefined, now: number): number { + if (!iso) return 0; + return Math.max(0, Math.floor((now - Date.parse(iso)) / SECOND)); +} + +/** Duration between two instants, e.g. `1.4s`, `3m 12s`, `1h 5m`. */ +export function formatDuration(startIso: string | null, endIso: string | null, now: number): string { + if (!startIso) return '—'; + const ms = (endIso ? Date.parse(endIso) : now) - Date.parse(startIso); + if (ms < MINUTE) return `${(ms / SECOND).toFixed(ms < 10 * SECOND ? 1 : 0)}s`; + if (ms < HOUR) return `${Math.floor(ms / MINUTE)}m ${Math.floor((ms % MINUTE) / SECOND)}s`; + return `${Math.floor(ms / HOUR)}h ${Math.floor((ms % HOUR) / MINUTE)}m`; +} + +/** Local `YYYY-MM-DD HH:MM` for exact-time tooltips. */ +export function formatStamp(iso: string | null | undefined): string { + if (!iso) return '—'; + const d = new Date(iso); + const pad = (n: number) => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; +} diff --git a/ui/src/lib/mock/timeline.ts b/ui/src/lib/mock/timeline.ts deleted file mode 100644 index dfbde2d70a..0000000000 --- a/ui/src/lib/mock/timeline.ts +++ /dev/null @@ -1,81 +0,0 @@ -// Deterministic mock run history for the activity timeline. - -export type TimelineStatus = 'ok' | 'warn' | 'fail' | 'running' | 'scheduled'; -export type TimelineRun = { - id: string; - job: string; - status: TimelineStatus; - start: number; - end: number; - trigger: 'schedule' | 'sensor' | 'manual'; -}; -export type TimelineJob = { name: string }; - -const MIN = 60_000; -export const TIMELINE_NOW = new Date(2026, 4, 31, 12, 8).getTime(); - -type JobSpec = TimelineJob & { - every: number; // minutes between ticks - at: number; // minute offset within the day - dur: number; // typical duration, minutes - trigger: TimelineRun['trigger']; -}; - -const SPECS: JobSpec[] = [ - { name: 'nightly_build', every: 180, at: 0, dur: 24, trigger: 'schedule' }, - { name: 'marts_only', every: 240, at: 30, dur: 14, trigger: 'schedule' }, - { name: 'revenue_refresh', every: 360, at: 75, dur: 38, trigger: 'schedule' }, - { name: 'customer_snapshots', every: 720, at: 150, dur: 52, trigger: 'schedule' }, - { name: 'finance_daily', every: 1440, at: 360, dur: 95, trigger: 'schedule' }, - { name: 'hourly_orders', every: 60, at: 10, dur: 6, trigger: 'schedule' }, - { name: 'load_sources', every: 120, at: 50, dur: 9, trigger: 'sensor' }, - { name: 'load_payments', every: 90, at: 20, dur: 11, trigger: 'sensor' }, - { name: 'load_customers', every: 240, at: 5, dur: 18, trigger: 'schedule' }, - { name: 'sync_crm', every: 360, at: 200, dur: 26, trigger: 'schedule' }, - { name: 'ingest_events', every: 30, at: 3, dur: 3, trigger: 'sensor' }, - { name: 'checks_only', every: 180, at: 100, dur: 7, trigger: 'schedule' }, - { name: 'freshness_checks', every: 60, at: 40, dur: 2, trigger: 'schedule' }, - { name: 'audit_nightly', every: 1440, at: 150, dur: 44, trigger: 'schedule' }, -]; - -export const TIMELINE_JOBS: TimelineJob[] = SPECS.map(({ name }) => ({ name })); - -function rng(seed: number) { - return () => { - seed |= 0; - seed = (seed + 0x6d2b79f5) | 0; - let t = Math.imul(seed ^ (seed >>> 15), 1 | seed); - t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; - return ((t ^ (t >>> 14)) >>> 0) / 4294967296; - }; -} - -function build(): TimelineRun[] { - const runs: TimelineRun[] = []; - const from = TIMELINE_NOW - 8 * 1440 * MIN; - const to = TIMELINE_NOW + 1440 * MIN; - const day0 = new Date(from); - day0.setHours(0, 0, 0, 0); - SPECS.forEach((spec, jobIndex) => { - const rand = rng(jobIndex * 7919 + 17); - for (let t = day0.getTime() + spec.at * MIN; t < to; t += spec.every * MIN) { - if (t < from) continue; - const dur = spec.dur * (0.7 + rand() * 0.7) * MIN; - const id = `run-${Math.floor(rand() * 0xffff).toString(16).padStart(4, '0')}`; - if (t > TIMELINE_NOW) { - runs.push({ id, job: spec.name, status: 'scheduled', start: t, end: t + spec.dur * MIN, trigger: spec.trigger }); - continue; - } - const r = rand(); - const status: TimelineStatus = - t + dur > TIMELINE_NOW ? 'running' : r < 0.035 ? 'fail' : r < 0.07 ? 'warn' : 'ok'; - runs.push({ id, job: spec.name, status, start: t, end: status === 'running' ? TIMELINE_NOW : t + dur, trigger: spec.trigger }); - } - }); - // Anchors that match the rest of the prototype's mock data. - runs.push({ id: 'run-4f2a', job: 'nightly_build', status: 'fail', start: TIMELINE_NOW - 22 * MIN, end: TIMELINE_NOW - 18 * MIN, trigger: 'schedule' }); - runs.push({ id: 'run-91cd', job: 'marts_only', status: 'ok', start: TIMELINE_NOW - 97 * MIN, end: TIMELINE_NOW - 84 * MIN, trigger: 'manual' }); - return runs.sort((a, b) => a.start - b.start); -} - -export const TIMELINE_RUNS = build(); diff --git a/ui/src/lib/state/app.svelte.ts b/ui/src/lib/state/app.svelte.ts new file mode 100644 index 0000000000..0b8ea00242 --- /dev/null +++ b/ui/src/lib/state/app.svelte.ts @@ -0,0 +1,168 @@ +import { browser } from '$app/environment'; +import { + channelMatches, + createClient, + hasCapability, + type ApiClient, + type Bootstrap, + type Capability, + type ConnectionState, + type NavCounts, + type PlatformEvent, + type Scope, + type WarehouseSync, + type WarehouseSyncRequestOutcome +} from '$lib/api'; + +const SCOPE_KEY = 'sb-scope'; + +function message(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +/** + * App-wide state: the bootstrap, the selected location and target, and the + * shell's live data (navigation counts, platform events, warehouse sync). + */ +class AppState { + client: ApiClient; + bootstrap = $state(null); + bootError = $state(null); + locationId = $state(''); + targetName = $state(''); + /** Wall clock, ticking every second, for ages and elapsed times. */ + now = $state(Date.now()); + connection = $state('connecting'); + navCounts = $state(null); + platformEvents = $state([]); + warehouseSync = $state(null); + lastSyncRequest = $state(null); + + readonly scope = $derived( + this.locationId && this.targetName ? { locationId: this.locationId, target: this.targetName } : null + ); + readonly location = $derived(this.bootstrap?.locations.find((l) => l.id === this.locationId) ?? null); + readonly target = $derived(this.location?.targets.find((t) => t.name === this.targetName) ?? null); + readonly user = $derived(this.bootstrap?.user ?? null); + readonly capabilities = $derived(this.bootstrap?.capabilities ?? []); + + #started = false; + + constructor() { + this.client = createClient(new URLSearchParams(browser ? window.location.search : '')); + } + + can(capability: Capability): boolean { + return hasCapability(this.capabilities, capability); + } + + async start(): Promise { + if (this.#started) return; + this.#started = true; + setInterval(() => (this.now = Date.now()), 1000); + this.client.changes.onConnectionState((state) => (this.connection = state)); + this.client.changes.subscribe((signal) => { + if (channelMatches(signal.channel, ['platform_events'])) void this.#loadPlatformEvents(); + if (channelMatches(signal.channel, ['warehouse_sync'])) void this.#loadWarehouseSync(); + if (channelMatches(signal.channel, ['runs', 'triggers', 'checks'])) void this.#loadNavCounts(); + }); + try { + this.bootstrap = await this.client.bootstrap(); + } catch (error) { + this.bootError = message(error); + return; + } + this.#restoreScope(); + void this.#loadPlatformEvents(); + this.#loadScoped(); + } + + selectLocation(locationId: string): void { + const location = this.bootstrap?.locations.find((l) => l.id === locationId); + if (!location) return; + this.locationId = location.id; + this.targetName = (location.targets.find((t) => t.isDefault) ?? location.targets[0]).name; + this.#scopeChanged(); + } + + selectTarget(targetName: string): void { + if (!this.location?.targets.some((t) => t.name === targetName)) return; + this.targetName = targetName; + this.#scopeChanged(); + } + + async requestWarehouseSync(): Promise { + const scope = this.scope; + if (!scope) return; + const result = await this.client.requestWarehouseSync(scope); + this.lastSyncRequest = result.outcome; + this.warehouseSync = result.sync; + } + + #scopeChanged() { + if (browser) localStorage.setItem(SCOPE_KEY, JSON.stringify(this.scope)); + this.navCounts = null; + this.warehouseSync = null; + this.lastSyncRequest = null; + this.#loadScoped(); + } + + #restoreScope() { + const locations = this.bootstrap?.locations ?? []; + const search = browser ? new URLSearchParams(window.location.search) : new URLSearchParams(); + let saved: Partial = {}; + try { + saved = browser ? JSON.parse(localStorage.getItem(SCOPE_KEY) ?? '{}') ?? {} : {}; + } catch { + saved = {}; + } + const wantedLocation = search.get('location') ?? saved.locationId; + const location = + locations.find((l) => l.id === wantedLocation || l.name === wantedLocation) ?? locations[0]; + if (!location) return; + const wantedTarget = search.get('target') ?? saved.target; + const target = + location.targets.find((t) => t.name === wantedTarget) ?? + location.targets.find((t) => t.isDefault) ?? + location.targets[0]; + this.locationId = location.id; + this.targetName = target.name; + } + + #loadScoped() { + void this.#loadNavCounts(); + void this.#loadWarehouseSync(); + } + + async #loadNavCounts() { + const scope = this.scope; + if (!scope) return; + try { + const counts = await this.client.navCounts(scope); + if (this.scope === scope) this.navCounts = counts; + } catch { + // Counts are decoration; the pages report their own errors. + } + } + + async #loadWarehouseSync() { + const scope = this.scope; + if (!scope) return; + try { + const sync = await this.client.getWarehouseSync(scope); + if (this.scope === scope) this.warehouseSync = sync; + } catch { + this.warehouseSync = null; + } + } + + async #loadPlatformEvents() { + try { + this.platformEvents = await this.client.listPlatformEvents({ activeOnly: true }); + } catch { + this.platformEvents = []; + } + } +} + +export const app = new AppState(); diff --git a/ui/src/lib/state/resource.svelte.ts b/ui/src/lib/state/resource.svelte.ts new file mode 100644 index 0000000000..79361e5e0f --- /dev/null +++ b/ui/src/lib/state/resource.svelte.ts @@ -0,0 +1,74 @@ +import type { ApiClient, Scope } from '$lib/api'; +import { channelMatches } from '$lib/api'; +import { app } from './app.svelte'; + +export type Resource = { + readonly data: T | undefined; + readonly error: string | null; + readonly loading: boolean; + refresh(): void; +}; + +/** + * Loads data for the selected location and target, reloading when the scope + * changes and when a "changed" signal arrives on one of `topics`. + * + * Must be called during component initialisation. Reactive values read + * synchronously inside `load` (such as route params) are also dependencies. + */ +export function createResource( + load: (client: ApiClient, scope: Scope) => Promise, + topics: readonly string[] = [] +): Resource { + let data = $state(undefined); + let error = $state(null); + let loading = $state(true); + let latest = 0; + + function run(scope: Scope) { + const request = ++latest; + loading = true; + load(app.client, scope).then( + (value) => { + if (request !== latest) return; + data = value; + error = null; + loading = false; + }, + (reason: unknown) => { + if (request !== latest) return; + error = reason instanceof Error ? reason.message : String(reason); + loading = false; + } + ); + } + + function refresh() { + if (app.scope) run(app.scope); + } + + $effect(() => { + const scope = app.scope; + if (scope) run(scope); + }); + + $effect(() => { + if (!topics.length) return; + return app.client.changes.subscribe((signal) => { + if (channelMatches(signal.channel, topics)) refresh(); + }); + }); + + return { + get data() { + return data; + }, + get error() { + return error; + }, + get loading() { + return loading; + }, + refresh + }; +} diff --git a/ui/src/lib/status.ts b/ui/src/lib/status.ts new file mode 100644 index 0000000000..732fb1ef9f --- /dev/null +++ b/ui/src/lib/status.ts @@ -0,0 +1,94 @@ +import type { CheckResult, NodeCounts, RunKind, RunStatus, TickOutcome, TriggerState } from '$lib/api/types'; + +export const runStatusTone: Record = { + queued: 'var(--sb-text-faint)', + launched: 'var(--sb-running)', + running: 'var(--sb-running)', + unresponsive: 'var(--sb-warning)', + success: 'var(--sb-success)', + warn: 'var(--sb-warning)', + partial: 'color-mix(in srgb, var(--sb-error) 55%, var(--sb-warning))', + failed: 'var(--sb-error)', + cancelled: 'var(--sb-text-faint)', + presumed_failed: 'var(--sb-error)' +}; + +export const runStatusLabel: Record = { + queued: 'Queued', + launched: 'Launched', + running: 'Running', + unresponsive: 'Unresponsive', + success: 'Success', + warn: 'Warning', + partial: 'Partial', + failed: 'Failed', + cancelled: 'Cancelled', + presumed_failed: 'Presumed failed' +}; + +const ACTIVE: ReadonlySet = new Set(['queued', 'launched', 'running', 'unresponsive']); +const PROBLEM: ReadonlySet = new Set(['failed', 'partial', 'presumed_failed']); + +/** Not yet terminal: waiting, starting or executing. */ +export function isActiveRun(status: RunStatus): boolean { + return ACTIVE.has(status); +} + +/** Terminal with at least one failure. */ +export function isProblemRun(status: RunStatus): boolean { + return PROBLEM.has(status); +} + +/** Sort weight: problems and live runs first. */ +export function runStatusRank(status: RunStatus): number { + if (PROBLEM.has(status)) return 3; + if (ACTIVE.has(status)) return 2; + if (status === 'warn') return 1; + return 0; +} + +export const tickOutcomeLabel: Record = { + requested: 'Run requested', + skipped: 'Skipped', + covered: 'Covered by a waiting run', + failed: 'Tick failed', + deferred: 'Deferred' +}; + +export const triggerStateLabel: Record = { + running: 'Running', + paused: 'Paused' +}; + +export const checkResultTone: Record = { + pass: 'var(--sb-success)', + fail: 'var(--sb-error)', + warn: 'var(--sb-warning)' +}; + +export const checkResultLabel: Record = { + pass: 'Passed', + fail: 'Failed', + warn: 'Warning' +}; + +/** e.g. `8 ok · 1 warn · 1 failed · 3 skipped`, or `4 running · 10 pending`. */ +export function formatNodeCounts(counts: NodeCounts): string { + const parts: string[] = []; + if (counts.running) parts.push(`${counts.running} running`); + if (counts.success) parts.push(`${counts.success} ok`); + if (counts.warn) parts.push(`${counts.warn} warn`); + if (counts.failed) parts.push(`${counts.failed} failed`); + if (counts.skipped) parts.push(`${counts.skipped} skipped`); + if (counts.pending) parts.push(`${counts.pending} pending`); + return parts.join(' · ') || '—'; +} + +export const runKindLabel: Record = { + job: 'Job', + group: 'Group', + adhoc: 'Ad hoc', + rerun: 'Rerun', + sensor: 'Sensor', + schedule: 'Schedule' +}; diff --git a/ui/src/routes/providers/+page.svelte b/ui/src/routes/connections/+page.svelte similarity index 100% rename from ui/src/routes/providers/+page.svelte rename to ui/src/routes/connections/+page.svelte diff --git a/ui/src/routes/providers/[id]/+page.svelte b/ui/src/routes/connections/[id]/+page.svelte similarity index 100% rename from ui/src/routes/providers/[id]/+page.svelte rename to ui/src/routes/connections/[id]/+page.svelte From 9d14efb85870eefb161353653107b31b27c26c77 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:06:04 +0000 Subject: [PATCH 04/62] feat(ui): add the orchestration shell with location and target switchers The sidebar follows the new navigation with capability-gated admin items and live counts. The top bar shows location / target, connection state, snapshot age and warehouse sync freshness with a coalescing refresh, above a platform-alert banner. Pages without content yet render a not-built state. --- ui/src/lib/components/app-sidebar.svelte | 161 +++++++++++------- ui/src/lib/components/app-topbar.svelte | 47 +++-- ui/src/lib/components/not-built.svelte | 38 +++++ .../shell/platform-alert-banner.svelte | 103 +++++++++++ .../components/shell/scope-switcher.svelte | 150 ++++++++++++++++ .../shell/warehouse-sync-status.svelte | 84 +++++++++ ui/src/lib/run/run-panel.svelte | 2 +- ui/src/routes/+layout.svelte | 35 +++- ui/src/routes/admin/audit/+page.svelte | 5 + ui/src/routes/admin/roles/+page.svelte | 5 + ui/src/routes/admin/users/+page.svelte | 5 + ui/src/routes/alerts/+page.svelte | 5 + ui/src/routes/changes/+page.svelte | 5 + ui/src/routes/connections/[id]/+page.svelte | 4 +- ui/src/routes/locations/+page.svelte | 5 + ui/src/routes/maintenance/+page.svelte | 5 + ui/src/routes/settings/+page.svelte | 5 + 17 files changed, 584 insertions(+), 80 deletions(-) create mode 100644 ui/src/lib/components/not-built.svelte create mode 100644 ui/src/lib/components/shell/platform-alert-banner.svelte create mode 100644 ui/src/lib/components/shell/scope-switcher.svelte create mode 100644 ui/src/lib/components/shell/warehouse-sync-status.svelte create mode 100644 ui/src/routes/admin/audit/+page.svelte create mode 100644 ui/src/routes/admin/roles/+page.svelte create mode 100644 ui/src/routes/admin/users/+page.svelte create mode 100644 ui/src/routes/alerts/+page.svelte create mode 100644 ui/src/routes/changes/+page.svelte create mode 100644 ui/src/routes/locations/+page.svelte create mode 100644 ui/src/routes/maintenance/+page.svelte create mode 100644 ui/src/routes/settings/+page.svelte diff --git a/ui/src/lib/components/app-sidebar.svelte b/ui/src/lib/components/app-sidebar.svelte index 2240d637c7..a7324b4f6f 100644 --- a/ui/src/lib/components/app-sidebar.svelte +++ b/ui/src/lib/components/app-sidebar.svelte @@ -1,47 +1,101 @@ @@ -49,63 +103,49 @@
SQLBuild SQLBuild - v0.87 + {#if app.bootstrap} + v{app.bootstrap.serverVersion} + {/if}
-
- W - waffle-shopSnowflake · production - ⌄ -
+ -