diff --git a/apps/web/src/page/integrations/embed.ts b/apps/web/src/page/integrations/embed.ts index d55ac3238..eeaf3ff49 100644 --- a/apps/web/src/page/integrations/embed.ts +++ b/apps/web/src/page/integrations/embed.ts @@ -9,6 +9,7 @@ import type { Message } from "./message" import type { Model } from "./model" import { init } from "./update" import { cardView, connectButton } from "./view" +import { awsDisconnectDialog } from "./view-aws" import { cloudflareReconnectButton } from "./view-cloudflare" import { gcpDialogs } from "./view-gcp-dialogs" @@ -42,7 +43,7 @@ export const cardEmbed = Submodel.defineView((model /** * A source's header: the Docs link, Connect while available, Cloudflare's Reconnect, and Google - * Cloud's removal dialogs (the source's Delete opens them). The source's own Delete disconnects. + * Cloud's and AWS's removal dialogs (the source's Delete opens them, on any tab). */ export const actionsEmbed = Submodel.defineView((model, inputs, h) => { const entry = catalogEntry(inputs.integration) @@ -52,10 +53,14 @@ export const actionsEmbed = Submodel.defineView((mo overview?.kind === "connected" && inputs.integration === "cloudflare" ? [cloudflareReconnectButton(h, model)] : [] - const dialogs = - inputs.integration === "gcp" && (inputs.focusId ?? null) !== null + const isFocused = (inputs.focusId ?? null) !== null + const dialogs = !isFocused + ? [] + : inputs.integration === "gcp" ? gcpDialogs(h, model, inputs.timeZone) - : [] + : inputs.integration === "aws" + ? [awsDisconnectDialog(h, model)] + : [] return h.div( [h.Class("flex shrink-0 items-center gap-3")], [ diff --git a/apps/web/src/page/integrations/view-aws.ts b/apps/web/src/page/integrations/view-aws.ts index e59e79805..0fdca8287 100644 --- a/apps/web/src/page/integrations/view-aws.ts +++ b/apps/web/src/page/integrations/view-aws.ts @@ -438,8 +438,10 @@ const removedPanel = (h: H, model: Model, focusId: string | null): ReadonlyArray ), ) -const disconnectConfirm = (h: H, model: Model, status: V2AwsIntegration): Html => { - const connection = status.connections.find((entry) => entry.id === model.awsCard.disconnectId) +/** Mounted once: by the card on the integrations page, by the header on a source page. */ +export const awsDisconnectDialog = (h: H, model: Model): Html => { + const status = Option.getOrNull(AsyncData.getData(model.statuses.aws.data)) + const connection = status?.connections.find((entry) => entry.id === model.awsCard.disconnectId) return confirmDialog(h, { slotId: "aws-disconnect", model: model.awsCard.disconnectDialog, @@ -487,7 +489,7 @@ const connectedView = ( ], ), ]), - disconnectConfirm(h, model, status), + ...(focusId === null ? [awsDisconnectDialog(h, model)] : []), ], ) } diff --git a/apps/web/src/page/sources/detail/detail.scene.test.ts b/apps/web/src/page/sources/detail/detail.scene.test.ts index 489b7a9c8..0c575e1a4 100644 --- a/apps/web/src/page/sources/detail/detail.scene.test.ts +++ b/apps/web/src/page/sources/detail/detail.scene.test.ts @@ -8,7 +8,17 @@ import { V2SourceUsage, } from "@maple/domain/http/v2" import { Option, Result, Schema } from "effect" -import { expect as expectView, given, Mount, role, scene, text, withViewInputs } from "foldkit/scene" +import { + all, + expect as expectView, + expectAll, + given, + Mount, + role, + scene, + text, + withViewInputs, +} from "foldkit/scene" import { describe, expect, test } from "vitest" import { ResolveThemeColors, ThemeMessage } from "../../../charts/theme-colors" import { toLoadFailure } from "../../../data/load-failure" @@ -728,6 +738,49 @@ describe("integration focus", () => { expect(clicked.model.connection?.awsCard.disconnectDialog.isOpen).toBe(true) }) + test("an AWS source's Delete shows the disconnect dialog from the Overview tab too", () => { + const connection = Schema.decodeSync(AwsConnectionId)("018f2b3c-4d5e-4f70-8192-a3b4c5d6e7f8") + const model = loaded( + source({ + platform: "aws", + kind: "integration", + key_prefix: null, + integration_id: encodePublicId(PublicIdPrefixes.awsConnection, connection), + }), + ) + const withStatus = pageUpdate( + model, + Message.GotConnectionMessage({ + message: IntegrationMessage.CompletedLoadAws({ + key: model.connection?.statuses.aws.key ?? "", + result: Result.succeed({ + object: "aws_integration", + available: true, + modules: [], + connections: [awsConnection(connection)], + }), + }), + }), + ).model + const name = "Disconnect AWS account 123456789012" + const mountDialog = Mount.resolve(Dialog.MountDialog, Dialog.Message.CompletedMountDialog()) + scene( + { update: pageUpdate, view }, + given(pageUpdate(withStatus, Message.ClickedDelete()).model), + resolveTheme, + mountDialog, + expectView(role("alertdialog", { name })).toExist(), + ) + // The Connection tab mounts it once, not once per card and header. + scene( + { update: pageUpdate, view }, + given(pageUpdate({ ...withStatus, tab: "connection" }, Message.ClickedDelete()).model), + resolveTheme, + mountDialog, + expectAll(all.role("alertdialog", { name })).toHaveCount(1), + ) + }) + test("disconnecting its own AWS connection leaves for the AWS page, where the cleanup waits", () => { const connection = Schema.decodeSync(AwsConnectionId)("018f2b3c-4d5e-4f70-8192-a3b4c5d6e7f8") const model = loaded(