Spreadsheet-UX op een echte relationele engine
Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.
import { useState, type ReactNode } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { ROLES, type AgentKey, type Role } from '@wellformed/shared';
import { api } from '../api/client';
import { useAgentKeys } from '../api/hooks';
import { useI18n } from '../i18n';
import { useSession } from '../state/session';
import { useToasts } from '../state/toasts';
import { absolute } from '../state/route';
import { Modal } from './Modal';
/**
* The keys an AI assistant works through.
*
* The thing worth being clear about on this screen is that a key is *yours*.
* It acts as you, with your role, and everything it does lands in the audit
* trail under your name with the key's label beside it. So the label is not
* decoration — it is how someone reading the trail in three months knows which
* assistant did what.
*/
export function AgentKeysDialog({ onClose }: { onClose(): void }): ReactNode {
const session = useSession();
const toasts = useToasts();
const queryClient = useQueryClient();
const { t } = useI18n();
const isAdmin = session.can('admin');
const myRole = session.workspace?.role ?? 'viewer';
const keys = useAgentKeys(true);
const [creating, setCreating] = useState(false);
const [label, setLabel] = useState('');
const [ceiling, setCeiling] = useState<Role>('viewer');
const [busy, setBusy] = useState(false);
/** Shown until this dialog closes, because there is no second chance to read it. */
const [issued, setIssued] = useState<AgentKey | null>(null);
const refresh = () => queryClient.invalidateQueries({ queryKey: ['agentKeys'] });
/** A ceiling above your own role would be inert, so it is not offered. */
const available = ROLES.filter((r) => rank(r) <= rank(myRole));
async function create() {
setBusy(true);
try {
const { key } = await api.createAgentKey({ label: label.trim(), ceiling });
await refresh();
setIssued(key);
setCreating(false);
setLabel('');
await copy(key.secret!);
toasts.push(t('keys.copied'), 'success');
} catch (error) {
toasts.fail(error);
} finally {
setBusy(false);
}
}
async function revoke(key: AgentKey) {
const ok = window.confirm(t('keys.revokeConfirm', key.label));
if (!ok) return;
try {
await api.revokeAgentKey(key.id);
if (issued?.id === key.id) setIssued(null);
await refresh();
} catch (error) {
toasts.fail(error);
}
}
const rows = keys.data?.keys ?? [];
return (
<Modal
title={t('keys.title')}
wide
onClose={onClose}
footer={
<>
<div className="grow" />
{!creating && (
<button className="btn" onClick={() => setCreating(true)}>
{t('keys.newKey')}
</button>
)}
<button className="btn primary" onClick={onClose}>
{t('common.done')}
</button>
</>
}
>
<div className="mb">
<p className="hint" style={{ marginTop: 0 }}>
{t('keys.intro')}
</p>
{keys.isLoading && <div className="hint">{t('common.loading')}</div>}
{!keys.isLoading && rows.length === 0 && !creating && (
<div className="gridstate">{t('keys.noneYet')}</div>
)}
{rows.length > 0 && (
<table className="plain">
<thead>
<tr>
<th>{t('keys.label')}</th>
<th>{t('keys.can')}</th>
{isAdmin && <th>{t('keys.heldBy')}</th>}
<th>{t('keys.lastUsed')}</th>
<th />
</tr>
</thead>
<tbody>
{rows.map((key) => (
<tr key={key.id}>
<td>{key.label}</td>
<td>{t(`role.${key.ceiling}`)}</td>
{isAdmin && (
<td className="hint" style={{ fontWeight: 400 }}>
{key.accountId === session.account?.id ? t('keys.you') : key.accountName}
</td>
)}
<td className="hint" style={{ fontWeight: 400 }}>
{key.lastUsedAt ? key.lastUsedAt.replace(' ', ' · ') : t('keys.never')}
</td>
<td style={{ textAlign: 'right', whiteSpace: 'nowrap' }}>
<button
className="iconbtn danger"
style={{ padding: '3px 7px' }}
onClick={() => void revoke(key)}
>
{t('keys.revoke')}
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
{issued?.secret && (
<div className="notice">
<div className="hint">{t('keys.copyNow')}</div>
<input
className="mono"
readOnly
aria-label={t('keys.newKeyAria')}
value={issued.secret}
onFocus={(e) => e.target.select()}
/>
<div className="hint" style={{ marginTop: 8 }}>
{t('keys.pointBefore')}
<span className="mono">{absolute(`/api/w/${session.workspace?.slug ?? ''}/ai/manifest`)}</span>
{t('keys.pointMiddle')}
<span className="mono">Authorization: Bearer <key></span>
{t('keys.pointAfter')}
</div>
</div>
)}
{creating && (
<div style={{ borderTop: '1px solid var(--line)', paddingTop: 12, display: 'grid', gap: 10 }}>
<label>
{t('keys.whatFor')}
<input
value={label}
onChange={(e) => setLabel(e.target.value)}
placeholder="Claude Code"
autoFocus
/>
</label>
<label>
{t('keys.itMay')}
<select value={ceiling} onChange={(e) => setCeiling(e.target.value as Role)}>
{available.map((r) => (
<option key={r} value={r}>
{t(`role.${r}`)} — {t(`keys.ceilingHelp.${r}`)}
</option>
))}
</select>
</label>
<span className="hint">{t('keys.ceilingHint', t(`role.${myRole}`))}</span>
<div className="rowend">
<button className="btn" onClick={() => setCreating(false)}>
{t('common.cancel')}
</button>
<button
className="btn primary"
onClick={() => void create()}
disabled={busy || !label.trim()}
>
{t('keys.createKey')}
</button>
</div>
</div>
)}
</div>
</Modal>
);
}
function rank(role: Role): number {
return ROLES.indexOf(role);
}
async function copy(text: string): Promise<void> {
try {
await navigator.clipboard.writeText(text);
} catch {
// Clipboard access can be refused; the key is on screen either way.
}
}