From c59571f17ca47f9efe0fa32656d30ffbf5b65e46 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Mon, 13 Jul 2026 11:40:41 +0100 Subject: [PATCH] pre commit fix and other fixes --- frontend/.storybook/preview.tsx | 18 ++++++- .../public/locales/en-US/translation.toml | 14 ++--- .../editor/src/portal/api/processorFlow.ts | 8 +-- .../src/portal/components/ProcessorFlow.tsx | 12 ++--- .../components/processor-flow/FlowSankey.tsx | 52 ++++++++++++------- frontend/editor/src/portal/mocks/policies.ts | 37 +++++++++++-- .../src/portal/views/Infrastructure.tsx | 26 +++++++++- 7 files changed, 125 insertions(+), 42 deletions(-) diff --git a/frontend/.storybook/preview.tsx b/frontend/.storybook/preview.tsx index a0b1549f2a..d881ee066f 100644 --- a/frontend/.storybook/preview.tsx +++ b/frontend/.storybook/preview.tsx @@ -43,14 +43,30 @@ function parseEnTranslation(): Record { } } +const enTranslationResources = parseEnTranslation(); if (!i18next.isInitialized) { + // initImmediate: false → initialise synchronously from the inline resources + // (there's no async backend here), so i18next is ready before the first story + // renders. Without it the first render can beat init and stick on raw keys. void i18next.use(initReactI18next).init({ lng: "en", fallbackLng: "en", - resources: { en: { translation: parseEnTranslation() } }, + resources: { en: { translation: enTranslationResources } }, interpolation: { escapeValue: false }, react: { useSuspense: false }, + initImmediate: false, }); +} else { + // Something initialised i18next first (e.g. the app's async TOML backend): + // inject the shipped English copy synchronously so t() never renders raw keys. + i18next.addResourceBundle( + "en", + "translation", + enTranslationResources, + true, + true, + ); + if (i18next.language !== "en") void i18next.changeLanguage("en"); } // Start MSW once. Storybook runs in a browser so this uses the service worker. diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index 115c19c358..d6792c7d1f 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -7695,13 +7695,6 @@ failed = "Failed" heading = "Outcomes" success = "Delivered" -[portal.processorFlow.sankey] -waist = "Policies · {{n}} active" - -[portal.processorFlow.sankey.empty] -description = "Connect a source and switch on a policy to watch documents flow through the processor." -title = "No flow yet" - [portal.processorFlow.policies] activeCount = "{{n}} active" count = "{{n}} · 24h" @@ -7709,6 +7702,13 @@ heading = "Policies" setUp = "Set up" soon = "Soon" +[portal.processorFlow.sankey] +waist = "Policies · {{n}} active" + +[portal.processorFlow.sankey.empty] +description = "Connect a source and switch on a policy to watch documents flow through the processor." +title = "No flow yet" + [portal.processorFlow.sources] comingSoonTag = "Connect" editor = "Stirling PDF Editor" diff --git a/frontend/editor/src/portal/api/processorFlow.ts b/frontend/editor/src/portal/api/processorFlow.ts index 5c61ab7748..460f7e4de5 100644 --- a/frontend/editor/src/portal/api/processorFlow.ts +++ b/frontend/editor/src/portal/api/processorFlow.ts @@ -109,10 +109,10 @@ function buildPolicies( return POLICY_CATEGORIES.map((cat) => { const dp = decoded.find((p) => p.categoryId === cat.id); const configured = Boolean(dp?.enabled); - const state: FlowPolicyState = cat.comingSoon - ? "locked" - : configured - ? "active" + const state: FlowPolicyState = configured + ? "active" + : cat.comingSoon + ? "locked" : "off"; const runs24h = dp ? runs.filter((r) => r.policyId === dp.id && r.createdAt >= cutoff).length diff --git a/frontend/editor/src/portal/components/ProcessorFlow.tsx b/frontend/editor/src/portal/components/ProcessorFlow.tsx index e3e1c683f3..4b4a832ad3 100644 --- a/frontend/editor/src/portal/components/ProcessorFlow.tsx +++ b/frontend/editor/src/portal/components/ProcessorFlow.tsx @@ -54,6 +54,10 @@ export function ProcessorFlow() { `${toPortalPath(VIEW_PATHS.policies)}?setup=${encodeURIComponent(key)}`, ); + /** Deep-link to Infrastructure with the audit-log tab open. */ + const openAuditLog = () => + navigate(`${toPortalPath(VIEW_PATHS.infrastructure)}?tab=audit`); + const sources = data?.sources ?? []; const policies = data?.policies ?? []; const outcomes = data?.outcomes ?? []; @@ -140,11 +144,7 @@ export function ProcessorFlow() { ) : lens === "sankey" ? ( - + ) : (
@@ -168,7 +168,7 @@ export function ProcessorFlow() { setActiveView("infrastructure")} + onOpen={openAuditLog} />
diff --git a/frontend/editor/src/portal/components/processor-flow/FlowSankey.tsx b/frontend/editor/src/portal/components/processor-flow/FlowSankey.tsx index c9ffd0efc0..1d90705c2c 100644 --- a/frontend/editor/src/portal/components/processor-flow/FlowSankey.tsx +++ b/frontend/editor/src/portal/components/processor-flow/FlowSankey.tsx @@ -4,6 +4,7 @@ import { EmptyState } from "@app/ui"; import type { FlowOutcome, FlowOutcomeKey, + FlowPolicy, FlowSource, } from "@portal/api/processorFlow"; import { @@ -14,19 +15,18 @@ import { interface FlowSankeyProps { sources: FlowSource[]; outcomes: FlowOutcome[]; - activeCount: number; + policies: FlowPolicy[]; } /** * Sankey lens: sources → policies waist → outcomes, ribbon width ∝ 24h volume. - * Shows a friendly empty state when nothing has flowed yet. + * The waist splits into one segment per active policy. Shows a friendly empty + * state when nothing has flowed yet. */ -export function FlowSankey({ - sources, - outcomes, - activeCount, -}: FlowSankeyProps) { +export function FlowSankey({ sources, outcomes, policies }: FlowSankeyProps) { const { t } = useTranslation(); + const activePolicies = policies.filter((p) => p.state === "active"); + const activeCount = activePolicies.length; const flows = sources.filter((s) => s.docs24h > 0); const srcSum = flows.reduce((sum, s) => sum + s.docs24h, 0); @@ -145,19 +145,31 @@ export function FlowSankey({ accM += lt[i]; }); - // Waist node. - bars.push( - , - ); + // Waist: one segment per active policy (sized by its 24h runs) so the centre + // reads as distinct policies rather than a single bar. + const segGap = 4; + const nSeg = Math.max(activePolicies.length, 1); + const segAvail = midH - (nSeg - 1) * segGap; + const polSum = activePolicies.reduce((a, p) => a + p.runs24h, 0); + const segPolicies = activePolicies.length ? activePolicies : [null]; + let segY = midY; + segPolicies.forEach((p, i) => { + const wgt = p && polSum > 0 ? p.runs24h / polSum : 1 / nSeg; + const h = Math.max(2, segAvail * wgt); + bars.push( + , + ); + segY += h + segGap; + }); texts.push( + i < Math.round(total * 0.6) + ? "pol_security_default" + : "pol_ingestion_default"; // 40 successful runs spread across the last ~13h. const delivered = Array.from({ length: 40 }, (_, i) => ({ runId: `run_ok_${i}`, - policyId: "pol_security_default", + policyId: policyFor(i, 40), status: "COMPLETED" as const, currentStep: 2, stepCount: 2, @@ -62,7 +93,7 @@ export function seedPolicyRuns(): PolicyRunView[] { // 3 failures within the last few hours. const failed = Array.from({ length: 3 }, (_, i) => ({ runId: `run_fail_${i}`, - policyId: "pol_security_default", + policyId: policyFor(i, 3), status: "FAILED" as const, currentStep: 1, stepCount: 2, diff --git a/frontend/editor/src/portal/views/Infrastructure.tsx b/frontend/editor/src/portal/views/Infrastructure.tsx index bc0920382c..7d720f6895 100644 --- a/frontend/editor/src/portal/views/Infrastructure.tsx +++ b/frontend/editor/src/portal/views/Infrastructure.tsx @@ -1,4 +1,5 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; +import { useSearchParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { Button, Tabs, type TabItem } from "@app/ui"; import { useView } from "@portal/contexts/ViewContext"; @@ -18,10 +19,33 @@ type InfraTab = | "storage" | "audit"; +const INFRA_TABS: InfraTab[] = [ + "deployments", + "api-keys", + "security", + "models", + "storage", + "audit", +]; + export function Infrastructure() { const { t } = useTranslation(); const [tab, setTab] = useState("deployments"); const { setActiveView } = useView(); + const [searchParams, setSearchParams] = useSearchParams(); + + // Deep-link (?tab=) from elsewhere (e.g. the home visualiser's outcome + // cards → audit log): open that tab, then drop the param. + useEffect(() => { + const requested = searchParams.get("tab"); + if (!requested) return; + if ((INFRA_TABS as string[]).includes(requested)) { + setTab(requested as InfraTab); + } + const next = new URLSearchParams(searchParams); + next.delete("tab"); + setSearchParams(next, { replace: true }); + }, [searchParams, setSearchParams]); const tabs: TabItem[] = [ { key: "deployments", label: t("portal.infrastructure.tabs.deployments") },