Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,25 @@ export async function getDeployments(id: string, signal?: AbortSignal): Promise<
return await backendRequest<DeploymentDTO[]>(`/glues/${id}/deployments`, { signal });
}

export interface AssociateDeploymentAccountParams {
type: string;
accountSelector?: Record<string, string | undefined>;
accountId: string;
}

export async function associateDeploymentAccount(
deploymentId: string,
params: AssociateDeploymentAccountParams,
signal?: AbortSignal,
): Promise<void> {
await backendRequest<{ success: true }>(`/deployments/${deploymentId}/associateAccount`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(params),
signal,
});
}

function areDeploymentsEqual(a: DeploymentDTO, b: DeploymentDTO): boolean {
if (a.status !== b.status) {
return false;
Expand All @@ -230,6 +249,9 @@ function areDeploymentsEqual(a: DeploymentDTO, b: DeploymentDTO): boolean {
if (!equal(a.registrationGroupsToSetup, b.registrationGroupsToSetup)) {
return false;
}
if (!equal(a.compatibleAccounts, b.compatibleAccounts)) {
return false;
}
return true;
}

Expand Down
18 changes: 14 additions & 4 deletions commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,12 @@ export async function deploy(options: DeployOptions, file: string) {
};

let instance: Instance | undefined;
const unmountUI = () => {
let exitOnUnmount = false;
const unmountUI = async () => {
if (instance) {
exitOnUnmount = false;
instance.unmount();
await instance.waitUntilExit();
instance = undefined;
}
};
Expand All @@ -53,7 +56,14 @@ export async function deploy(options: DeployOptions, file: string) {
if (instance) {
instance.rerender(element);
} else {
exitOnUnmount = true;
instance = render(element);
instance.waitUntilExit().then(() => {
// Exit only when Ink unmounts itself, such as when the user presses Ctrl-C.
if (exitOnUnmount) {
process.exit(0);
}
});
}
};

Expand All @@ -65,7 +75,7 @@ export async function deploy(options: DeployOptions, file: string) {
updateUI({ codeAnalysisDuration: performance.now() - duration, codeAnalysisState: "success" });

if (options.debugWriteCreateDeploymentParams) {
unmountUI();
await unmountUI();
await Deno.writeTextFile(
options.debugWriteCreateDeploymentParams,
JSON.stringify(deploymentParams, null, 2) + "\n",
Expand Down Expand Up @@ -99,7 +109,7 @@ export async function deploy(options: DeployOptions, file: string) {
!lookupResult.value ||
absPath.localeCompare(lookupResult.value, undefined, { sensitivity: "base" }) !== 0
) {
unmountUI();
await unmountUI();
console.warn(
`Warning: You are deploying to an existing glue named %c${
JSON.stringify(glueName)
Expand Down Expand Up @@ -141,6 +151,6 @@ export async function deploy(options: DeployOptions, file: string) {
}
// Sometimes client libraries keep connections alive or something, preventing
// the process from naturally exiting immediately, so we explicitly exit here.
unmountUI();
await unmountUI();
Deno.exit();
}
1 change: 1 addition & 0 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@cliffy/prompt": "jsr:@cliffy/prompt@^1.0.0",
"@cliffy/table": "jsr:@cliffy/table@^1.0.0",
"@cliffy/keypress": "jsr:@cliffy/keypress@^1.0.0",
"@inkjs/ui": "npm:@inkjs/ui@^2.0.0",
"@opensrc/deno-open": "jsr:@opensrc/deno-open@^1.0.0",
"@std/assert": "jsr:@std/assert@^1.0.18",
"@std/async": "jsr:@std/async@^1.2.0",
Expand Down
29 changes: 28 additions & 1 deletion deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

156 changes: 144 additions & 12 deletions ui/common.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { Box, Newline, Text } from "ink";
import Spinner from "ink-spinner";
import Link from "ink-link";
import { Select } from "@inkjs/ui";
import { useRef, useState } from "react";
import { open } from "@opensrc/deno-open";
import type {
AccountInjectionDTO,
AccountSlimDTO,
BuildStepDTO,
BuildStepName,
DeploymentDTO,
RegistrationGroupToSetup,
SecretInjectionDTO,
StepStatus,
TriggerDTO,
} from "../backend.ts";
import { associateDeploymentAccount } from "../backend.ts";
import { toSortedByTypeThenLabel } from "./utils.ts";
import { prettyLabels } from "../lib/prettyLabels.ts";

Expand Down Expand Up @@ -91,10 +97,7 @@ export const RegistrationAccountSetupSection = (
{registrationGroupsToSetup.map((ats) => (
<Box paddingLeft={2} key={JSON.stringify([ats.type, ats.accountSelector])}>
<Text>
{ats.type} {ats.accountSelector ? `(${prettyLabels(ats.accountSelector)})` : ""}:{" "}
<Link url={ats.accountSetupUrl}>
<Text bold>{ats.accountSetupUrl}</Text>
</Link>
{ats.type} {ats.accountSelector ? `(${prettyLabels(ats.accountSelector)})` : ""}
</Text>
</Box>
))}
Expand All @@ -105,19 +108,148 @@ export const RegistrationAccountSetupSection = (
</Text>
)}
{secretsToSetup.map((secretInjection) => (
<Box paddingLeft={2} key={secretInjection.id}>
<Text>
{secretInjection.name} ({secretInjection.label}):{" "}
<Link url={secretInjection.secretSetupUrl}>
<Text bold>{secretInjection.secretSetupUrl}</Text>
</Link>
</Text>
</Box>
<Text key={secretInjection.id}>
{secretInjection.name} ({secretInjection.label}):<Newline />
<Link url={secretInjection.secretSetupUrl} fallback={false}>
<Text bold>{secretInjection.secretSetupUrl}</Text>
</Link>
</Text>
))}
</Box>
);
};

export function AccountPickerSection({ deployment }: { deployment: DeploymentDTO }) {
const accountPickerNeeded = deployment.buildSteps.some((step) =>
step.name === "registrationAuth" && step.status === "in_progress"
);
const registrationGroup = deployment.registrationGroupsToSetup[0];
if (!accountPickerNeeded || !registrationGroup) {
return null;
}

const key = JSON.stringify([registrationGroup.type, registrationGroup.accountSelector]);
return (
<AccountPicker
key={key}
deploymentId={deployment.id}
registrationGroup={registrationGroup}
compatibleAccounts={deployment.compatibleAccounts}
/>
);
}

function AccountPicker(
{ deploymentId, registrationGroup, compatibleAccounts }: {
deploymentId: string;
registrationGroup: RegistrationGroupToSetup;
compatibleAccounts: AccountSlimDTO[];
},
) {
const [associating, setAssociating] = useState(false);
const [setupUrl, setSetupUrl] = useState<string>();
const [error, setError] = useState<string>();
const accountsById = new Map(compatibleAccounts.map((account) => [account.id, account]));
const usableAccounts = registrationGroup.compatibleAccountIds.filter((candidate) =>
!candidate.missingScopes?.length && accountsById.has(candidate.id)
);
const accountsNeedingScopes = registrationGroup.compatibleAccountIds.filter((candidate) =>
candidate.missingScopes?.length && candidate.accountSetupUrl && accountsById.has(candidate.id)
);

const groupDisplayName = accountTypeDisplayName(registrationGroup.type) +
(registrationGroup.accountSelector
? ` (${prettyLabels(registrationGroup.accountSelector)})`
: "");
const setupUrls = new Map(
accountsNeedingScopes.map((candidate) => [`setup:${candidate.id}`, candidate.accountSetupUrl!]),
);

const lastPickedValueRef = useRef<string>(undefined);

return (
<Box flexDirection="column">
<Text>Choose {groupDisplayName} account:</Text>
<Select
isDisabled={associating || setupUrl !== undefined}
options={[
...usableAccounts.map((candidate) => ({
label: accountDisplayName(accountsById.get(candidate.id)!),
value: `account:${candidate.id}`,
})),
...accountsNeedingScopes.map((candidate) => ({
label: `${
accountDisplayName(accountsById.get(candidate.id)!)
} (additional permissions required)`,
value: `setup:${candidate.id}`,
})),
{ label: "Add new account", value: "add" },
]}
onChange={async (value) => {
// work around https://github.com/vadimdemedes/ink-ui/issues/26
if (value === lastPickedValueRef.current) {
return;
}
lastPickedValueRef.current = value;
Comment thread
Macil marked this conversation as resolved.

setError(undefined);
const selectedSetupUrl = value === "add"
? registrationGroup.accountSetupUrl
: setupUrls.get(value);
if (selectedSetupUrl) {
setSetupUrl(selectedSetupUrl);
try {
await open(selectedSetupUrl);
} catch (caught) {
setError(caught instanceof Error ? caught.message : String(caught));
}
Comment thread
Macil marked this conversation as resolved.
return;
}

const accountId = value.slice("account:".length);
setAssociating(true);
try {
await associateDeploymentAccount(deploymentId, {
type: registrationGroup.type,
accountSelector: registrationGroup.accountSelector,
accountId,
});
} catch (caught) {
setAssociating(false);
setError(caught instanceof Error ? caught.message : String(caught));
}
}}
/>
{associating && <Text color="gray">Associating account...</Text>}
{setupUrl && (
<Text>
Complete account setup in your browser:<Newline />
<Link url={setupUrl} fallback={false}>
<Text bold>{setupUrl}</Text>
</Link>
</Text>
)}
{error && <Text color="red">{error}</Text>}
</Box>
);
}

/**
* Returns a human-readable display name for the account type. Capitalizes the
* type for display purposes.
*/
function accountTypeDisplayName(accountType: string): string {
return accountType.charAt(0).toUpperCase() + accountType.slice(1);
}

function accountDisplayName(account: AccountSlimDTO): string {
let displayName = `${accountTypeDisplayName(account.type)} {${prettyLabels(account.labels)}}`;
if (account.redactedApiKey) {
displayName += ` (${account.redactedApiKey})`;
}
return displayName;
}

export const CompletedRegistrationList = (
{ triggers, accountInjections, secretInjections }: {
triggers: TriggerDTO[];
Expand Down
8 changes: 8 additions & 0 deletions ui/deploy.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { BuildStepDTO, DeploymentDTO, StepStatus } from "../backend.ts";
import React from "react";
import { Box } from "ink";
import {
AccountPickerSection,
BuildStepStatusRow,
ClientStepRow,
CompletedRegistrationList,
Expand Down Expand Up @@ -78,6 +80,12 @@ export const DeployUI = (
</Text>
</Text>
)}

{deployment && (
<Box paddingTop={1}>
<AccountPickerSection deployment={deployment} />
</Box>
)}
</>
);
};
Loading