Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 78 additions & 4 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<DriverContextView | null>(null);
const [testingView, setTestingView] = useState<DriverContextView | null>(
null,
Expand Down Expand Up @@ -386,19 +391,39 @@ 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,
environment.environment,
).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) => {
Expand All @@ -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,
);
Expand Down Expand Up @@ -518,6 +554,8 @@ export function App() {
product={selectedProductOverview}
selected={selected}
loading={loading}
selectedEnvironment={activeEnvironment?.environment ?? selectedEnvironment}
onSelectEnvironment={setSelectedEnvironment}
/>
<section className="lane-grid" aria-busy={loading}>
<LanePanel
Expand Down Expand Up @@ -930,6 +968,17 @@ function EvidenceDetailDrawer({
);
}

function configStatusErrorMessage(environment: string, apiError: unknown): string {
const prefix = `${environment} config status`;
if (apiError instanceof LaunchplaneApiError) {
return `${prefix}: ${apiError.message}`;
}
if (apiError instanceof Error) {
return `${prefix}: ${apiError.message}`;
}
return `${prefix}: request failed.`;
}

function StateFixtureGallery({
actions,
}: {
Expand All @@ -942,6 +991,7 @@ function StateFixtureGallery({
useState<WorkGraphFilter>("all");
const [fixtureWorkGraphMode, setFixtureWorkGraphMode] =
useState<WorkGraphMode>("all");
const [fixtureEnvironment, setFixtureEnvironment] = useState("prod");
const readyProd = fixtureLane({
instance: "prod",
artifact: "ghcr.io/every/verireel@sha256:11112222",
Expand Down Expand Up @@ -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",
Expand All @@ -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[] = [
Expand Down Expand Up @@ -1300,6 +1365,15 @@ function StateFixtureGallery({
lane={readyProd}
/>
</div>
<div className="fixture-wide">
<ProductOverviewShell
product={fixtureProducts[0]}
selected={choiceFromProductOverview(fixtureProducts[0])}
loading={false}
selectedEnvironment={fixtureEnvironment}
onSelectEnvironment={setFixtureEnvironment}
/>
</div>
<div className="fixture-wide">
<EvidenceTimeline
prod={readyProd}
Expand Down
83 changes: 80 additions & 3 deletions frontend/src/ProductOverviewShell.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AlertTriangle, Database } from "lucide-react";
import { AlertTriangle, Database, ExternalLink } from "lucide-react";

import { KeyValue, PanelHead } from "./panel-ui";
import { SkeletonRows, StateBlock, StatusPill } from "./status-ui";
Expand All @@ -11,12 +11,20 @@ export function ProductOverviewShell({
product,
selected,
loading,
selectedEnvironment,
onSelectEnvironment,
}: {
product: ProductSiteOverview | null;
selected: DriverChoice;
loading: boolean;
selectedEnvironment: string;
onSelectEnvironment: (environment: string) => void;
}) {
const environments = product?.environments ?? [];
const activeEnvironment =
environments.find(
(environment) => environment.environment === selectedEnvironment,
) ?? environments[0];
const enabledActions = product?.available_actions.filter(
(action) => action.enabled,
);
Expand Down Expand Up @@ -63,15 +71,20 @@ export function ProductOverviewShell({
<div className="product-environment-strip">
{environments.length ? (
environments.map((environment) => (
<div
<button
type="button"
className="product-environment-pill"
key={`${environment.environment}:${environment.context}`}
data-environment={environment.environment}
data-selected={
activeEnvironment?.environment === environment.environment
}
onClick={() => onSelectEnvironment(environment.environment)}
>
<span>{environment.environment}</span>
<strong>{freshnessLabel(environment.trust_state)}</strong>
<code>{environment.context}</code>
</div>
</button>
))
) : (
<StateBlock
Expand Down Expand Up @@ -103,6 +116,9 @@ export function ProductOverviewShell({
</div>
</div>
)}
{!loading && activeEnvironment ? (
<EnvironmentDetailStrip environment={activeEnvironment} />
) : null}
{product?.warnings.length ? (
<div className="overview-warning-list">
{product.warnings.map((warning) => (
Expand All @@ -116,3 +132,64 @@ export function ProductOverviewShell({
</section>
);
}

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 (
<div className="environment-detail-strip" data-environment={environment.environment}>
<div className="environment-detail-identity">
<span className={`lane-chip lane-chip-${environment.environment}`}>
{environment.environment}
</span>
<code>{environment.context}</code>
{environment.base_url ? (
<a href={environment.base_url} target="_blank" rel="noreferrer">
<ExternalLink size={13} aria-hidden="true" />
{environment.base_url}
</a>
) : null}
</div>
<div className="environment-detail-facts">
<KeyValue
label="Health URL"
value={environment.health_url}
mono
muted={!environment.health_url}
/>
<KeyValue
label="Enabled lane actions"
value={`${enabledActions.length} enabled`}
status={enabledActions.length ? "pass" : "unknown"}
/>
<KeyValue
label="Blocked lane actions"
value={`${blockedActions.length} blocked`}
status={blockedActions.length ? "blocked" : "pass"}
/>
</div>
{environment.available_actions.length ? (
<div className="environment-action-strip">
{environment.available_actions.slice(0, 4).map((action) => (
<span
className="environment-action-chip"
data-enabled={action.enabled}
key={action.action_id}
title={action.disabled_reasons.join("; ") || action.description}
>
{action.label}
</span>
))}
</div>
) : null}
</div>
);
}
Loading
Loading