From d0725c25e1d0cc623a87a1795f896c1b77302610 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Fri, 23 Oct 2026 01:51:12 +0200 Subject: [PATCH] fix(sources): open the AWS disconnect dialog from any source tab An AWS source's Delete opens the AWS disconnect dialog, but only the AWS card rendered it, and the card only shows on the Connection tab. On Overview or Settings the dialog opened in the model with nothing on the page. The source header now mounts it, as it already does for Google Cloud, and the card mounts it only on the integrations page. --- apps/web/src/page/integrations/embed.ts | 13 +++-- apps/web/src/page/integrations/view-aws.ts | 8 ++- .../page/sources/detail/detail.scene.test.ts | 55 ++++++++++++++++++- 3 files changed, 68 insertions(+), 8 deletions(-) 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(