From 378f2005a3d2aeaa364199cbca412382851f0d57 Mon Sep 17 00:00:00 2001
From: Matt Jenkinson <75292329+mattdjenkinson@users.noreply.github.com>
Date: Fri, 6 Nov 2026 13:31:17 +0000
Subject: [PATCH 2/4] fix(ui): Slide the deploying bar across the card
Tailwind's animate-pulse only faded the bar in place. The host compiles no
keyframes the plugin could use, so the bar is animated with the Web
Animations API instead, and stays still under prefers-reduced-motion.
---
.../src/components/workload-overview-card.tsx | 37 ++++++++++++++++---
1 file changed, 31 insertions(+), 6 deletions(-)
diff --git a/ui/consumer/src/components/workload-overview-card.tsx b/ui/consumer/src/components/workload-overview-card.tsx
index a7b824e2..82863290 100644
--- a/ui/consumer/src/components/workload-overview-card.tsx
+++ b/ui/consumer/src/components/workload-overview-card.tsx
@@ -57,7 +57,7 @@ import {
LogsIcon,
TriangleAlertIcon,
} from 'lucide-react';
-import { useMemo, type ReactNode } from 'react';
+import { useEffect, useMemo, useRef, type ReactNode } from 'react';
import { Link } from 'react-router';
const INFO_COLOR = 'var(--color-badge-info)';
@@ -65,8 +65,6 @@ const INFO_COLOR = 'var(--color-badge-info)';
/** Text blocks keep this much width and push their neighbours onto the next line instead. */
const WRAP_BASIS = { flexBasis: '18rem' } as const;
-// Inline: cloud-portal compiles no `h-1`.
-const BAR_STYLE = { height: 4 } as const;
/** A tone's color at a low opacity, for the ring and the attention band. */
function tint(color: string, percent: number): string {
@@ -193,12 +191,39 @@ const PHASE_RING: Record
(null);
+ useEffect(() => {
+ const el = bar.current;
+ if (!el || typeof el.animate !== 'function') return;
+ if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) return;
+ const animation = el.animate([{ transform: 'translateX(-100%)' }, { transform: 'translateX(300%)' }], {
+ duration: 1600,
+ easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
+ iterations: Infinity,
+ });
+ return () => animation.cancel();
+ }, []);
+ return (
+
+
+
+ );
+}
+
function DeployingNote() {
return (
<>
-
-
-
+
Most deploys finish in under a minute. CPU, memory and network appear once an instance is ready.
From 161f743508fc1c2fff4ab05058fe0340e01ea617 Mon Sep 17 00:00:00 2001
From: Matt Jenkinson <75292329+mattdjenkinson@users.noreply.github.com>
Date: Fri, 6 Nov 2026 15:20:22 +0000
Subject: [PATCH 3/4] chore(ui): Bump datum-ui to 3.8.0 in both plugins
---
ui/consumer/package.json | 2 +-
ui/consumer/pnpm-lock.yaml | 37 ++++++------
ui/provider/package.json | 2 +-
ui/provider/pnpm-lock.yaml | 114 +++++++++++++++++++++++++++++++------
4 files changed, 119 insertions(+), 36 deletions(-)
diff --git a/ui/consumer/package.json b/ui/consumer/package.json
index b5a53967..98a6881c 100644
--- a/ui/consumer/package.json
+++ b/ui/consumer/package.json
@@ -14,7 +14,7 @@
},
"dependencies": {
"@datum-cloud/activity-ui": "0.7.1",
- "@datum-cloud/datum-ui": "3.6.0",
+ "@datum-cloud/datum-ui": "3.8.0",
"@tanstack/react-query": "5.102.8",
"@tanstack/react-table": "^9.3.1",
"@tanstack/react-virtual": "^3.14.26",
diff --git a/ui/consumer/pnpm-lock.yaml b/ui/consumer/pnpm-lock.yaml
index d600b664..583670e8 100644
--- a/ui/consumer/pnpm-lock.yaml
+++ b/ui/consumer/pnpm-lock.yaml
@@ -13,10 +13,10 @@ importers:
dependencies:
'@datum-cloud/activity-ui':
specifier: 0.7.1
- version: 0.7.1(23f2bc69542bccbc3ee5c81689a30302)
+ version: 0.7.1(4d79df9ee4747b64609303fe30ef220b)
'@datum-cloud/datum-ui':
- specifier: 3.6.0
- version: 3.6.0(@monaco-editor/react@4.7.0(monaco-editor@0.52.2)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@tanstack/react-table@9.3.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@tanstack/react-virtual@3.14.26(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(ai@7.0.161(zod@4.6.7))(date-fns-tz@3.2.0(date-fns@4.4.0))(date-fns@4.4.0)(lucide-react@1.67.0(@types/react@19.3.0)(react@19.3.0))(monaco-editor@0.52.2)(motion@13.5.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(nuqs@2.10.2(react-router@8.3.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0))(react-day-picker@10.0.2(@types/react@19.3.0)(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(recharts@3.10.1(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react-is@19.3.0)(react@19.3.0)(redux@5.0.1))(sonner@2.0.8(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(zod@4.6.7)
+ specifier: 3.8.0
+ version: 3.8.0(@monaco-editor/react@4.7.0(monaco-editor@0.52.2)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@tanstack/react-table@9.3.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@tanstack/react-virtual@3.14.26(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(ai@7.0.161(zod@4.6.7))(date-fns-tz@3.2.0(date-fns@4.4.0))(date-fns@4.4.0)(lucide-react@1.67.0(@types/react@19.3.0)(react@19.3.0))(monaco-editor@0.52.2)(motion@13.5.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(nuqs@2.10.2(react-router@8.3.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0))(react-day-picker@10.0.2(@types/react@19.3.0)(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(recharts@3.10.1(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react-is@19.3.0)(react@19.3.0)(redux@5.0.1))(sonner@2.0.8(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(zod@4.6.7)
'@tanstack/react-query':
specifier: 5.102.8
version: 5.102.8(react@19.3.0)
@@ -261,8 +261,8 @@ packages:
peerDependencies:
ai: '>=7 <8'
- '@datum-cloud/datum-ui@3.6.0':
- resolution: {integrity: sha512-SWA6m7k1UXsu8JZEjFGo0K32zfaoNFozYtLlrx7emNSP1TRBIOLkFVtRSewIhWKVPPMIdgI2o5zE2EuF+F0MOg==}
+ '@datum-cloud/datum-ui@3.8.0':
+ resolution: {integrity: sha512-T0IJxfBllKP7/TWO1Pbi7XCKKUYa0icaK176Okoyy1k4gR6ibOVl9QZSDrqR7LiHKGOVar7htzbetDID6K9vsQ==}
peerDependencies:
'@conform-to/react': '>=1.19.4 <2'
'@conform-to/zod': '>=1.19.4 <2'
@@ -304,7 +304,7 @@ packages:
recharts: '>=3 <4'
sonner: '>=2.0.7 <3'
streamdown: '>=2 <3'
- zod: '>=4 <5'
+ zod: '>=4.6.6 <5'
peerDependenciesMeta:
'@conform-to/react':
optional: true
@@ -356,8 +356,6 @@ packages:
optional: true
monaco-editor:
optional: true
- motion:
- optional: true
nprogress:
optional: true
nuqs:
@@ -1825,8 +1823,8 @@ packages:
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
- isomorphic-dompurify@3.22.0:
- resolution: {integrity: sha512-cz/BkSODnQir4IV2quqbuEHhAGz4MAYGvhoemsxuwMpY/oZbRHlNjPLPyIQ0fdULiwDPUE+OHPwWb6XTEiNk1A==}
+ isomorphic-dompurify@3.23.0:
+ resolution: {integrity: sha512-RuYFgdNApoTlR3QxVUyWdS/1ATpLEWEVg3H2tMImbfr6HkYPnuW1sdEtW4PRGlFhp04VbQQjcjXnucHYpXT44A==}
engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0}
deprecated: Raised the minimum Node.js version (breaking) without a major bump. Use 4.x for the same code with correct semver, or pin 3.19.0 for Node < 22.22.2.
@@ -2152,6 +2150,9 @@ packages:
tailwind-merge@3.6.0:
resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==}
+ tailwind-merge@3.7.0:
+ resolution: {integrity: sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA==}
+
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
@@ -2566,9 +2567,9 @@ snapshots:
'@date-fns/tz@1.5.0': {}
- '@datum-cloud/activity-ui@0.7.1(23f2bc69542bccbc3ee5c81689a30302)':
+ '@datum-cloud/activity-ui@0.7.1(4d79df9ee4747b64609303fe30ef220b)':
dependencies:
- '@datum-cloud/datum-ui': 3.6.0(@monaco-editor/react@4.7.0(monaco-editor@0.52.2)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@tanstack/react-table@9.3.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@tanstack/react-virtual@3.14.26(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(ai@7.0.161(zod@4.6.7))(date-fns-tz@3.2.0(date-fns@4.4.0))(date-fns@4.4.0)(lucide-react@1.67.0(@types/react@19.3.0)(react@19.3.0))(monaco-editor@0.52.2)(motion@13.5.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(nuqs@2.10.2(react-router@8.3.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0))(react-day-picker@10.0.2(@types/react@19.3.0)(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(recharts@3.10.1(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react-is@19.3.0)(react@19.3.0)(redux@5.0.1))(sonner@2.0.8(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(zod@4.6.7)
+ '@datum-cloud/datum-ui': 3.8.0(@monaco-editor/react@4.7.0(monaco-editor@0.52.2)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@tanstack/react-table@9.3.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@tanstack/react-virtual@3.14.26(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(ai@7.0.161(zod@4.6.7))(date-fns-tz@3.2.0(date-fns@4.4.0))(date-fns@4.4.0)(lucide-react@1.67.0(@types/react@19.3.0)(react@19.3.0))(monaco-editor@0.52.2)(motion@13.5.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(nuqs@2.10.2(react-router@8.3.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0))(react-day-picker@10.0.2(@types/react@19.3.0)(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(recharts@3.10.1(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react-is@19.3.0)(react@19.3.0)(redux@5.0.1))(sonner@2.0.8(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(zod@4.6.7)
'@monaco-editor/react': 4.7.0(monaco-editor@0.52.2)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@radix-ui/react-tooltip': 1.2.16(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
class-variance-authority: 0.7.1
@@ -2584,7 +2585,7 @@ snapshots:
dependencies:
ai: 7.0.161(zod@4.6.7)
- '@datum-cloud/datum-ui@3.6.0(@monaco-editor/react@4.7.0(monaco-editor@0.52.2)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@tanstack/react-table@9.3.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@tanstack/react-virtual@3.14.26(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(ai@7.0.161(zod@4.6.7))(date-fns-tz@3.2.0(date-fns@4.4.0))(date-fns@4.4.0)(lucide-react@1.67.0(@types/react@19.3.0)(react@19.3.0))(monaco-editor@0.52.2)(motion@13.5.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(nuqs@2.10.2(react-router@8.3.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0))(react-day-picker@10.0.2(@types/react@19.3.0)(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(recharts@3.10.1(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react-is@19.3.0)(react@19.3.0)(redux@5.0.1))(sonner@2.0.8(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(zod@4.6.7)':
+ '@datum-cloud/datum-ui@3.8.0(@monaco-editor/react@4.7.0(monaco-editor@0.52.2)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@tanstack/react-table@9.3.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@tanstack/react-virtual@3.14.26(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(ai@7.0.161(zod@4.6.7))(date-fns-tz@3.2.0(date-fns@4.4.0))(date-fns@4.4.0)(lucide-react@1.67.0(@types/react@19.3.0)(react@19.3.0))(monaco-editor@0.52.2)(motion@13.5.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(nuqs@2.10.2(react-router@8.3.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0))(react-day-picker@10.0.2(@types/react@19.3.0)(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(recharts@3.10.1(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react-is@19.3.0)(react@19.3.0)(redux@5.0.1))(sonner@2.0.8(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(zod@4.6.7)':
dependencies:
'@datum-cloud/datum-ui-assistant-core': 0.1.0(ai@7.0.161(zod@4.6.7))
'@radix-ui/react-avatar': 1.2.6(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
@@ -2606,11 +2607,12 @@ snapshots:
class-variance-authority: 0.7.1
clsx: 2.1.1
cmdk: 1.1.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
- isomorphic-dompurify: 3.22.0
+ isomorphic-dompurify: 3.23.0
lucide-react: 1.67.0(@types/react@19.3.0)(react@19.3.0)
+ motion: 13.5.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
- tailwind-merge: 3.6.0
+ tailwind-merge: 3.7.0
tw-animate-css: 1.4.0
optionalDependencies:
'@monaco-editor/react': 4.7.0(monaco-editor@0.52.2)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
@@ -2620,7 +2622,6 @@ snapshots:
date-fns: 4.4.0
date-fns-tz: 3.2.0(date-fns@4.4.0)
monaco-editor: 0.52.2
- motion: 13.5.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
nuqs: 2.10.2(react-router@8.3.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0)
react-day-picker: 10.0.2(@types/react@19.3.0)(react@19.3.0)
recharts: 3.10.1(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react-is@19.3.0)(react@19.3.0)(redux@5.0.1)
@@ -3896,7 +3897,7 @@ snapshots:
isexe@2.0.0: {}
- isomorphic-dompurify@3.22.0:
+ isomorphic-dompurify@3.23.0:
dependencies:
dompurify: 3.4.17
jsdom: 30.0.1
@@ -4196,6 +4197,8 @@ snapshots:
tailwind-merge@3.6.0: {}
+ tailwind-merge@3.7.0: {}
+
tiny-invariant@1.3.3: {}
tinybench@6.2.2: {}
diff --git a/ui/provider/package.json b/ui/provider/package.json
index 069fcd0b..29867d54 100644
--- a/ui/provider/package.json
+++ b/ui/provider/package.json
@@ -12,7 +12,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
- "@datum-cloud/datum-ui": "^3.5.0",
+ "@datum-cloud/datum-ui": "^3.8.0",
"@monaco-editor/react": "^4.7.0",
"@tanstack/react-query": "5.101.4",
"@tanstack/react-table": "^9.0.0",
diff --git a/ui/provider/pnpm-lock.yaml b/ui/provider/pnpm-lock.yaml
index 30f1629a..1ec46946 100644
--- a/ui/provider/pnpm-lock.yaml
+++ b/ui/provider/pnpm-lock.yaml
@@ -13,8 +13,8 @@ importers:
.:
dependencies:
'@datum-cloud/datum-ui':
- specifier: ^3.5.0
- version: 3.5.0(@monaco-editor/react@4.7.0(monaco-editor@0.55.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(@tanstack/react-table@9.2.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(@tanstack/react-virtual@3.14.10(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(date-fns-tz@3.2.0(date-fns@4.4.0))(date-fns@4.4.0)(js-yaml@5.4.2)(lucide-react@1.31.0(react@19.2.8))(monaco-editor@0.55.1)(motion@13.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(nuqs@2.10.1(react-router@7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(react-day-picker@10.0.1(@types/react@19.2.18)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(recharts@3.10.1(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react-is@19.3.0)(react@19.2.8)(redux@5.0.1))(sonner@2.0.8(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(zod@4.6.7)
+ specifier: ^3.8.0
+ version: 3.8.0(@monaco-editor/react@4.7.0(monaco-editor@0.55.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(@tanstack/react-table@9.2.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(@tanstack/react-virtual@3.14.10(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(ai@7.0.163(zod@4.6.7))(date-fns-tz@3.2.0(date-fns@4.4.0))(date-fns@4.4.0)(js-yaml@5.4.2)(lucide-react@1.31.0(react@19.2.8))(monaco-editor@0.55.1)(motion@13.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(nuqs@2.10.1(react-router@7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(react-day-picker@10.0.1(@types/react@19.2.18)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(recharts@3.10.1(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react-is@19.3.0)(react@19.2.8)(redux@5.0.1))(sonner@2.0.8(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(zod@4.6.7)
'@monaco-editor/react':
specifier: ^4.7.0
version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
@@ -94,6 +94,22 @@ importers:
packages:
+ '@ai-sdk/gateway@4.0.136':
+ resolution: {integrity: sha512-P851NL80P85Ee6MhblRuuCzNlQdOYU5rKmdt/Wuo5dMtkzvIHhyVZY9wtfKG3wBJUUoxHxWKsI2aR6O75o8diA==}
+ engines: {node: '>=22'}
+ peerDependencies:
+ zod: ^3.25.76 || ^4.1.8
+
+ '@ai-sdk/provider-utils@5.0.75':
+ resolution: {integrity: sha512-k4yAsFliGZwoTpyTpm3ptxwIeaPvRh5vGh/UWoqDiLLXKoyz4zuBGS7p3ygf8to5Bh4fAOHynotLBsWLIYdu2A==}
+ engines: {node: '>=22'}
+ peerDependencies:
+ zod: ^3.25.76 || ^4.1.8
+
+ '@ai-sdk/provider@4.0.36':
+ resolution: {integrity: sha512-GqvWZSZdBelCGughlBdOoupT258rgM3YMbaLAS9WSZJ0vlt8c9vm3My+ZT31mYhiReMCMCF2qF2axnGbILpk1A==}
+ engines: {node: '>=22'}
+
'@asamuzakjp/css-color@6.0.7':
resolution: {integrity: sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==}
engines: {node: ^22.13.0 || >=24.0.0}
@@ -228,8 +244,13 @@ packages:
'@date-fns/tz@1.5.0':
resolution: {integrity: sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==}
- '@datum-cloud/datum-ui@3.5.0':
- resolution: {integrity: sha512-ShGLKZrfWJdsNHGiUrLmFDl0ZhsP2Z7LKQPzcD6uPxBRqM7hNUYNq/cKri7KWFIosfB6Z8GzenZKWOX8QQ+/WQ==}
+ '@datum-cloud/datum-ui-assistant-core@0.1.0':
+ resolution: {integrity: sha512-k/d17fVwYDt/DswVuWOCWo6hsAUJ/Y1hLeambRb3PckUqXnU+RdXJpYi62gNqHLpUZdU7rmmsVOohxyhFoxX0w==}
+ peerDependencies:
+ ai: '>=7 <8'
+
+ '@datum-cloud/datum-ui@3.8.0':
+ resolution: {integrity: sha512-T0IJxfBllKP7/TWO1Pbi7XCKKUYa0icaK176Okoyy1k4gR6ibOVl9QZSDrqR7LiHKGOVar7htzbetDID6K9vsQ==}
peerDependencies:
'@conform-to/react': '>=1.19.4 <2'
'@conform-to/zod': '>=1.19.4 <2'
@@ -271,7 +292,7 @@ packages:
recharts: '>=3 <4'
sonner: '>=2.0.7 <3'
streamdown: '>=2 <3'
- zod: '>=4 <5'
+ zod: '>=4.6.6 <5'
peerDependenciesMeta:
'@conform-to/react':
optional: true
@@ -323,8 +344,6 @@ packages:
optional: true
monaco-editor:
optional: true
- motion:
- optional: true
nprogress:
optional: true
nuqs:
@@ -1429,16 +1448,29 @@ packages:
cpu: [x64]
os: [win32]
+ '@vercel/oidc@3.2.0':
+ resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==}
+ engines: {node: '>= 20'}
+
'@vitejs/plugin-react@5.2.0':
resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==}
engines: {node: ^20.19.0 || >=22.12.0}
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
+ '@workflow/serde@4.1.0':
+ resolution: {integrity: sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ==}
+
adm-zip@0.6.1:
resolution: {integrity: sha512-Xwrja8nx9e5o2N1my4DsKCeKpdrnACyr1wtbPxBDgGzKzKyE9kRtBFA8mWldI+RVlD7CBZNWY/wQ2+ydwOR6kQ==}
engines: {node: '>=14.0'}
+ ai@7.0.163:
+ resolution: {integrity: sha512-nm8+qn36NQXO4Hjfj7SlbcNRhUyNXKtdoS9xhXr/z92lvdGFuPkzHmA2tFzhaUQy13IitNYrcw2QSNpEJC/iSA==}
+ engines: {node: '>=22'}
+ peerDependencies:
+ zod: ^3.25.76 || ^4.1.8
+
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
@@ -1588,6 +1620,10 @@ packages:
eventemitter3@5.0.4:
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
+ eventsource-parser@3.1.1:
+ resolution: {integrity: sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==}
+ engines: {node: '>=18.0.0'}
+
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
@@ -1635,8 +1671,8 @@ packages:
is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
- isomorphic-dompurify@3.22.0:
- resolution: {integrity: sha512-cz/BkSODnQir4IV2quqbuEHhAGz4MAYGvhoemsxuwMpY/oZbRHlNjPLPyIQ0fdULiwDPUE+OHPwWb6XTEiNk1A==}
+ isomorphic-dompurify@3.23.0:
+ resolution: {integrity: sha512-RuYFgdNApoTlR3QxVUyWdS/1ATpLEWEVg3H2tMImbfr6HkYPnuW1sdEtW4PRGlFhp04VbQQjcjXnucHYpXT44A==}
engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0}
deprecated: Raised the minimum Node.js version (breaking) without a major bump. Use 4.x for the same code with correct semver, or pin 3.19.0 for Node < 22.22.2.
@@ -1666,6 +1702,9 @@ packages:
engines: {node: '>=6'}
hasBin: true
+ json-schema@0.4.0:
+ resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
+
json5@2.2.3:
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
engines: {node: '>=6'}
@@ -1902,8 +1941,8 @@ packages:
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
- tailwind-merge@3.6.0:
- resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==}
+ tailwind-merge@3.7.0:
+ resolution: {integrity: sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA==}
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
@@ -2067,6 +2106,26 @@ packages:
snapshots:
+ '@ai-sdk/gateway@4.0.136(zod@4.6.7)':
+ dependencies:
+ '@ai-sdk/provider': 4.0.36
+ '@ai-sdk/provider-utils': 5.0.75(zod@4.6.7)
+ '@vercel/oidc': 3.2.0
+ zod: 4.6.7
+
+ '@ai-sdk/provider-utils@5.0.75(zod@4.6.7)':
+ dependencies:
+ '@ai-sdk/provider': 4.0.36
+ '@standard-schema/spec': 1.1.0
+ '@workflow/serde': 4.1.0
+ eventsource-parser: 3.1.1
+ undici: 7.30.1
+ zod: 4.6.7
+
+ '@ai-sdk/provider@4.0.36':
+ dependencies:
+ json-schema: 0.4.0
+
'@asamuzakjp/css-color@6.0.7':
dependencies:
'@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
@@ -2224,8 +2283,13 @@ snapshots:
'@date-fns/tz@1.5.0': {}
- '@datum-cloud/datum-ui@3.5.0(@monaco-editor/react@4.7.0(monaco-editor@0.55.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(@tanstack/react-table@9.2.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(@tanstack/react-virtual@3.14.10(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(date-fns-tz@3.2.0(date-fns@4.4.0))(date-fns@4.4.0)(js-yaml@5.4.2)(lucide-react@1.31.0(react@19.2.8))(monaco-editor@0.55.1)(motion@13.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(nuqs@2.10.1(react-router@7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(react-day-picker@10.0.1(@types/react@19.2.18)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(recharts@3.10.1(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react-is@19.3.0)(react@19.2.8)(redux@5.0.1))(sonner@2.0.8(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(zod@4.6.7)':
+ '@datum-cloud/datum-ui-assistant-core@0.1.0(ai@7.0.163(zod@4.6.7))':
+ dependencies:
+ ai: 7.0.163(zod@4.6.7)
+
+ '@datum-cloud/datum-ui@3.8.0(@monaco-editor/react@4.7.0(monaco-editor@0.55.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(@tanstack/react-table@9.2.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(@tanstack/react-virtual@3.14.10(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(ai@7.0.163(zod@4.6.7))(date-fns-tz@3.2.0(date-fns@4.4.0))(date-fns@4.4.0)(js-yaml@5.4.2)(lucide-react@1.31.0(react@19.2.8))(monaco-editor@0.55.1)(motion@13.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(nuqs@2.10.1(react-router@7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8))(react-day-picker@10.0.1(@types/react@19.2.18)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(recharts@3.10.1(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react-is@19.3.0)(react@19.2.8)(redux@5.0.1))(sonner@2.0.8(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(zod@4.6.7)':
dependencies:
+ '@datum-cloud/datum-ui-assistant-core': 0.1.0(ai@7.0.163(zod@4.6.7))
'@radix-ui/react-avatar': 1.2.6(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
'@radix-ui/react-checkbox': 1.3.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
'@radix-ui/react-collapsible': 1.1.20(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
@@ -2245,21 +2309,22 @@ snapshots:
class-variance-authority: 0.7.1
clsx: 2.1.1
cmdk: 1.1.1(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
- isomorphic-dompurify: 3.22.0
+ isomorphic-dompurify: 3.23.0
lucide-react: 1.31.0(react@19.2.8)
+ motion: 13.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
react: 19.2.8
react-dom: 19.2.8(react@19.2.8)
- tailwind-merge: 3.6.0
+ tailwind-merge: 3.7.0
tw-animate-css: 1.4.0
optionalDependencies:
'@monaco-editor/react': 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
'@tanstack/react-table': 9.2.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
'@tanstack/react-virtual': 3.14.10(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ ai: 7.0.163(zod@4.6.7)
date-fns: 4.4.0
date-fns-tz: 3.2.0(date-fns@4.4.0)
js-yaml: 5.4.2
monaco-editor: 0.55.1
- motion: 13.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
nuqs: 2.10.1(react-router@7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)
react-day-picker: 10.0.1(@types/react@19.2.18)(react@19.2.8)
recharts: 3.10.1(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react-is@19.3.0)(react@19.2.8)(redux@5.0.1)
@@ -3182,6 +3247,8 @@ snapshots:
'@typescript/typescript-win32-x64@7.0.2':
optional: true
+ '@vercel/oidc@3.2.0': {}
+
'@vitejs/plugin-react@5.2.0(vite@7.3.7)':
dependencies:
'@babel/core': 7.29.7
@@ -3194,8 +3261,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@workflow/serde@4.1.0': {}
+
adm-zip@0.6.1: {}
+ ai@7.0.163(zod@4.6.7):
+ dependencies:
+ '@ai-sdk/gateway': 4.0.136(zod@4.6.7)
+ '@ai-sdk/provider': 4.0.36
+ '@ai-sdk/provider-utils': 5.0.75(zod@4.6.7)
+ zod: 4.6.7
+
argparse@2.0.1: {}
aria-hidden@1.2.6:
@@ -3351,6 +3427,8 @@ snapshots:
eventemitter3@5.0.4: {}
+ eventsource-parser@3.1.1: {}
+
fdir@6.5.0(picomatch@4.0.5):
optionalDependencies:
picomatch: 4.0.5
@@ -3383,7 +3461,7 @@ snapshots:
is-potential-custom-element-name@1.0.1: {}
- isomorphic-dompurify@3.22.0:
+ isomorphic-dompurify@3.23.0:
dependencies:
dompurify: 3.4.17
jsdom: 30.0.1
@@ -3429,6 +3507,8 @@ snapshots:
jsesc@3.1.0: {}
+ json-schema@0.4.0: {}
+
json5@2.2.3: {}
lru-cache@11.5.2: {}
@@ -3641,7 +3721,7 @@ snapshots:
symbol-tree@3.2.4: {}
- tailwind-merge@3.6.0: {}
+ tailwind-merge@3.7.0: {}
tiny-invariant@1.3.3: {}
From ace44fba3e166377dc9d5da6119d417c4e8a5963 Mon Sep 17 00:00:00 2001
From: Matt Jenkinson <75292329+mattdjenkinson@users.noreply.github.com>
Date: Fri, 6 Nov 2026 16:52:13 +0000
Subject: [PATCH 4/4] feat(ui): Show a workload's load balancers, and load the
Overview in place
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
A Load balancers card on the workload Overview lists each load balancer
whose rules send traffic to the workload: its state, hostname and the
routes that reach it ("/ → :8080 (http)", with the workload's share when
a rule splits traffic). Route reading comes from Kevin's
feat/workload-overview-polish branch. With no load balancer, "Add to a
load balancer" makes the workload's NetworkService if it has none (the
same object `datumctl compute url` makes) and opens the portal's create
dialog with the workload chosen.
A hostname belongs to its load balancer, so the page no longer presents
one as the workload's URL: the URL and Visit button by the title, the
endpoint line on the overview card and the Load balancer row in
Configuration are gone.
Loading states now match what they stand in for. The stats strip holds
each value, hint and sparkline with a bone until its data is in, instead
of showing zero or a flat line. The Instances and Load balancers cards
load as their tables, headers included, and the page and workloads-list
skeletons use the live tables' markup.
Co-authored-by: Kevin Williams
---
ui/consumer/src/adapter.ts | 10 +
ui/consumer/src/components/skeletons.tsx | 208 +++++++++++---
.../src/components/workload-instances.tsx | 6 +-
.../components/workload-load-balancers.tsx | 260 ++++++++++++++++++
.../src/components/workload-overview-card.tsx | 141 +++++-----
.../src/components/workload-page-chrome.tsx | 132 ++-------
ui/consumer/src/lib/alb-status.test.ts | 1 +
ui/consumer/src/lib/api.test.ts | 112 ++++++++
ui/consumer/src/lib/api.ts | 156 ++++++++++-
.../src/lib/workload-presenters.test.ts | 35 +++
ui/consumer/src/lib/workload-presenters.ts | 20 ++
ui/consumer/src/pages/workload-detail.tsx | 159 ++---------
ui/consumer/src/schema.ts | 2 +
13 files changed, 862 insertions(+), 380 deletions(-)
create mode 100644 ui/consumer/src/components/workload-load-balancers.tsx
diff --git a/ui/consumer/src/adapter.ts b/ui/consumer/src/adapter.ts
index a3198639..48153b0e 100644
--- a/ui/consumer/src/adapter.ts
+++ b/ui/consumer/src/adapter.ts
@@ -192,6 +192,15 @@ function deriveReplicasPerRegion(placements: RawWorkloadPlacement[]): number | u
return mins.every((m) => m === first) ? first : undefined;
}
+function deriveNamedPorts(runtime?: RawRuntime): Workload['namedPorts'] {
+ const containers = runtime?.sandbox?.containers ?? [];
+ return containers.flatMap((c) =>
+ (c.ports ?? []).flatMap((p) =>
+ p.name && p.port !== undefined ? [{ name: p.name, port: p.port, protocol: p.protocol ?? 'TCP' }] : []
+ )
+ );
+}
+
function derivePorts(runtime?: RawRuntime): string[] {
const containers = runtime?.sandbox?.containers ?? [];
return containers.flatMap((c) =>
@@ -349,6 +358,7 @@ export function toWorkload(raw: RawWorkload): Workload {
runtimeType: runtime ? (runtime.sandbox ? 'Container sandbox' : 'Virtual machine') : undefined,
tags: deriveTags(runtime),
ports,
+ namedPorts: deriveNamedPorts(runtime),
locations: workloadLocations(placements, raw.status?.placements ?? []),
resources: deriveResources(runtime),
replicasPerRegion: deriveReplicasPerRegion(placements),
diff --git a/ui/consumer/src/components/skeletons.tsx b/ui/consumer/src/components/skeletons.tsx
index 18609bcb..65541c09 100644
--- a/ui/consumer/src/components/skeletons.tsx
+++ b/ui/consumer/src/components/skeletons.tsx
@@ -8,6 +8,10 @@
import { Card, CardContent, CardHeader } from '@datum-cloud/datum-ui/card';
import { STRIP_GAP } from './stat-strip';
import { Skeleton } from '@datum-cloud/datum-ui/skeleton';
+import { cn } from '@datum-cloud/datum-ui/utils';
+import { Icon } from '@datum-cloud/datum-ui/icons';
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@datum-cloud/datum-ui/table';
+import { ChevronDown, ChevronUp } from 'lucide-react';
import type { CSSProperties } from 'react';
const PANEL_STYLE = { height: '27rem' } as const;
@@ -16,15 +20,16 @@ const CHART_STYLE = { height: 224 } as const;
const TOPOLOGY_STYLE = { height: '28rem' } as const;
/**
- * The list table's columns before usage data arrives. CPU and Memory only
- * appear once a row has data, so the skeleton leaves them out.
+ * The list table's columns before usage data arrives, each with a bone the
+ * width of what it usually holds. CPU and Memory only appear once a row has
+ * data, so the skeleton leaves them out.
*/
-const TABLE_COLUMNS = ['Name', 'Status', 'Network', 'Locations'] as const;
-
-/** Same thin line the live table draws (`TABLE_STYLES` in workload-table). */
-const ROW_LINE_STYLE = {
- borderBottom: '1px solid color-mix(in oklab, var(--border) 60%, transparent)',
-} as const;
+const TABLE_COLUMNS = [
+ { title: 'Name', bones: ['h-4 w-32'] },
+ { title: 'Status', bones: ['size-2 rounded-full', 'h-4 w-20'] },
+ { title: 'Network', bones: ['h-4 w-16'] },
+ { title: 'Locations', bones: ['size-1.5 rounded-full', 'h-4 w-40'] },
+] as const;
function Bone({ className, style }: { className?: string; style?: CSSProperties }) {
return ;
@@ -147,10 +152,21 @@ export function WorkloadListCardsSkeleton() {
);
}
+/** The sort chevrons `SortableHeader` draws, at rest. */
+function SortChevrons() {
+ return (
+
+
+
+
+ );
+}
+
/**
- * Open table like the live one: plain muted header, `h-10` rows, thin lines.
- * `summary={false}` is the in-page fallback, where the strip and toolbar are
- * already on screen.
+ * The live table's shell (`WorkloadTable`: datum-ui's DataTable markup in a
+ * rounded, bordered panel, with sortable headers) and placeholder rows in it, so the
+ * swap to real rows doesn't move anything. `summary={false}` is the in-page
+ * fallback, where the strip and toolbar are already on screen.
*/
export function WorkloadListTableSkeleton({ summary = true }: { summary?: boolean }) {
return (
@@ -161,39 +177,130 @@ export function WorkloadListTableSkeleton({ summary = true }: { summary?: boolea
>
) : null}
-
-
- {TABLE_COLUMNS.map((column) => (
-
- {column}
-
- ))}
+
+ {/* DataTable's class and slots: the host styles its tables by these. */}
+
+
+
+
+ {TABLE_COLUMNS.map((column) => (
+
+
+ {column.title}
+
+
+
+ ))}
+
+
+
+ {Array.from({ length: 6 }).map((_, row) => (
+
+ {TABLE_COLUMNS.map((column) => (
+
+
+ {column.bones.map((bone) => (
+
+ ))}
+
+
+ ))}
+
+ ))}
+
+
- {Array.from({ length: 6 }).map((_, row) => (
-
- {TABLE_COLUMNS.map((column) => (
-
- ))}
-
- ))}
);
}
-/** Placeholder instance rows, the same `h-10` as `WorkloadInstances` rows. */
-export function InstanceRowsSkeleton({ rows = 2 }: { rows?: number }) {
+// Column heads as the Instances and Load balancers cards set them.
+const CARD_HEAD = 'text-muted-foreground text-3xs h-9 font-semibold uppercase';
+
+interface CardColumn {
+ title: string;
+ /** Bone classes, side by side in the cell. */
+ bones: readonly string[];
+ /** Hidden below `md`, as the live column is. */
+ wide?: boolean;
+}
+
+/** The Instances card's columns, before network data adds its sparkline column. */
+const INSTANCE_COLUMNS: readonly CardColumn[] = [
+ { title: 'Instance', bones: ['h-4 w-48'] },
+ { title: 'Location', bones: ['h-4 w-20'], wide: true },
+ { title: 'State', bones: ['h-4 w-14 rounded-sm'] },
+ { title: 'Address', bones: ['h-4 w-32'], wide: true },
+ { title: '', bones: ['size-6 rounded-md', 'size-6 rounded-md', 'size-6 rounded-md'] },
+];
+
+const LOAD_BALANCER_COLUMNS: readonly CardColumn[] = [
+ { title: 'Load balancer', bones: ['h-4 w-28'] },
+ { title: 'State', bones: ['h-4 w-14 rounded-sm'] },
+ { title: 'Hostname', bones: ['h-4 w-56'], wide: true },
+ { title: 'Routes', bones: ['h-4 w-32'] },
+ { title: '', bones: ['size-6 rounded-md'] },
+];
+
+/**
+ * A card's table while its rows load: the live table's header and cell
+ * padding, with bones where the values go, so the rows land without a jump.
+ */
+function CardTableSkeleton({ columns, rows }: { columns: readonly CardColumn[]; rows: number }) {
+ const edge = (index: number) =>
+ index === 0 ? 'pl-(--card-px)' : index === columns.length - 1 ? 'pr-(--card-px)' : undefined;
return (
-
- {Array.from({ length: rows }).map((_, index) => (
-
-
-
-
-
-
- ))}
-
+
+
+
+ {columns.map((column, index) => (
+
+ {column.title}
+
+ ))}
+
+
+
+ {Array.from({ length: rows }, (_, row) => (
+
+ {columns.map((column, index) => (
+
+
+ {column.bones.map((bone, i) => (
+
+ ))}
+
+
+ ))}
+
+ ))}
+
+
+ );
+}
+
+/** The Instances card's table while instances load. */
+export function InstanceTableSkeleton({ rows = 2 }: { rows?: number }) {
+ return
;
+}
+
+/** The Load balancers card's table while the lookup runs. */
+export function LoadBalancerTableSkeleton() {
+ return
;
+}
+
+/** A sectioned card's bordered header: icon and title. */
+function CardHeaderBone() {
+ return (
+
+
+
+
+
+
);
}
@@ -222,19 +329,30 @@ export function WorkloadOverviewSkeleton() {