<script module lang="ts">
export interface WelcomeKey {
rkey: string;
name: string;
fingerprint?: string;
createdAt: string;
}
</script>
<script lang="ts">
import Plus from "$icon/plus";
import Trash from "$icon/trash-2";
import KeyIcon from "$icon/key";
import X from "$icon/x";
import Button from "#lib/components/ui/Button.svelte";
import ConfirmModal from "#lib/components/ui/ConfirmModal.svelte";
import ErrorAlert from "#lib/components/ui/Error.svelte";
import Field from "#lib/components/ui/Field.svelte";
import Input from "#lib/components/ui/Input.svelte";
import Textarea from "#lib/components/ui/Textarea.svelte";
import TimeAgo from "#lib/components/ui/TimeAgo.svelte";
import SettingsEmpty from "#lib/components/settings/SettingsEmpty.svelte";
import SettingsList from "#lib/components/settings/SettingsList.svelte";
import SettingsRow from "#lib/components/settings/SettingsRow.svelte";
import SettingsToolbar from "#lib/components/settings/SettingsToolbar.svelte";
import StepHeader from "#lib/components/welcome/StepHeader.svelte";
let {
keys,
onAdd,
onDelete
}: {
keys: WelcomeKey[];
onAdd: (name: string, key: string) => Promise<void>;
onDelete: (rkey: string) => Promise<void>;
} = $props();
let formOpen = $state(false);
let name = $state("");
let key = $state("");
let adding = $state(false);
let deleting = $state<string>();
let error = $state<string>();
let pendingDelete = $state<WelcomeKey>();
let confirmOpen = $state(false);
const canAdd = $derived(name.trim() !== "" && key.trim() !== "");
const closeForm = () => {
formOpen = false;
name = "";
key = "";
};
const add = async (event: SubmitEvent) => {
event.preventDefault();
if (!canAdd || adding) return;
adding = true;
error = undefined;
try {
await onAdd(name.trim(), key.trim());
closeForm();
} catch (cause) {
error = cause instanceof Error ? cause.message : "Could not add key. Try again.";
} finally {
adding = false;
}
};
const requestDelete = (pubkey: WelcomeKey) => {
if (deleting) return;
pendingDelete = pubkey;
confirmOpen = true;
};
const remove = async () => {
if (!pendingDelete || deleting) return;
const pubkey = pendingDelete;
deleting = pubkey.rkey;
error = undefined;
try {
await onDelete(pubkey.rkey);
} catch (cause) {
error = cause instanceof Error ? cause.message : "Could not delete key. Try again.";
} finally {
deleting = undefined;
confirmOpen = false;
pendingDelete = undefined;
}
};
</script>
<div class="flex flex-col gap-xl">
<StepHeader
title="Add SSH keys"
subtitle="Add a public SSH key to push to your repositories over SSH."
/>
<div class="flex w-full flex-col gap-md">
<SettingsToolbar>
{#snippet end()}
{#if !formOpen}
<Button icon={Plus} onclick={() => (formOpen = true)}>Add key</Button>
{/if}
{/snippet}
</SettingsToolbar>
{#if formOpen}
<form
class="flex flex-col gap-md rounded-md border border-border-default bg-background-subtle p-xs"
onsubmit={add}
>
<Field label="Key name" labelCase="normal">
{#snippet children({ id })}
<Input {id} placeholder="my-laptop" required bind:value={name} />
{/snippet}
</Field>
<Field label="Public key" labelCase="normal">
{#snippet children({ id })}
<Textarea
{id}
size="small"
rows={3}
required
placeholder="ssh-ed25519 AAAA..."
bind:value={key}
/>
{/snippet}
</Field>
<div class="flex items-center justify-end gap-2">
<Button variant="ghost" icon={X} disabled={adding} onclick={closeForm}>
Cancel
</Button>
<Button
type="submit"
variant={canAdd ? "primary" : "default"}
icon={Plus}
disabled={!canAdd}
loading={adding}
>
Add key
</Button>
</div>
</form>
{/if}
<ErrorAlert label={error} />
{#if keys.length === 0}
<SettingsEmpty message="No keys added yet" />
{:else}
<SettingsList>
{#each keys as pubkey (pubkey.rkey)}
<SettingsRow icon={KeyIcon} title={pubkey.name} stack>
{#snippet meta()}
{#if pubkey.fingerprint}
<span
class="typography-monospace-regular break-all text-foreground-muted"
>
{pubkey.fingerprint}
</span>
{/if}
<span class="typography-paragraph-regular text-foreground-muted">
added <TimeAgo value={pubkey.createdAt} variant="full" />
</span>
{/snippet}
<Button
variant="danger"
icon={Trash}
loading={deleting === pubkey.rkey}
onclick={() => requestDelete(pubkey)}
>
Delete
</Button>
</SettingsRow>
{/each}
</SettingsList>
{/if}
</div>
</div>
<ConfirmModal
bind:open={confirmOpen}
title={`Delete ${pendingDelete?.name ?? "key"}?`}
description="This removes the key from your account. You will need to add it again to push with it."
confirmLabel="Delete key"
loading={!!deleting}
onconfirm={() => void remove()}
/>