@@ -467,6 +525,46 @@ function drawConnectors(loading = false): void {
${meta.desc ? html`${meta.desc}
` : ""}
${needsReconnect && p.refreshError ? html`Refresh failed: ${p.refreshError}
` : ""}
+ ${
+ id === "google" && googleAccounts.length
+ ? html`
+
Connected accounts
+ ${googleAccounts.map(
+ (account) =>
+ html`
+
+
${googleAccountLabel(account.accountType)}
+
+ ${account.needsReconnect ? "Reconnect required" : "Gmail, Calendar, Drive, and files"}
+
+
+
+ ${
+ account.needsReconnect
+ ? html``
+ : ""
+ }
+
+
+
`,
+ )}
+
`
+ : ""
+ }
${
grants.length
? html`
@@ -493,8 +591,20 @@ function drawConnectors(loading = false): void {
: ""
}
- ${available ? html`` : ""}
- ${connected || needsReconnect ? html`` : ""}
+ ${connectorAction}
+ ${
+ id !== "google" && (connected || needsReconnect)
+ ? html``
+ : ""
+ }
`;
@@ -770,12 +880,13 @@ async function createDrop(): Promise
{
}
}
-async function startConnector(provider: string): Promise {
+async function startConnector(provider: string, accountType?: GoogleAccountType): Promise {
const stateEpoch = keychainOperations.captureEpoch();
connectorNotice = "";
try {
const r = await api<{ authorizeUrl?: string }>(`/api/connectors/${encodeURIComponent(provider)}/start`, {
method: "POST",
+ body: JSON.stringify(accountType ? { accountType } : {}),
});
if (!keychainOperations.isCurrentEpoch(stateEpoch)) return;
if (r.authorizeUrl) {
@@ -790,13 +901,17 @@ async function startConnector(provider: string): Promise {
drawConnectors(false);
}
-async function revokeConnector(provider: string): Promise {
+async function revokeConnector(provider: string, accountType?: GoogleAccountType): Promise {
const hosts = new Set(
(connectorProviders[provider]?.hosts ?? [])
.map((entry) => (typeof entry === "string" ? entry : entry.host))
.filter((host): host is string => Boolean(host)),
);
- const providerCredentials = keychainConnectorCredentials.filter((credential) => hosts.has(credential.host));
+ const providerCredentials = keychainConnectorCredentials.filter(
+ (credential) =>
+ hosts.has(credential.host) &&
+ (accountType === undefined || (credential.accountType ?? "default") === accountType),
+ );
const credentialIds = new Set(providerCredentials.map((credential) => credential.credentialId));
const credentialsById = new Map(
providerCredentials.map((credential) => [
@@ -812,7 +927,7 @@ async function revokeConnector(provider: string): Promise {
: "";
confirmationOpener = document.activeElement instanceof HTMLElement ? document.activeElement : null;
confirmation = {
- title: `Disconnect ${CONNECTOR_LABELS[provider]?.name ?? provider}?`,
+ title: `Disconnect ${accountType ? googleAccountLabel(accountType) : (CONNECTOR_LABELS[provider]?.name ?? provider)}?`,
body: `${impact} Automations using this account may stop working.`.trim(),
action: "Disconnect account",
run: async () => {
@@ -822,7 +937,7 @@ async function revokeConnector(provider: string): Promise {
confirmationOpener = null;
drawConnectors();
try {
- await performRevokeConnector(provider, operation.epoch);
+ await performRevokeConnector(provider, operation.epoch, accountType);
} finally {
if (keychainOperations.finishMutation(operation)) drawConnectors();
}
@@ -831,10 +946,17 @@ async function revokeConnector(provider: string): Promise {
drawConnectors();
}
-async function performRevokeConnector(provider: string, stateEpoch: number): Promise {
+async function performRevokeConnector(
+ provider: string,
+ stateEpoch: number,
+ accountType?: GoogleAccountType,
+): Promise {
connectorNotice = "";
try {
- await api("/api/connectors/revoke", { method: "POST", body: JSON.stringify({ provider }) });
+ await api("/api/connectors/revoke", {
+ method: "POST",
+ body: JSON.stringify({ provider, ...(accountType ? { accountType } : {}) }),
+ });
} catch (e) {
if (keychainOperations.isCurrentEpoch(stateEpoch)) connectorNotice = errMessage(e, "Could not disconnect.");
}
diff --git a/plugins/web-ui/src/shell.css b/plugins/web-ui/src/shell.css
index dad84b7a8..611c17505 100644
--- a/plugins/web-ui/src/shell.css
+++ b/plugins/web-ui/src/shell.css
@@ -3772,6 +3772,12 @@ a.chat-row-open {
.kc-access-row + .kc-access-row {
border-top: 1px solid var(--kc-line);
}
+.kc-account-actions {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ flex: 0 0 auto;
+}
.kc-credential-facts {
display: flex;
align-items: center;
@@ -3906,6 +3912,19 @@ a.chat-row-open {
width: 100%;
justify-content: flex-end;
}
+ .kc-access-row {
+ align-items: flex-start;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+ .kc-account-actions {
+ flex-wrap: wrap;
+ width: 100%;
+ }
+ .kc-account-actions .kc-text-action {
+ min-height: 32px;
+ padding: 5px 0;
+ }
.kc-summary {
display: grid;
grid-template-columns: 1fr 1fr;
diff --git a/plugins/web-ui/test/api-body-parsing.test.ts b/plugins/web-ui/test/api-body-parsing.test.ts
index 74f0383a0..f63ad9e35 100644
--- a/plugins/web-ui/test/api-body-parsing.test.ts
+++ b/plugins/web-ui/test/api-body-parsing.test.ts
@@ -81,3 +81,30 @@ test("routes that historically tolerated an empty body still do", async () => {
const forked = calls.at(-1);
assert.deepEqual(forked?.body, { principalId: "alice" });
});
+
+test("connector start forwards an optional Google account type", async () => {
+ const r = await fetch(`${base}/api/connectors/google/start`, {
+ method: "POST",
+ headers,
+ body: JSON.stringify({ accountType: "company" }),
+ });
+ assert.equal(r.status, 200);
+ const started = calls.at(-1);
+ const url = new URL(started?.url ?? "", "http://core.test");
+ assert.equal(url.pathname, "/v1/connectors/oauth/google/start");
+ assert.equal(url.searchParams.get("accountType"), "company");
+});
+
+test("connector revoke forwards the selected Google account only", async () => {
+ const r = await fetch(`${base}/api/connectors/revoke`, {
+ method: "POST",
+ headers,
+ body: JSON.stringify({ provider: "google", accountType: "personal" }),
+ });
+ assert.equal(r.status, 200);
+ assert.deepEqual(calls.at(-1)?.body, {
+ principalId: "alice",
+ provider: "google",
+ accountType: "personal",
+ });
+});
diff --git a/plugins/web-ui/test/keychain-flow.test.ts b/plugins/web-ui/test/keychain-flow.test.ts
index a1073748d..4dca1358d 100644
--- a/plugins/web-ui/test/keychain-flow.test.ts
+++ b/plugins/web-ui/test/keychain-flow.test.ts
@@ -156,7 +156,9 @@ test("keychain rows reserve success badges for actionable states", () => {
});
test("keychain actions keep secondary weight and compact mobile sizing", () => {
- assert.match(connectorsSource, /\$\{available \? html`