181 lines
6.6 KiB
TypeScript
181 lines
6.6 KiB
TypeScript
// "API-Schluessel bearbeiten" on the shared FormDialog chrome.
|
|
//
|
|
// The edit route rendered the boilerplate ApiKeyForm inside an OpenModal, so it
|
|
// wore the same odd popup shape the create route did — plus a FormGroup footer
|
|
// whose delete button raised its own confirm. The fields are the same three the
|
|
// action already accepts; rotate and delete sit in their own section below them,
|
|
// because both are irreversible in a way that saving a new alias is not.
|
|
//
|
|
// Presentational: the route owns the submits. The admin API-key routes keep the
|
|
// shared ApiKeyForm for now.
|
|
|
|
import { useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { FeatherRefreshCw, FeatherSave, FeatherTrash2 } from "@subframe/core";
|
|
import { Alert } from "~/ui/components/Alert";
|
|
import { Button } from "~/ui/components/Button";
|
|
import { Checkbox } from "~/ui/components/Checkbox";
|
|
import { TextInput } from "~/ui/components/TextInput";
|
|
import { DateField } from "../tasks/DateField";
|
|
import { FormDialog } from "../shared/FormDialog";
|
|
|
|
export type ApiKeyEditInput = {
|
|
alias: string;
|
|
/** ISO date (yyyy-mm-dd) or "" for a key that does not expire. */
|
|
expires: string;
|
|
active: boolean;
|
|
};
|
|
|
|
export type ApiKeyEditData = {
|
|
/** Redacted key (prefix…last four) — the raw secret never reaches the client. */
|
|
redactedKey: string;
|
|
alias: string;
|
|
expires: string;
|
|
active: boolean;
|
|
canUpdate: boolean;
|
|
canDelete: boolean;
|
|
canRotate: boolean;
|
|
};
|
|
|
|
export function ApiKeyEditDialog({
|
|
data,
|
|
submitting,
|
|
errorMessage,
|
|
onSave,
|
|
onRotate,
|
|
onDelete,
|
|
onClose,
|
|
}: {
|
|
data: ApiKeyEditData;
|
|
submitting: boolean;
|
|
errorMessage: string | null;
|
|
onSave: (input: ApiKeyEditInput) => void;
|
|
onRotate: () => void;
|
|
onDelete: () => void;
|
|
onClose: () => void;
|
|
}): React.JSX.Element {
|
|
const { t } = useTranslation();
|
|
const [alias, setAlias] = useState(data.alias);
|
|
const [expires, setExpires] = useState(data.expires);
|
|
const [active, setActive] = useState(data.active);
|
|
const [confirm, setConfirm] = useState<"rotate" | "delete" | null>(null);
|
|
|
|
const disabled = !data.canUpdate;
|
|
const canSubmit = !disabled && alias.trim().length > 0 && !submitting;
|
|
|
|
if (confirm) {
|
|
const isDelete = confirm === "delete";
|
|
return (
|
|
<FormDialog
|
|
open
|
|
onOpenChange={(open: boolean) => (open ? undefined : setConfirm(null))}
|
|
size="xs"
|
|
uiId={`ui.settings.apiKeys.editDialog.${confirm}Confirm`}
|
|
title={isDelete ? t("models.apiKey.deleteTitle") : t("models.apiKey.rotateTitle")}
|
|
footer={
|
|
<>
|
|
<Button className="mobile:w-full" type="button" variant="tertiary" onClick={() => setConfirm(null)}>
|
|
{t("shared.cancel")}
|
|
</Button>
|
|
<Button
|
|
className="mobile:w-full"
|
|
variant={isDelete ? "destructive" : "primary"}
|
|
icon={isDelete ? <FeatherTrash2 /> : <FeatherRefreshCw />}
|
|
loading={submitting}
|
|
disabled={submitting}
|
|
onClick={() => (isDelete ? onDelete() : onRotate())}
|
|
data-ui-id={`ui.settings.apiKeys.editDialog.${confirm}Submit`}
|
|
>
|
|
{isDelete ? t("shared.delete") : t("models.apiKey.rotate")}
|
|
</Button>
|
|
</>
|
|
}
|
|
>
|
|
<span className="text-body-default-regular-150 font-body-default-regular-150 text-text-neutral-secondary">
|
|
{isDelete ? t("models.apiKey.deleteBody", { alias: data.alias }) : t("models.apiKey.rotateBody", { alias: data.alias })}
|
|
</span>
|
|
</FormDialog>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<FormDialog
|
|
open
|
|
onOpenChange={(open: boolean) => (open ? undefined : onClose())}
|
|
title={t("models.apiKey.editTitle")}
|
|
subtitle={data.redactedKey}
|
|
uiId="ui.settings.apiKeys.editDialog"
|
|
footer={
|
|
<>
|
|
<Button type="button" variant="tertiary" onClick={onClose} disabled={submitting}>
|
|
{t("shared.cancel")}
|
|
</Button>
|
|
<Button
|
|
icon={<FeatherSave />}
|
|
loading={submitting}
|
|
disabled={!canSubmit}
|
|
onClick={() => onSave({ alias: alias.trim(), expires, active })}
|
|
data-ui-id="ui.settings.apiKeys.editDialog.submit"
|
|
>
|
|
{t("shared.save")}
|
|
</Button>
|
|
</>
|
|
}
|
|
>
|
|
{errorMessage ? <Alert variant="error" title={errorMessage} /> : null}
|
|
|
|
<TextInput className="w-full max-w-full" label={t("models.apiKey.alias")} showLabel>
|
|
<TextInput.Input
|
|
value={alias}
|
|
onChange={(event) => setAlias(event.target.value)}
|
|
disabled={disabled}
|
|
autoComplete="off"
|
|
required
|
|
data-ui-id="ui.settings.apiKeys.editDialog.alias"
|
|
/>
|
|
</TextInput>
|
|
|
|
<div className="flex w-full flex-col gap-1">
|
|
<DateField label={t("models.apiKey.expires")} name="expires" value={expires} onChange={setExpires} />
|
|
<span className="text-body-small-regular-150 font-body-small-regular-150 text-text-neutral-tertiary">{t("models.apiKey.expiresHint")}</span>
|
|
</div>
|
|
|
|
<Checkbox className="w-fit" label={t("models.apiKey.active")} checked={active} disabled={disabled} onCheckedChange={(checked: boolean) => setActive(checked)} />
|
|
|
|
{data.canRotate || data.canDelete ? (
|
|
<div className="flex w-full flex-col gap-3 border-t border-solid border-border-default pt-4">
|
|
{/* Both are irreversible in a way that renaming a key is not, so they
|
|
stand apart from the save action and ask before they run. */}
|
|
<span className="text-body-small-medium-0 font-body-small-medium-0 text-text-neutral-tertiary">{t("models.apiKey.dangerSection")}</span>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
{data.canRotate ? (
|
|
<Button
|
|
type="button"
|
|
variant="secondary"
|
|
icon={<FeatherRefreshCw />}
|
|
disabled={submitting}
|
|
onClick={() => setConfirm("rotate")}
|
|
data-ui-id="ui.settings.apiKeys.editDialog.rotate"
|
|
>
|
|
{t("models.apiKey.rotate")}
|
|
</Button>
|
|
) : null}
|
|
{data.canDelete ? (
|
|
<Button
|
|
type="button"
|
|
variant="destructive"
|
|
icon={<FeatherTrash2 />}
|
|
disabled={submitting}
|
|
onClick={() => setConfirm("delete")}
|
|
data-ui-id="ui.settings.apiKeys.editDialog.delete"
|
|
>
|
|
{t("shared.delete")}
|
|
</Button>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</FormDialog>
|
|
);
|
|
}
|