From 3a25f19a667c4a3bb366abd318ca35bf609e94b8 Mon Sep 17 00:00:00 2001 From: Chris Busillo Date: Thu, 7 May 2026 19:45:51 -0400 Subject: [PATCH] Add operator environment detail strip --- frontend/src/App.tsx | 82 ++++++++++++++++- frontend/src/ProductOverviewShell.tsx | 83 ++++++++++++++++- frontend/src/styles.css | 126 ++++++++++++++++++++++++++ 3 files changed, 284 insertions(+), 7 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f84ab9d2..6ac95584 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -233,6 +233,11 @@ export function App() { productOverviews.find( (overview) => overview.product === selected.driverId, ) ?? null; + const [selectedEnvironment, setSelectedEnvironment] = useState("prod"); + const activeEnvironment = + selectedProductOverview?.environments.find( + (environment) => environment.environment === selectedEnvironment, + ) ?? selectedProductOverview?.environments[0]; const [prodView, setProdView] = useState(null); const [testingView, setTestingView] = useState( null, @@ -386,9 +391,15 @@ export function App() { const environments = selectedProductOverview.environments.filter( (environment) => environment.environment.trim(), ); + setConfigStatuses([]); + if (!environments.length) { + setConfigStatusError(""); + setConfigStatusLoading(false); + return; + } setConfigStatusLoading(true); setConfigStatusError(""); - Promise.all( + Promise.allSettled( environments.map((environment) => readProductEnvironmentConfigStatus( selectedProductOverview.product, @@ -396,9 +407,23 @@ export function App() { ).then((payload) => payload.config_status), ), ) - .then((statuses) => { + .then((results) => { if (!controller.signal.aborted) { + const statuses = results.flatMap((result) => + result.status === "fulfilled" ? [result.value] : [], + ); + const failures = results.flatMap((result, index) => + result.status === "rejected" + ? [ + configStatusErrorMessage( + environments[index].environment, + result.reason, + ), + ] + : [], + ); setConfigStatuses(statuses); + setConfigStatusError(failures[0] ?? ""); } }) .catch((apiError: unknown) => { @@ -422,6 +447,17 @@ export function App() { return () => controller.abort(); }, [authStatus, selectedProductOverview, refreshKey]); + useEffect(() => { + if ( + selectedProductOverview?.environments.length && + !selectedProductOverview.environments.some( + (environment) => environment.environment === selectedEnvironment, + ) + ) { + setSelectedEnvironment(selectedProductOverview.environments[0].environment); + } + }, [selectedProductOverview, selectedEnvironment]); + const currentDriver = drivers.find( (driver) => driver.driver_id === selected.driverId, ); @@ -518,6 +554,8 @@ export function App() { product={selectedProductOverview} selected={selected} loading={loading} + selectedEnvironment={activeEnvironment?.environment ?? selectedEnvironment} + onSelectEnvironment={setSelectedEnvironment} />
("all"); const [fixtureWorkGraphMode, setFixtureWorkGraphMode] = useState("all"); + const [fixtureEnvironment, setFixtureEnvironment] = useState("prod"); const readyProd = fixtureLane({ instance: "prod", artifact: "ghcr.io/every/verireel@sha256:11112222", @@ -976,7 +1026,16 @@ function StateFixtureGallery({ trust_state: "verified", provenance: readyTesting.provenance, warnings: [], - available_actions: [], + available_actions: FIXTURE_GENERIC_WEB_ACTIONS.map((action) => ({ + ...action, + authz_action: "product_environment.write", + enabled: action.action_id === "prod_promotion_workflow", + disabled_reasons: + action.action_id === "prod_promotion" + ? ["Prod promotion runs through the product-owned workflow."] + : [], + trust_state: "verified", + })), }, { environment: "prod", @@ -986,7 +1045,13 @@ function StateFixtureGallery({ trust_state: "verified", provenance: readyProd.provenance, warnings: [], - available_actions: [], + available_actions: FIXTURE_GENERIC_WEB_ACTIONS.map((action) => ({ + ...action, + authz_action: "product_environment.write", + enabled: false, + disabled_reasons: ["Prod lane writes require fresh backup evidence."], + trust_state: "recorded", + })), }, ]; const fixtureProducts: ProductSiteOverview[] = [ @@ -1300,6 +1365,15 @@ function StateFixtureGallery({ lane={readyProd} /> +
+ +
void; }) { const environments = product?.environments ?? []; + const activeEnvironment = + environments.find( + (environment) => environment.environment === selectedEnvironment, + ) ?? environments[0]; const enabledActions = product?.available_actions.filter( (action) => action.enabled, ); @@ -63,15 +71,20 @@ export function ProductOverviewShell({
{environments.length ? ( environments.map((environment) => ( -
onSelectEnvironment(environment.environment)} > {environment.environment} {freshnessLabel(environment.trust_state)} {environment.context} -
+ )) ) : (
)} + {!loading && activeEnvironment ? ( + + ) : null} {product?.warnings.length ? (
{product.warnings.map((warning) => ( @@ -116,3 +132,64 @@ export function ProductOverviewShell({
); } + +function EnvironmentDetailStrip({ + environment, +}: { + environment: ProductSiteOverview["environments"][number]; +}) { + const enabledActions = environment.available_actions.filter( + (action) => action.enabled, + ); + const blockedActions = environment.available_actions.filter( + (action) => !action.enabled, + ); + return ( +
+
+ + {environment.environment} + + {environment.context} + {environment.base_url ? ( + + + ) : null} +
+
+ + + +
+ {environment.available_actions.length ? ( +
+ {environment.available_actions.slice(0, 4).map((action) => ( + + {action.label} + + ))} +
+ ) : null} +
+ ); +} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index efaa1a72..5d33b25e 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -598,6 +598,7 @@ p { } .product-environment-pill { + appearance: none; display: grid; min-height: 86px; align-content: space-between; @@ -605,7 +606,33 @@ p { border: 1px solid var(--hair); border-radius: 6px; background: var(--bg-2); + color: inherit; + font: inherit; padding: 10px; + text-align: left; + cursor: pointer; + transition: + background 140ms ease, + border-color 140ms ease, + box-shadow 140ms ease, + transform 140ms ease; +} + +.product-environment-pill:hover, +.product-environment-pill:focus-visible { + border-color: var(--accent); + background: color-mix(in oklab, var(--accent) 7%, var(--bg-2)); + outline: 0; +} + +.product-environment-pill:focus-visible { + box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 24%, transparent); +} + +.product-environment-pill[data-selected="true"] { + border-color: var(--accent); + background: color-mix(in oklab, var(--accent) 10%, var(--bg-2)); + box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--accent) 34%, transparent); } .product-environment-pill[data-environment="prod"] { @@ -628,6 +655,91 @@ p { font-weight: 600; } +.environment-detail-strip { + display: grid; + grid-template-columns: minmax(240px, 0.9fr) minmax(0, 1.45fr); + gap: 1px; + margin: 0 16px 16px; + overflow: hidden; + border: 1px solid var(--hair); + border-radius: 6px; + background: var(--hair); +} + +.environment-detail-strip[data-environment="prod"] { + border-color: color-mix(in oklab, var(--lane-prod) 36%, var(--hair)); +} + +.environment-detail-strip[data-environment="testing"] { + border-color: color-mix(in oklab, var(--lane-testing) 36%, var(--hair)); +} + +.environment-detail-identity, +.environment-detail-facts, +.environment-action-strip { + min-width: 0; + background: var(--panel); + padding: 12px; +} + +.environment-detail-identity { + display: grid; + align-content: start; + gap: 8px; +} + +.environment-detail-identity code, +.environment-detail-identity a, +.environment-detail-facts code, +.environment-action-chip { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.environment-detail-identity a { + display: inline-flex; + min-width: 0; + align-items: center; + gap: 6px; + color: var(--accent); + text-decoration: none; +} + +.environment-detail-identity a:hover { + text-decoration: underline; +} + +.environment-detail-facts { + display: grid; + grid-template-columns: minmax(0, 1.25fr) repeat(2, minmax(120px, 0.65fr)); + gap: 10px; +} + +.environment-action-strip { + grid-column: 1 / -1; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 8px; + border-top: 1px solid var(--hair); +} + +.environment-action-chip { + min-height: 34px; + border: 1px solid var(--hair-soft); + border-radius: 6px; + background: var(--bg-2); + color: var(--fg-muted); + padding: 8px 10px; + font-size: 11px; + font-weight: 700; +} + +.environment-action-chip[data-enabled="true"] { + border-color: color-mix(in oklab, var(--status-pass) 34%, var(--hair-soft)); + color: var(--fg-strong); +} + .overview-warning-list { display: grid; gap: 6px; @@ -1857,6 +1969,8 @@ p { .operator-cockpit, .lane-grid, .product-overview-grid, + .environment-detail-strip, + .environment-detail-facts, .work-grid, .work-grid-evidence, .config-status-environment, @@ -1873,6 +1987,10 @@ p { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .environment-action-strip { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .product-inventory-row { grid-template-columns: minmax(180px, 0.9fr) minmax(0, 1fr) auto; } @@ -1926,6 +2044,14 @@ p { grid-template-columns: minmax(0, 1fr); } + .environment-detail-strip { + margin: 0 12px 12px; + } + + .environment-action-strip { + grid-template-columns: minmax(0, 1fr); + } + .cockpit-metrics, .product-inventory-row, .queue-row,