From b187a0466f355e40cb015d281a675a0b2eb70e58 Mon Sep 17 00:00:00 2001 From: JeremyFunk Date: Fri, 16 Oct 2026 00:35:47 +0200 Subject: [PATCH] refactor(web): simpler Google Cloud connection rows A healthy connection is one row: its name, what it collects, and when logs and metrics last arrived. Anything else expands the card: one headline per thing to know or do, with its explanation and the card's one button, then Logs and Key metrics side by side with links to where the data shows up. - Configure moves into the row menu, beside View logs, View infrastructure and Disconnect. - The header drops the "Last log entry / Last metrics / Projects" facts row, which repeated the connections; the status line carries the project count. - Words in sans, ids and commands in mono. - /lab/gcp-connections shows each state over fixture connectors. --- .../integrations/gcp-integration-card.tsx | 761 ++++++++++-------- apps/web/src/lab/gcp-connections-lab.tsx | 111 +++ apps/web/src/lab/registry.ts | 8 + apps/web/src/routeTree.gen.ts | 21 + apps/web/src/routes/integrations.tsx | 7 - apps/web/src/routes/lab/gcp-connections.tsx | 5 + 6 files changed, 564 insertions(+), 349 deletions(-) create mode 100644 apps/web/src/lab/gcp-connections-lab.tsx create mode 100644 apps/web/src/routes/lab/gcp-connections.tsx diff --git a/apps/web/src/components/integrations/gcp-integration-card.tsx b/apps/web/src/components/integrations/gcp-integration-card.tsx index 15e43f5169..8b607d06a4 100644 --- a/apps/web/src/components/integrations/gcp-integration-card.tsx +++ b/apps/web/src/components/integrations/gcp-integration-card.tsx @@ -7,11 +7,12 @@ import type { V2GcpConnector } from "@maple/domain/http/v2" import { Badge } from "@maple/ui/components/ui/badge" import { Button } from "@maple/ui/components/ui/button" import { ConfirmDialog } from "@maple/ui/components/ui/confirm-dialog" -import { DropdownMenuItem } from "@maple/ui/components/ui/dropdown-menu" +import { DropdownMenuItem, DropdownMenuSeparator } from "@maple/ui/components/ui/dropdown-menu" import { Panel } from "@maple/ui/components/ui/panel" import { RowActionsMenu } from "@maple/ui/components/ui/row-actions-menu" import { Skeleton } from "@maple/ui/components/ui/skeleton" import { StatusDot } from "@maple/ui/components/ui/status-dot" +import { countLabel } from "@maple/ui/lib/format" import { TONE_TEXT, type Tone } from "@maple/ui/lib/tone" import { cn } from "@maple/ui/lib/utils" @@ -22,13 +23,14 @@ import { import { ErrorState } from "@/components/common/error-state" import { RelativeTime } from "@/components/common/relative-time" import { SectionHeading } from "@/components/common/section-heading" -import { AlertWarningIcon, GoogleCloudIcon, GoogleCloudMonoIcon } from "@/components/icons" +import { ArrowRightIcon, GoogleCloudIcon, GoogleCloudMonoIcon } from "@/components/icons" import { useIntervalRefresh } from "@/hooks/use-interval-refresh" import { useIsOrgAdmin } from "@/hooks/use-is-org-admin" import { useLiveClock } from "@/hooks/use-live-clock" import { useAsyncAction } from "@/hooks/use-mutation-action" import { Result, useAtom, useAtomRefresh, useAtomSet, useAtomValue } from "@/lib/effect-atom" import { showErrorToast } from "@/lib/error-toast" +import { encodeLogAttributeFilter } from "@/lib/logs/log-attribute-filters" import { retainedQuery } from "@/lib/services/common/atom-client" import { getActiveOrgId } from "@/lib/services/common/auth-headers" import { MapleApiV2AtomClient } from "@/lib/services/common/v2-atom-client" @@ -60,6 +62,7 @@ import { gcpRunAsked, gcpScopeLabel, gcpScriptNeeded, + gcpSetupRunning, gcpWorstState, type GcpCapability, } from "./gcp-connector-state" @@ -130,165 +133,327 @@ export function GcpMessage({ text }: { text: string }) { const ASK_ADMIN = "Ask a Maple organization admin to run the setup script." -/** - * What hangs under a connection's row when there is something to know or do: a plain hint, or a - * warning or failure. `run` is the way into the script: the card's one filled button for an admin, - * a sentence for everyone else. - */ -function Band({ - tone, - run, - children, -}: { - tone?: "warn" | "crit" - run?: { readonly label: string; readonly onRun: (() => void) | null } - children: React.ReactNode -}) { - return ( -
-
- {tone === undefined ? null : ( - - )} -
- {children} - {run?.onRun === null ?

{ASK_ADMIN}

: null} -
-
- {run === undefined || run.onRun === null ? null : ( - - )} -
+/** The card's one filled button, which runs the script; for everyone but admins, the sentence asking for it. */ +function RunScript({ label, onRun }: { label: string; onRun: (() => void) | null }) { + return onRun === null ? ( +

{ASK_ADMIN}

+ ) : ( + ) } -/** A band's headline. */ -function Headline({ children }: { children: React.ReactNode }) { - return

{children}

-} +const RETRIES = "Maple retries every 5 minutes. If this lasts an hour, write to support@maple.dev." -function Retries() { - return ( -

- Maple retries every 5 minutes. If this lasts an hour, write to{" "} - - support@maple.dev - - . -

- ) +/** Something to know or do about a connection: a sentence for a headline, what it means, the one thing to do. */ +interface Issue { + readonly key: string + readonly tone: "crit" | "warn" | "live" + readonly title: string + readonly body?: React.ReactNode + readonly action?: React.ReactNode } -/** One capability in a connection's row: its name, its state in a word or two, when it last delivered. */ -function Status({ - name, - status, - detail, - className, -}: { - name: string - /** A null tone is a capability that is off. */ - status: { readonly tone: Tone | null; readonly label: string } - detail?: React.ReactNode - className?: string -}) { +function IssueNote({ issue }: { issue: Issue }) { return ( -
- {name} - - {status.label} +
+ {/* As wide as the row's dot lane, so the headline lines up with the connection's name. */} + + - {detail === undefined || detail === null ? null : ( - {detail} - )} +
+

{issue.title}

+ {issue.body === undefined ? null : ( +
{issue.body}
+ )} + {issue.action === undefined ? null :
{issue.action}
} +
) } -const OFF = { tone: null, label: "Off" } as const +type LogState = ReturnType +type MetricsState = ReturnType /** - * What a connection needs its admin to do about the script: finish the setup, or apply a saved - * change. Both lead to the same place, the apply step of the configuration. + * What a connection needs said about it, worst first; empty when it is healthy. The card has one + * filled button: the script being due takes it, else the first failure another run repairs, else + * missing live queries. */ -function ScriptDue({ +function connectionIssues({ connector, - needed, + log, + metrics, nowMs, metricsAvailable, onRunScript, }: { connector: V2GcpConnector - needed: "setup-pending" | "changes-pending" + log: LogState + metrics: MetricsState nowMs: number metricsAvailable: boolean - /** Absent for non-admins. */ onRunScript: (() => void) | null -}) { +}): ReadonlyArray { + const needed = gcpScriptNeeded(connector, nowMs, metricsAvailable) + const repairs = needed === null ? gcpRunAsked(connector, nowMs)[0] : undefined + const rerun = (capability: GcpCapability) => + repairs === capability ? : undefined const cleanedUp = connector.applied_logs_enabled === false && connector.applied_metrics_enabled === false - const lines = - needed === "setup-pending" - ? [ - connector.setup_reported_at === null - ? "Google Cloud has nothing of Maple's yet. Running the setup script takes about a minute." - : "The last run stopped part of the way. Run the script again: it continues where it stopped.", - ] - : gcpPendingChanges(connector, nowMs, metricsAvailable) + const issues: Array = [] + + if (log.kind === "failing") { + issues.push({ + key: "logs-failing", + tone: "crit", + title: "Log forwarding is failing", + body: , + action: rerun("logs"), + }) + } + if (metrics.kind === "failing") { + issues.push({ + key: "metrics-failing", + tone: "crit", + title: "Key metrics can't be read", + body: , + action: rerun("metrics"), + }) + } + if (needed !== null) { + const lines = + needed === "setup-pending" + ? [ + connector.setup_reported_at === null + ? "Google Cloud has nothing of Maple's yet. Running the setup script takes about a minute." + : "The last run stopped part of the way. Run the script again: it continues where it stopped.", + ] + : gcpPendingChanges(connector, nowMs, metricsAvailable) + issues.push({ + key: "script", + tone: "warn", + title: needed === "setup-pending" ? "Setup pending" : "Changes not applied in Google Cloud", + body: ( + <> + {lines.length === 1 ? ( +

{lines[0]}

+ ) : ( +
    + {lines.map((line) => ( +
  • {line}
  • + ))} +
+ )} + {onRunScript !== null && needed === "changes-pending" && cleanedUp ? ( +

To remove the connection instead, choose Disconnect in its menu.

+ ) : null} + + ), + action: ( + + ), + }) + } + if (metrics.kind === "incomplete") { + issues.push({ + key: "metrics-incomplete", + tone: "warn", + title: "Key metrics are incomplete", + body: , + action: rerun("metrics"), + }) + } + if (metrics.kind === "stalled") { + issues.push({ key: "metrics-stalled", tone: "warn", title: "No metrics in 30 minutes", body: RETRIES }) + } + // With key metrics on, the poller's own error says this. + if (!connector.metrics_enabled && gcpLiveQueriesMissing(connector, nowMs, metricsAvailable)) { + issues.push({ + key: "live-queries", + tone: "warn", + title: "Live queries not set up", + body: "The last setup run couldn't create the read-only service account, so Maple can't query this connection's metrics. Run the script again and fix what it names under Read-only access.", + action: + needed === null && repairs === undefined ? ( + + ) : undefined, + }) + } + if (log.kind === "waiting" && log.overdue) { + issues.push({ + key: "logs-overdue", + tone: "warn", + title: "No log entry in 20 minutes", + body: "Nothing that passes the filter was logged, or the sink can't publish. Write a test entry. If it isn't here within a minute, run the setup script again.", + action: ( + + {`gcloud logging write maple-test "hello from Maple" --project=${connector.project_id}`} + + ), + }) + } + if (metrics.kind === "waiting" && metrics.overdue) { + issues.push({ key: "metrics-overdue", tone: "warn", title: "No metrics have arrived yet", body: RETRIES }) + } + if (metrics.kind === "receiving" && metrics.resourcesError !== null) { + issues.push({ + key: "resources", + tone: "warn", + title: "Resource list incomplete", + body: , + }) + } + if (issues.length > 0) return issues + + // Nothing wrong yet: what the connection is waiting for. + if (gcpSetupRunning(connector, nowMs, metricsAvailable)) { + return [ + { + key: "running", + tone: "live", + title: "The setup script is still running", + body: "Logs and metrics start once it has finished.", + }, + ] + } + if (log.kind === "waiting" || metrics.kind === "waiting") { + return [ + { + key: "waiting", + tone: "live", + title: "Waiting for the first data", + body: + log.kind === "waiting" + ? "Google Cloud usually starts sending within 10 minutes. Logs written before then aren't forwarded." + : "The first metrics arrive within about 10 minutes.", + }, + ] + } + return [] +} + +/** A capability's state in a word or two, with when it last delivered. */ +function signalText( + capability: GcpCapability, + log: LogState, + metrics: MetricsState, +): { readonly tone: Tone; readonly text: React.ReactNode } { + if (capability === "logs") { + switch (log.kind) { + case "off": + return { tone: "neutral", text: "Off" } + case "failing": + return { + tone: "crit", + text: + log.lastLogReceivedAt === null ? ( + "Rejected" + ) : ( + + ), + } + case "setup-pending": + case "setup-running": + return { tone: "neutral", text: GCP_LOG_STATUS[log.kind].label } + case "waiting": + return log.overdue + ? { tone: "warn", text: "No entries in 20 minutes" } + : { tone: "neutral", text: "No entries yet" } + case "idle": + return { + tone: "neutral", + text: , + } + case "receiving": + return { tone: "ok", text: } + } + } + switch (metrics.kind) { + case "off": + return { tone: "neutral", text: "Off" } + case "setup-pending": + case "setup-running": + return { tone: "neutral", text: GCP_METRICS_STATUS[metrics.kind].label } + case "waiting": + return { tone: metrics.overdue ? "warn" : "neutral", text: "No reads yet" } + case "failing": + return { + tone: "crit", + text: + metrics.lastMetricsReceivedAt === null ? ( + "Can't be read" + ) : ( + + ), + } + case "incomplete": + case "stalled": + return { + tone: "warn", + text: ( + + ), + } + case "receiving": + return { tone: "ok", text: } + } +} + +const SIGNAL_LINK = "inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground" + +/** The logs this connection forwarded: ingest stamps each with the connection's id. */ +const logsSearch = (connector: V2GcpConnector) => ({ + attrs: [ + encodeLogAttributeFilter({ + source: "resource", + key: "maple_gcp_connector_id", + value: connector.id, + negated: false, + }), + ], +}) + +/** The infrastructure page, narrowed to the project when the connection is one. */ +const infraSearch = (connector: V2GcpConnector) => + connector.scope_type === "project" ? { project: connector.scope_id } : {} + +/** One capability in an expanded card: its name, its state, and where its data shows up once it does. */ +function Signal({ + name, + signal, + link, +}: { + name: string + signal: { readonly tone: Tone; readonly text: React.ReactNode } + link?: React.ReactNode +}) { return ( - - - {needed === "setup-pending" ? "Setup pending" : "Changes not applied in Google Cloud"} - - {lines.length === 1 ? ( -

{lines[0]}

- ) : ( -
    - {lines.map((line) => ( -
  • {line}
  • - ))} -
- )} - {onRunScript !== null && needed === "changes-pending" && cleanedUp ? ( -

- To remove the connection instead, choose Disconnect in its menu. -

- ) : null} -
+
+ {name} + {signal.text} + {link === undefined ? null :
{link}
} +
+ ) +} + +/** A column of the compact row: a label over a time. */ +function Fact({ label, children }: { label: string; children: React.ReactNode }) { + return ( +
+ {label} + {children} +
) } -function GcpConnection({ +export function GcpConnection({ connector, isAdmin, nowMs, @@ -336,172 +501,126 @@ function GcpConnection({ const log = gcpLogState(connector, nowMs) const metrics = gcpMetricsState(connector, nowMs) - const resourcesError = metrics.kind === "receiving" ? metrics.resourcesError : null - const state = gcpConnectionState(connector, nowMs, metricsAvailable) - const tone: Tone = - log.kind === "failing" || metrics.kind === "failing" - ? "crit" - : state === "healthy" - ? resourcesError === null - ? "ok" - : "warn" - : state === "waiting" - ? "neutral" - : "warn" - const onRunScript = isAdmin ? () => onConfigure("apply") : null - const needed = gcpScriptNeeded(connector, nowMs, metricsAvailable) - // The card has one filled button. The script being due takes it; else the first failure that - // another run repairs does. - const repairs = needed === null ? gcpRunAsked(connector, nowMs)[0] : undefined - const rerun = (capability: GcpCapability) => - repairs === capability ? { label: "Run the script again", onRun: onRunScript } : undefined - const lastRead = - "lastMetricsReceivedAt" in metrics && metrics.lastMetricsReceivedAt !== null ? ( - - ) : undefined + const issues = connectionIssues({ + connector, + log, + metrics, + nowMs, + metricsAvailable, + onRunScript: isAdmin ? () => onConfigure("apply") : null, + }) + const lastLog = log.kind === "receiving" || log.kind === "idle" ? log.lastLogReceivedAt : null + const lastRead = "lastMetricsReceivedAt" in metrics ? metrics.lastMetricsReceivedAt : null + const projects = + metrics.kind === "receiving" && metrics.projectCount !== null + ? countLabel(metrics.projectCount, "project") + : null + const collects = [connector.logs_enabled ? "Logs" : null, connector.metrics_enabled ? "key metrics" : null] + .filter((part) => part !== null) + .join(" and ") + + const name = ( +
+

+ {connector.scope_id} +

+ + {GCP_SCOPE_NAMES[connector.scope_type]} + +
+ ) + const subtitle = [ + collects === "" ? "Collects nothing" : collects.charAt(0).toUpperCase() + collects.slice(1), + projects, + connector.scope_type === "project" ? null : `host ${connector.project_id}`, + ] + .filter((part) => part !== null) + .join(" · ") + const menu = ( + + {isAdmin ? onConfigure("choose")}>Configure : null} + }>View logs + }> + View infrastructure + + {isAdmin ? ( + <> + + setDisconnectOpen(true)}> + Disconnect + + + ) : null} + + ) return ( - {/* One line where the card is wide: the name, a lane for each capability, the actions. Narrower, the capabilities go under the name. */} -
-
- {/* As wide as a band's icon, so the name, the wrapped statuses and a band's text share a lane. */} - - - -

- {connector.scope_id} -

- - {GCP_SCOPE_NAMES[connector.scope_type]} - - {connector.scope_type === "project" ? null : ( - - host {connector.project_id} + {issues.length === 0 ? ( + // Healthy: one row, the name and the times its data last arrived. +
+
+ + - )} -
-
- - ) : log.kind === "failing" && log.lastLogReceivedAt !== null ? ( - - ) : undefined - } - /> - -
- {isAdmin ? ( -
- - - setDisconnectOpen(true)}> - Disconnect - - +
+ {name} +

{subtitle}

+
- ) : null} -
- - {needed === null ? null : ( - - )} - {/* With key metrics on, the poller's own error says this. */} - {!connector.metrics_enabled && gcpLiveQueriesMissing(connector, nowMs, metricsAvailable) ? ( - - Live queries not set up -

- The last setup run couldn't create the read-only service account, so Maple - can't query this connection's metrics. Run the script again and fix what it - names under Read-only access. -

-
- ) : null} - {log.kind === "failing" ? ( - - - - ) : log.kind !== "waiting" ? null : log.overdue ? ( - - No log entry in 20 minutes -

- Nothing that passes the filter was logged, or the sink can't publish. Write a - test entry. If it isn't here within a minute, open Configure and run the setup - script again. -

-
- - {`gcloud logging write maple-test "hello from Maple" --project=${connector.project_id}`} - +
+ + {lastLog === null ? "Off" : } + + + {lastRead === null ? "Off" : } +
- + {menu} +
) : ( - -

- A new sink takes about 10 minutes to start forwarding. Entries logged before that are - not forwarded. -

-
- )} - {metrics.kind === "failing" ? ( - - - - ) : metrics.kind === "incomplete" ? ( - - - - ) : metrics.kind === "stalled" ? ( - - No metrics in 30 minutes - - - ) : metrics.kind === "waiting" ? ( - - {metrics.overdue ? ( - <> - No metrics have arrived yet - - - ) : ( -

The first metrics arrive within about 10 minutes.

- )} -
- ) : resourcesError === null ? null : ( - - - + <> +
+
+
+ {name} + {connector.scope_type === "project" ? null : ( +

{subtitle}

+ )} +
+
{menu}
+
+ {issues.map((issue) => ( + + ))} +
+
+ + View logs + + + ) + } + /> + + View infrastructure + + + ) + } + /> +
+ )} {neverReported ? ( @@ -538,14 +657,6 @@ function GcpConnection({ ) } -/** The newest of the times, null when none has arrived yet. */ -const latest = (times: ReadonlyArray): string | null => - times.reduce( - (newest, at) => - at !== null && (newest === null || Date.parse(at) > Date.parse(newest)) ? at : newest, - null, - ) - /** Every connection, and the clock their states turn on. */ function useGcpConnectors() { const status = Option.getOrNull(AsyncResult.value(useAtomValue(gcpStatusQuery()))) @@ -556,59 +667,24 @@ function useGcpConnectors() { } } -/** The page header's state: the worst connection's, and how many of several share it. */ +/** The page header's state: the worst connection's, how many of several share it, and how many projects send data. */ export function GcpHeaderStatus() { const { connectors, metricsAvailable, nowMs } = useGcpConnectors() const states = connectors.map((connector) => gcpConnectionState(connector, nowMs, metricsAvailable)) const worst = gcpWorstState(states) const inWorst = states.filter((state) => state === worst).length - return ( -
- - {GCP_CONNECTION_LABEL[worst]} - {inWorst < connectors.length ? ` · ${inWorst} of ${connectors.length} connections` : null} -
- ) -} - -/** The page header's facts: when each kind of data last arrived, and how many projects send it. */ -export function GcpHeaderFacts() { - const { connectors } = useGcpConnectors() - const logs = connectors.filter((connector) => connector.logs_enabled) - const metrics = connectors.filter((connector) => connector.metrics_enabled) const projects = connectors.reduce( (sum, connector) => sum + (connector.scope_type === "project" ? 1 : connector.discovered_project_count), 0, ) - const when = (times: ReadonlyArray) => { - const at = latest(times) - return at === null ? "none yet" : - } - const facts = [ - logs.length === 0 - ? null - : { - label: "Last log entry", - value: when(logs.map((connector) => connector.last_log_received_at)), - }, - metrics.length === 0 - ? null - : { - label: "Last metrics", - value: when(metrics.map((connector) => connector.last_metrics_received_at)), - }, - projects === 0 ? null : { label: "Projects", value: projects }, - ].filter((fact) => fact !== null) return ( -
- {facts.map((fact) => ( -
-
{fact.label}
-
{fact.value}
-
- ))} -
+
+ + {GCP_CONNECTION_LABEL[worst]} + {inWorst < connectors.length ? ` · ${inWorst} of ${connectors.length} connections` : null} + {projects > 1 ? ` · ${countLabel(projects, "project")}` : null} +
) } @@ -773,7 +849,8 @@ export function GcpIntegrationCard() {
{cleanup} -
+ {/* Sans for words; ids, times and commands set their own mono. */} +
1 ? connectors.length : undefined} diff --git a/apps/web/src/lab/gcp-connections-lab.tsx b/apps/web/src/lab/gcp-connections-lab.tsx new file mode 100644 index 0000000000..e9752555f9 --- /dev/null +++ b/apps/web/src/lab/gcp-connections-lab.tsx @@ -0,0 +1,111 @@ +import { Schema } from "effect" +import { V2GcpConnector } from "@maple/domain/http/v2" + +import { GcpConnection } from "@/components/integrations/gcp-integration-card" +import { DashboardLayout } from "@/components/layout/dashboard-layout" + +const NOW = Date.now() +const ago = (minutes: number) => new Date(NOW - minutes * 60_000).toISOString() + +/** A project connection whose script ran an hour ago and that delivers both. */ +const connector = (over: Partial = {}): V2GcpConnector => + Schema.decodeUnknownSync(V2GcpConnector)({ + id: "gcpc_YofPTrK9782DWwcnXhpcCw", + object: "gcp_connector", + scope_type: "project", + scope_id: "maple-development-project", + project_id: "maple-development-project", + logs_enabled: true, + metrics_enabled: true, + created_at: ago(90), + last_log_received_at: ago(1), + last_log_error: null, + applied_logs_enabled: true, + applied_metrics_enabled: true, + setup_reported_at: ago(60), + last_metrics_received_at: ago(4), + last_metrics_error: null, + discovered_project_count: 1, + last_resources_error: null, + ...over, + }) + +const SCENARIOS: ReadonlyArray<{ title: string; connector: V2GcpConnector }> = [ + { title: "Healthy project", connector: connector() }, + { + title: "Healthy folder", + connector: connector({ + scope_type: "folder", + scope_id: "482910374615", + project_id: "maple-shared-logging", + discovered_project_count: 13, + last_log_received_at: ago(0.2), + last_metrics_received_at: ago(2), + }), + }, + { + title: "Waiting for the first data", + connector: connector({ + setup_reported_at: ago(3), + last_log_received_at: null, + last_metrics_received_at: null, + }), + }, + { + title: "Overdue", + connector: connector({ + setup_reported_at: ago(24), + last_log_received_at: null, + last_metrics_received_at: null, + }), + }, + { + title: "Log forwarding failing", + connector: connector({ + last_log_received_at: ago(120), + last_log_error: + "The log sink can't publish to Maple's topic. Run the setup script again: it restores the permission. Google answered: PERMISSION_DENIED: pubsub.topics.publish.", + last_metrics_received_at: ago(2), + }), + }, + { + title: "Setup pending", + connector: connector({ + applied_logs_enabled: null, + applied_metrics_enabled: null, + setup_reported_at: null, + last_log_received_at: null, + last_metrics_received_at: null, + }), + }, +] + +/** Each state of a Google Cloud connection over fixture data, as an admin sees it. */ +export function GcpConnectionsLab() { + return ( + + + + + +
+ {SCENARIOS.map((scenario) => ( +
+

{scenario.title}

+ undefined} + onRemoved={() => undefined} + /> +
+ ))} +
+
+
+
+
+ ) +} diff --git a/apps/web/src/lab/registry.ts b/apps/web/src/lab/registry.ts index b1390117c6..77ede0b775 100644 --- a/apps/web/src/lab/registry.ts +++ b/apps/web/src/lab/registry.ts @@ -68,6 +68,14 @@ export const LAB_ENTRIES: ReadonlyArray = [ kind: "lab", session: "none", }, + { + path: "/lab/gcp-connections", + title: "Google Cloud connections", + description: + "A Google Cloud connection in each state, healthy to failing, over fixture connectors, no API needed.", + kind: "lab", + session: "none", + }, { path: "/lab/onboarding", title: "Onboarding", diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index 8850a25ae8..176b557a20 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -60,6 +60,7 @@ import { Route as LabErrorsRouteImport } from './routes/lab/errors' import { Route as LabFlowRouteImport } from './routes/lab/flow' import { Route as LabGcpRouteImport } from './routes/lab/gcp' import { Route as LabGcpConnectRouteImport } from './routes/lab/gcp-connect' +import { Route as LabGcpConnectionsRouteImport } from './routes/lab/gcp-connections' import { Route as LabGcpDashboardRouteImport } from './routes/lab/gcp-dashboard' import { Route as LabInfraRouteImport } from './routes/lab/infra' import { Route as LabLoadersRouteImport } from './routes/lab/loaders' @@ -380,6 +381,11 @@ const LabGcpConnectRoute = LabGcpConnectRouteImport.update({ path: '/gcp-connect', getParentRoute: () => LabRouteRoute, } as any) +const LabGcpConnectionsRoute = LabGcpConnectionsRouteImport.update({ + id: '/gcp-connections', + path: '/gcp-connections', + getParentRoute: () => LabRouteRoute, +} as any) const LabGcpDashboardRoute = LabGcpDashboardRouteImport.update({ id: '/gcp-dashboard', path: '/gcp-dashboard', @@ -757,6 +763,7 @@ export interface FileRoutesByFullPath { '/lab/flow': typeof LabFlowRoute '/lab/gcp': typeof LabGcpRoute '/lab/gcp-connect': typeof LabGcpConnectRoute + '/lab/gcp-connections': typeof LabGcpConnectionsRoute '/lab/gcp-dashboard': typeof LabGcpDashboardRoute '/lab/infra': typeof LabInfraRoute '/lab/loaders': typeof LabLoadersRoute @@ -873,6 +880,7 @@ export interface FileRoutesByTo { '/lab/flow': typeof LabFlowRoute '/lab/gcp': typeof LabGcpRoute '/lab/gcp-connect': typeof LabGcpConnectRoute + '/lab/gcp-connections': typeof LabGcpConnectionsRoute '/lab/gcp-dashboard': typeof LabGcpDashboardRoute '/lab/infra': typeof LabInfraRoute '/lab/loaders': typeof LabLoadersRoute @@ -991,6 +999,7 @@ export interface FileRoutesById { '/lab/flow': typeof LabFlowRoute '/lab/gcp': typeof LabGcpRoute '/lab/gcp-connect': typeof LabGcpConnectRoute + '/lab/gcp-connections': typeof LabGcpConnectionsRoute '/lab/gcp-dashboard': typeof LabGcpDashboardRoute '/lab/infra': typeof LabInfraRoute '/lab/loaders': typeof LabLoadersRoute @@ -1110,6 +1119,7 @@ export interface FileRouteTypes { | '/lab/flow' | '/lab/gcp' | '/lab/gcp-connect' + | '/lab/gcp-connections' | '/lab/gcp-dashboard' | '/lab/infra' | '/lab/loaders' @@ -1226,6 +1236,7 @@ export interface FileRouteTypes { | '/lab/flow' | '/lab/gcp' | '/lab/gcp-connect' + | '/lab/gcp-connections' | '/lab/gcp-dashboard' | '/lab/infra' | '/lab/loaders' @@ -1343,6 +1354,7 @@ export interface FileRouteTypes { | '/lab/flow' | '/lab/gcp' | '/lab/gcp-connect' + | '/lab/gcp-connections' | '/lab/gcp-dashboard' | '/lab/infra' | '/lab/loaders' @@ -1862,6 +1874,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LabGcpConnectRouteImport parentRoute: typeof LabRouteRoute } + '/lab/gcp-connections': { + id: '/lab/gcp-connections' + path: '/gcp-connections' + fullPath: '/lab/gcp-connections' + preLoaderRoute: typeof LabGcpConnectionsRouteImport + parentRoute: typeof LabRouteRoute + } '/lab/gcp-dashboard': { id: '/lab/gcp-dashboard' path: '/gcp-dashboard' @@ -2325,6 +2344,7 @@ interface LabRouteRouteChildren { LabFlowRoute: typeof LabFlowRoute LabGcpRoute: typeof LabGcpRoute LabGcpConnectRoute: typeof LabGcpConnectRoute + LabGcpConnectionsRoute: typeof LabGcpConnectionsRoute LabGcpDashboardRoute: typeof LabGcpDashboardRoute LabInfraRoute: typeof LabInfraRoute LabLoadersRoute: typeof LabLoadersRoute @@ -2362,6 +2382,7 @@ const LabRouteRouteChildren: LabRouteRouteChildren = { LabFlowRoute: LabFlowRoute, LabGcpRoute: LabGcpRoute, LabGcpConnectRoute: LabGcpConnectRoute, + LabGcpConnectionsRoute: LabGcpConnectionsRoute, LabGcpDashboardRoute: LabGcpDashboardRoute, LabInfraRoute: LabInfraRoute, LabLoadersRoute: LabLoadersRoute, diff --git a/apps/web/src/routes/integrations.tsx b/apps/web/src/routes/integrations.tsx index 67b0d08e68..36f2602dab 100644 --- a/apps/web/src/routes/integrations.tsx +++ b/apps/web/src/routes/integrations.tsx @@ -10,7 +10,6 @@ import { CloudflareHeaderActions, } from "@/components/integrations/cloudflare-account-card" import { - GcpHeaderFacts, GcpHeaderStatus, GcpIntegrationCard, } from "@/components/integrations/gcp-integration-card" @@ -319,12 +318,6 @@ function IntegrationHeader({ integration }: { integration: IntegrationId }) { ) : null}
- {/* Under the logo, the facts behind the state line. */} - {connected && integration === "gcp" ? ( -
- -
- ) : null}
) } diff --git a/apps/web/src/routes/lab/gcp-connections.tsx b/apps/web/src/routes/lab/gcp-connections.tsx new file mode 100644 index 0000000000..dc796111c5 --- /dev/null +++ b/apps/web/src/routes/lab/gcp-connections.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from "@tanstack/react-router" + +import { GcpConnectionsLab } from "@/lab/gcp-connections-lab" + +export const Route = createFileRoute("/lab/gcp-connections")({ component: GcpConnectionsLab })