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 2f1f73e537..ae0d93d498 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/gcp-infra", title: "Google Cloud infrastructure", diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index 4921d7102a..2fd7e53b1a 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -61,6 +61,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 LabGcpInfraRouteImport } from './routes/lab/gcp-infra' import { Route as LabGcpWorkloadRouteImport } from './routes/lab/gcp-workload' @@ -388,6 +389,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', @@ -776,6 +782,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/gcp-infra': typeof LabGcpInfraRoute '/lab/gcp-workload': typeof LabGcpWorkloadRoute @@ -895,6 +902,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/gcp-infra': typeof LabGcpInfraRoute '/lab/gcp-workload': typeof LabGcpWorkloadRoute @@ -1016,6 +1024,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/gcp-infra': typeof LabGcpInfraRoute '/lab/gcp-workload': typeof LabGcpWorkloadRoute @@ -1138,6 +1147,7 @@ export interface FileRouteTypes { | '/lab/flow' | '/lab/gcp' | '/lab/gcp-connect' + | '/lab/gcp-connections' | '/lab/gcp-dashboard' | '/lab/gcp-infra' | '/lab/gcp-workload' @@ -1257,6 +1267,7 @@ export interface FileRouteTypes { | '/lab/flow' | '/lab/gcp' | '/lab/gcp-connect' + | '/lab/gcp-connections' | '/lab/gcp-dashboard' | '/lab/gcp-infra' | '/lab/gcp-workload' @@ -1377,6 +1388,7 @@ export interface FileRouteTypes { | '/lab/flow' | '/lab/gcp' | '/lab/gcp-connect' + | '/lab/gcp-connections' | '/lab/gcp-dashboard' | '/lab/gcp-infra' | '/lab/gcp-workload' @@ -1905,6 +1917,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' @@ -2383,6 +2402,7 @@ interface LabRouteRouteChildren { LabFlowRoute: typeof LabFlowRoute LabGcpRoute: typeof LabGcpRoute LabGcpConnectRoute: typeof LabGcpConnectRoute + LabGcpConnectionsRoute: typeof LabGcpConnectionsRoute LabGcpDashboardRoute: typeof LabGcpDashboardRoute LabGcpInfraRoute: typeof LabGcpInfraRoute LabGcpWorkloadRoute: typeof LabGcpWorkloadRoute @@ -2423,6 +2443,7 @@ const LabRouteRouteChildren: LabRouteRouteChildren = { LabFlowRoute: LabFlowRoute, LabGcpRoute: LabGcpRoute, LabGcpConnectRoute: LabGcpConnectRoute, + LabGcpConnectionsRoute: LabGcpConnectionsRoute, LabGcpDashboardRoute: LabGcpDashboardRoute, LabGcpInfraRoute: LabGcpInfraRoute, LabGcpWorkloadRoute: LabGcpWorkloadRoute, 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 })