diff --git a/ui/consumer/.nvmrc b/ui/consumer/.nvmrc new file mode 100644 index 00000000..18c92ea9 --- /dev/null +++ b/ui/consumer/.nvmrc @@ -0,0 +1 @@ +v24 \ No newline at end of file diff --git a/ui/provider/.nvmrc b/ui/provider/.nvmrc new file mode 100644 index 00000000..18c92ea9 --- /dev/null +++ b/ui/provider/.nvmrc @@ -0,0 +1 @@ +v24 \ No newline at end of file diff --git a/ui/provider/README.md b/ui/provider/README.md index be19bc1a..034b6c86 100644 --- a/ui/provider/README.md +++ b/ui/provider/README.md @@ -37,9 +37,9 @@ Built for a staff member fielding "why isn't my workload starting" / "what's wrong with this workload" from a customer, not for general browsing — Overview and Instances surface raw conditions (type/status/reason/message), placements, network assignments, and scheduling gates, not just a coarse -health enum. Events/Logs/Metrics are honest "Coming Soon" placeholders (no -data source wired up for any of the three yet); YAML dumps the raw resource -(minus `metadata.managedFields`) as an escape hatch. All data is read client-side, polled via +health enum. The Logs tab mounts the empty datum-ui explorer until a Loki +query is wired; Events/Metrics stay honest "Coming Soon" placeholders. YAML +dumps the raw resource (minus `metadata.managedFields`) as an escape hatch. All data is read client-side, polled via `refetchInterval` (`src/lib/api.ts`) through staff-portal's own same-origin proxy — no new credential, no plugin-owned backend. diff --git a/ui/provider/bun.lock b/ui/provider/bun.lock index cbdae112..ea974f04 100644 --- a/ui/provider/bun.lock +++ b/ui/provider/bun.lock @@ -5,15 +5,18 @@ "": { "name": "@datum-cloud/workloads-provider-plugin", "dependencies": { - "@datum-cloud/datum-ui": "^1.7.1", + "@datum-cloud/datum-ui": "^2.3.0", "@monaco-editor/react": "^4.7.0", "@tanstack/react-query": "5.101.4", + "@tanstack/react-virtual": "^3", "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", "js-yaml": "^5.2.2", "lucide-react": "^1.21.0", "monaco-editor": "^0.55.0", + "motion": "^12", "react": "19.2.8", + "react-day-picker": "^10", "react-dom": "19.2.8", "react-router": "7.18.0", "sonner": "^2.0.7", @@ -87,7 +90,9 @@ "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="], - "@datum-cloud/datum-ui": ["@datum-cloud/datum-ui@1.7.1", "", { "dependencies": { "@radix-ui/react-avatar": "^1.2.6", "@radix-ui/react-checkbox": "^1.3.11", "@radix-ui/react-collapsible": "^1.1.20", "@radix-ui/react-dialog": "^1.1.23", "@radix-ui/react-dropdown-menu": "^2.1.24", "@radix-ui/react-hover-card": "^1.1.23", "@radix-ui/react-label": "^2.1.15", "@radix-ui/react-popover": "^1.1.23", "@radix-ui/react-radio-group": "^1.4.7", "@radix-ui/react-select": "^2.3.7", "@radix-ui/react-separator": "^1.1.15", "@radix-ui/react-slot": "^1.3.3", "@radix-ui/react-switch": "^1.3.7", "@radix-ui/react-tabs": "^1.1.21", "@radix-ui/react-tooltip": "^1.2.16", "@radix-ui/react-visually-hidden": "^1.2.11", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "isomorphic-dompurify": "^3.21.0", "tailwind-merge": "^3.6.0", "tw-animate-css": "^1.4.0" }, "peerDependencies": { "@conform-to/react": ">=1.19.4 <2", "@conform-to/zod": ">=1.19.4 <2", "@dnd-kit/core": ">=6.3.1 <7", "@dnd-kit/sortable": ">=10 <11", "@hookform/resolvers": ">=5.4.0 <6", "@monaco-editor/react": ">=4.7.0 <5", "@stepperize/react": ">=7 <8", "@streamdown/code": ">=1 <2", "@tanstack/react-table": ">=8.21.3 <9", "@tanstack/react-virtual": ">=3.14.3 <4", "@tiptap/extension-character-count": ">=3.27.1 <4", "@tiptap/extension-link": ">=3.27.1 <4", "@tiptap/extension-placeholder": ">=3.27.1 <4", "@tiptap/extension-underline": ">=3.27.1 <4", "@tiptap/react": ">=3.27.1 <4", "@tiptap/starter-kit": ">=3.27.1 <4", "ai": ">=7 <8", "date-fns": ">=4.1.0 <5", "date-fns-tz": ">=3 <4", "js-yaml": ">=5.2.2 <6", "leaflet": ">=1.9 <2", "leaflet-draw": ">=1 <2", "leaflet.fullscreen": ">=5 <6", "leaflet.markercluster": ">=1.5.3 <2", "lucide-react": ">=1 <2", "monaco-editor": ">=0.44.0 <1", "motion": ">=12 <13", "nprogress": ">=0.2 <1", "nuqs": ">=2 <3", "react": ">=19.2.7 <20", "react-day-picker": ">=10 <11", "react-dom": ">=19.2.7 <20", "react-dropzone": ">=15 <21", "react-hook-form": ">=7.80.0 <8", "react-leaflet": ">=5 <6", "react-leaflet-markercluster": ">=5.0.0-rc.0 <6", "react-number-format": ">=5.4.5 <6", "recharts": ">=3 <4", "sonner": ">=2.0.7 <3", "streamdown": ">=2 <3", "zod": ">=4 <5" }, "optionalPeers": ["@conform-to/react", "@conform-to/zod", "@dnd-kit/core", "@dnd-kit/sortable", "@hookform/resolvers", "@monaco-editor/react", "@stepperize/react", "@streamdown/code", "@tanstack/react-table", "@tanstack/react-virtual", "@tiptap/extension-character-count", "@tiptap/extension-link", "@tiptap/extension-placeholder", "@tiptap/extension-underline", "@tiptap/react", "@tiptap/starter-kit", "ai", "date-fns", "date-fns-tz", "js-yaml", "leaflet", "leaflet-draw", "leaflet.fullscreen", "leaflet.markercluster", "monaco-editor", "motion", "nprogress", "nuqs", "react-day-picker", "react-dropzone", "react-hook-form", "react-leaflet", "react-leaflet-markercluster", "react-number-format", "recharts", "sonner", "streamdown", "zod"] }, "sha512-D2e1tIc+6qzrqumRtwbmvfzP56b6o71XfNPykx77/i1gqbrmpm0wK4m9rvjNqzHY8B00VHfakvmtmmGd3Aua7g=="], + "@date-fns/tz": ["@date-fns/tz@1.5.0", "", {}, "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg=="], + + "@datum-cloud/datum-ui": ["@datum-cloud/datum-ui@2.3.0", "", { "dependencies": { "@radix-ui/react-avatar": "^1.2.6", "@radix-ui/react-checkbox": "^1.3.11", "@radix-ui/react-collapsible": "^1.1.20", "@radix-ui/react-dialog": "^1.1.23", "@radix-ui/react-dropdown-menu": "^2.1.24", "@radix-ui/react-hover-card": "^1.1.23", "@radix-ui/react-label": "^2.1.15", "@radix-ui/react-popover": "^1.1.23", "@radix-ui/react-radio-group": "^1.4.7", "@radix-ui/react-select": "^2.3.7", "@radix-ui/react-separator": "^1.1.15", "@radix-ui/react-slot": "^1.3.3", "@radix-ui/react-switch": "^1.3.7", "@radix-ui/react-tabs": "^1.1.21", "@radix-ui/react-tooltip": "^1.2.16", "@radix-ui/react-visually-hidden": "^1.2.11", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "isomorphic-dompurify": "^3.22.0", "tailwind-merge": "^3.6.0", "tw-animate-css": "^1.4.0" }, "peerDependencies": { "@conform-to/react": ">=1.19.4 <2", "@conform-to/zod": ">=1.19.4 <2", "@dnd-kit/core": ">=6.3.1 <7", "@dnd-kit/sortable": ">=10 <11", "@hookform/resolvers": ">=5.4.0 <6", "@monaco-editor/react": ">=4.7.0 <5", "@stepperize/react": ">=7 <8", "@streamdown/code": ">=1 <2", "@tanstack/react-table": ">=9 <10", "@tanstack/react-virtual": ">=3.14.3 <4", "@tiptap/extension-character-count": ">=3.27.1 <4", "@tiptap/extension-link": ">=3.27.1 <4", "@tiptap/extension-placeholder": ">=3.27.1 <4", "@tiptap/extension-underline": ">=3.27.1 <4", "@tiptap/react": ">=3.27.1 <4", "@tiptap/starter-kit": ">=3.27.1 <4", "ai": ">=7 <8", "date-fns": ">=4.1.0 <5", "date-fns-tz": ">=3 <4", "js-yaml": ">=5.2.2 <6", "leaflet": ">=1.9 <2", "leaflet-draw": ">=1 <2", "leaflet.fullscreen": ">=5 <6", "leaflet.markercluster": ">=1.5.3 <2", "lucide-react": ">=1 <2", "monaco-editor": ">=0.44.0 <1", "motion": ">=13 <14", "nprogress": ">=0.2 <1", "nuqs": ">=2 <3", "react": ">=19.2.7 <20", "react-day-picker": ">=10 <11", "react-dom": ">=19.2.7 <20", "react-dropzone": ">=15 <21", "react-hook-form": ">=7.80.0 <8", "react-leaflet": ">=5 <6", "react-leaflet-markercluster": ">=5.0.0-rc.0 <6", "react-number-format": ">=5.4.5 <6", "recharts": ">=3 <4", "sonner": ">=2.0.7 <3", "streamdown": ">=2 <3", "zod": ">=4 <5" }, "optionalPeers": ["@conform-to/react", "@conform-to/zod", "@dnd-kit/core", "@dnd-kit/sortable", "@hookform/resolvers", "@monaco-editor/react", "@stepperize/react", "@streamdown/code", "@tanstack/react-table", "@tanstack/react-virtual", "@tiptap/extension-character-count", "@tiptap/extension-link", "@tiptap/extension-placeholder", "@tiptap/extension-underline", "@tiptap/react", "@tiptap/starter-kit", "ai", "date-fns", "date-fns-tz", "js-yaml", "leaflet", "leaflet-draw", "leaflet.fullscreen", "leaflet.markercluster", "monaco-editor", "motion", "nprogress", "nuqs", "react-day-picker", "react-dropzone", "react-hook-form", "react-leaflet", "react-leaflet-markercluster", "react-number-format", "recharts", "sonner", "streamdown", "zod"] }, "sha512-d8cLCazU1TQBffHRdla9foatlAZUJeUeQjyeOz9g3W3HmIb32eM3LsxgvG67ksBCBp9tOoJB6zIuvC/MMXhbig=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], @@ -323,6 +328,10 @@ "@tanstack/react-query": ["@tanstack/react-query@5.101.4", "", { "dependencies": { "@tanstack/query-core": "5.101.4" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA=="], + "@tanstack/react-virtual": ["@tanstack/react-virtual@3.14.10", "", { "dependencies": { "@tanstack/virtual-core": "3.17.8" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-SRyoUbdFMRHuYXMijV5H4ZarQWpXkj3iANq8OFre+pybeVap8ZJjZ3Nz9bVjx4d8PfobVUQUdKyyyHYk3E+djw=="], + + "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.8", "", {}, "sha512-BfEvehNpOT75r5Ksc5xW6NZuXujTfb7nlSEyVu4XHG3gdxNg1KqXruWbDewXOUaUYIo4oRbSfkjIajz4MAT8tA=="], + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], @@ -395,6 +404,8 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "framer-motion": ["framer-motion@12.43.0", "", { "dependencies": { "motion-dom": "^12.43.0", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], @@ -429,6 +440,12 @@ "monaco-editor": ["monaco-editor@0.55.1", "", { "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" } }, "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A=="], + "motion": ["motion@12.43.0", "", { "dependencies": { "framer-motion": "^12.43.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-BQgQbSa9Hn3/mtbib0MK53y6JSANa+YKUKlaYnWzAVDH424RYQ5LVpV3pNiWH00BA2z4ojsSdMzqT7g2FQwjuQ=="], + + "motion-dom": ["motion-dom@12.43.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag=="], + + "motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.17", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g=="], @@ -447,6 +464,8 @@ "react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="], + "react-day-picker": ["react-day-picker@10.0.1", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0" }, "peerDependencies": { "@types/react": ">=16.8.0", "react": ">=16.8.0" }, "optionalPeers": ["@types/react"] }, "sha512-eNh6BlwcYInWaJtRv18mXQ06Ys/H6rdTZAnTaSdOYJuTpwP1JMCHNd1FDRadA+gbeinq+psdULN5Xnowy9mV8w=="], + "react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="], "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], diff --git a/ui/provider/package.json b/ui/provider/package.json index 69962de0..2f1300c0 100644 --- a/ui/provider/package.json +++ b/ui/provider/package.json @@ -11,12 +11,15 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@datum-cloud/datum-ui": "^1.7.1", + "@datum-cloud/datum-ui": "^2.3.0", "@monaco-editor/react": "^4.7.0", "@tanstack/react-query": "5.101.4", + "@tanstack/react-virtual": "^3", "lucide-react": "^1.21.0", "monaco-editor": "^0.55.0", + "motion": "^12", "react": "19.2.8", + "react-day-picker": "^10", "react-dom": "19.2.8", "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", diff --git a/ui/provider/src/components/workload-logs.tsx b/ui/provider/src/components/workload-logs.tsx new file mode 100644 index 00000000..b6e2b72d --- /dev/null +++ b/ui/provider/src/components/workload-logs.tsx @@ -0,0 +1,55 @@ +/** + * Workload log explorer built on `@datum-cloud/datum-ui/logs`. + * + * Full explorer for the Workload Logs tab. No log backend yet — entries stay + * empty until a Loki query is wired. + */ +import { + lastThirtyMinutes, + Logs, + type LogFilters, + type LogTimeRange, +} from '@datum-cloud/datum-ui/logs'; +import { cn } from '@datum-cloud/datum-ui/utils'; +import { useCallback, useState } from 'react'; + +const COLUMNS = ['time', 'severity', 'message'] as const; + +/** Full log explorer for the Workload Logs tab. */ +export function WorkloadLogsExplorer({ className }: { className?: string }) { + const [filters, setFilters] = useState({}); + const [search, setSearch] = useState(''); + const [live, setLive] = useState(false); + const [timeRange, setTimeRange] = useState(() => lastThirtyMinutes()); + + const handleRefresh = useCallback(() => { + setTimeRange(lastThirtyMinutes()); + }, []); + + return ( +
+ + + +
+ ); +} diff --git a/ui/provider/src/pages/workload-detail.tsx b/ui/provider/src/pages/workload-detail.tsx index f275165e..7ae4ce64 100644 --- a/ui/provider/src/pages/workload-detail.tsx +++ b/ui/provider/src/pages/workload-detail.tsx @@ -6,9 +6,10 @@ * "what's wrong with this workload" from a customer: Overview surfaces raw * conditions (not just a coarse health enum), Instances is the direct * per-instance drill-down (image pull failures, crash loops, scheduling - * gates, quota), Events/Logs/Metrics are honest "Coming Soon" placeholders - * since none of those have a data source wired up yet, and YAML gives an - * escape hatch to the raw resource for anything the tabs don't surface. + * gates, quota), Logs mounts the empty datum-ui explorer until Loki is + * wired, Events/Metrics stay honest "Coming Soon" placeholders, and YAML + * gives an escape hatch to the raw resource for anything the tabs don't + * surface. * * `projectName` comes from `useParams()` resolving the ancestor route param * from staff-portal's project-scoped plugin mount @@ -21,6 +22,7 @@ import { ConditionsTable } from '../components/conditions-table'; import { DetailList, StatusBadge } from '../components/detail-list'; import { StatStrip, type Stat } from '../components/stat-strip'; import { ErrorOrRestrictedState, LoadingSkeleton } from '../components/states'; +import { WorkloadLogsExplorer } from '../components/workload-logs'; import { useWorkload, useWorkloadInstances, useWorkloadRaw } from '../lib/api'; import { healthToBadgeType, @@ -336,12 +338,7 @@ export default function WorkloadDetail() { /> - +