docs(access): add credential lifecycle contextual help
Module Package Release / publish-packages (push) Successful in 12s
Module Package Release / publish-packages (push) Successful in 12s
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@govoplan/access-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -49,6 +49,33 @@ assert.match(serviceAccounts, /rotateServiceAccountCredential/);
|
||||
assert.match(serviceAccounts, /revokeServiceAccountCredential/);
|
||||
assert.match(serviceAccounts, /Secrets are shown once/);
|
||||
assert.match(serviceAccounts, /<ConfirmDialog[\s\S]*Retire service account/);
|
||||
|
||||
for (const contextId of [
|
||||
"access.api-keys.action.create",
|
||||
"access.api-keys.action.revoke",
|
||||
"access.api-keys.field.owner",
|
||||
"access.api-keys.field.expiry",
|
||||
"access.api-keys.field.scopes",
|
||||
"access.api-keys.secret",
|
||||
"access.api-keys.confirm-revoke"
|
||||
]) {
|
||||
assert.ok(apiKeys.includes(contextId), `API-key help context ${contextId} is missing`);
|
||||
}
|
||||
|
||||
for (const contextId of [
|
||||
"access.service-accounts.action.create",
|
||||
"access.service-accounts.action.activation",
|
||||
"access.service-accounts.action.retire",
|
||||
"access.service-accounts.field.scope-ceiling",
|
||||
"access.service-accounts.action.rotate-credential",
|
||||
"access.service-accounts.action.revoke-credential",
|
||||
"access.service-accounts.field.credential-expiry",
|
||||
"access.service-accounts.field.credential-scopes",
|
||||
"access.service-accounts.secret",
|
||||
"access.service-accounts.confirm-retire"
|
||||
]) {
|
||||
assert.ok(serviceAccounts.includes(contextId), `Service-account help context ${contextId} is missing`);
|
||||
}
|
||||
assert.match(moduleSource, /access\.admin\.tenant-service-accounts/);
|
||||
assert.match(moduleSource, /access\.settings\.sessions/);
|
||||
assert.match(moduleSource, /"settings\.sections": accessSettingsSections/);
|
||||
|
||||
@@ -103,8 +103,8 @@ export default function ApiKeysPanel({ settings, auth, canCreate, canRevoke }: {
|
||||
{ id: "last_used", header: "i18n:govoplan-access.last_used.f1109d3d", width: 180, minWidth: 150, resizable: true, sortable: true, value: (row) => row.last_used_at || "", render: (row) => formatDateTime(row.last_used_at) },
|
||||
{ id: "expires", header: "i18n:govoplan-access.expires.a99be3da", width: 180, minWidth: 150, resizable: true, sortable: true, value: (row) => row.expires_at || "", render: (row) => row.expires_at ? formatDateTime(row.expires_at) : "i18n:govoplan-access.no_expiry.39d436aa" },
|
||||
{ id: "actions", header: "i18n:govoplan-access.actions.c3cd636a", width: 108, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[
|
||||
{ id: "inspect", label: i18nMessage("i18n:govoplan-access.inspect_value.9d5d1071", { value0: row.name }), icon: <Search />, onClick: () => setViewing(row) },
|
||||
{ id: "revoke", label: i18nMessage("i18n:govoplan-access.revoke_value.34640d6a", { value0: row.name }), icon: <Trash2 />, variant: "danger", applicable: !row.revoked_at, disabled: !canRevoke, disabledReason: row.revoked_at ? "i18n:govoplan-access.revoked.85f17ac0" : !canRevoke ? ACCESS_INTERFACE_I18N.writePermissionRequired : undefined, onClick: () => setRevoking(row) }
|
||||
{ id: "inspect", label: i18nMessage("i18n:govoplan-access.inspect_value.9d5d1071", { value0: row.name }), icon: <Search />, helpContextId: "access.api-keys.action.inspect", helpModuleId: "access", onClick: () => setViewing(row) },
|
||||
{ id: "revoke", label: i18nMessage("i18n:govoplan-access.revoke_value.34640d6a", { value0: row.name }), icon: <Trash2 />, variant: "danger", helpContextId: "access.api-keys.action.revoke", helpModuleId: "access", applicable: !row.revoked_at, disabled: !canRevoke, disabledReason: row.revoked_at ? "i18n:govoplan-access.revoked.85f17ac0" : !canRevoke ? ACCESS_INTERFACE_I18N.writePermissionRequired : undefined, onClick: () => setRevoking(row) }
|
||||
]} /> }],
|
||||
[canRevoke]);
|
||||
|
||||
@@ -153,20 +153,20 @@ export default function ApiKeysPanel({ settings, auth, canCreate, canRevoke }: {
|
||||
|
||||
return (
|
||||
<>
|
||||
<AdminPageLayout title="i18n:govoplan-access.tenant_api_keys.4b1d81f8" description="i18n:govoplan-access.tenant_scoped_automation_credentials_are_capped_.9059dcae" loading={loading} error={error} success={success} actions={<><DocumentationHelpLink reference={ACCESS_REFERENCE_DOCUMENTATION} /><ToggleSwitch label="i18n:govoplan-access.show_revoked.b4265807" checked={showRevoked} onChange={setShowRevoked} /><Button onClick={() => void load()} disabled={loading} disabledReason={loading ? ACCESS_INTERFACE_I18N.loading : undefined}>i18n:govoplan-access.reload.cce71553</Button><AdminIconButton label="i18n:govoplan-access.add_api_key.725d9988" icon={<Plus />} variant="primary" onClick={openCreate} disabled={!canCreate || !users.length} disabledReason={!canCreate ? ACCESS_INTERFACE_I18N.createPermissionRequired : !users.length ? ACCESS_INTERFACE_I18N.selectUserAndScopes : undefined} /></>}>
|
||||
<AdminPageLayout title="i18n:govoplan-access.tenant_api_keys.4b1d81f8" description="i18n:govoplan-access.tenant_scoped_automation_credentials_are_capped_.9059dcae" loading={loading} error={error} success={success} helpContextId="access.admin.api-keys" helpModuleId="access" actions={<><DocumentationHelpLink reference={ACCESS_REFERENCE_DOCUMENTATION} /><ToggleSwitch label="i18n:govoplan-access.show_revoked.b4265807" checked={showRevoked} helpContextId="access.api-keys.field.show-revoked" helpModuleId="access" onChange={setShowRevoked} /><Button helpContextId="access.api-keys.action.reload" helpModuleId="access" onClick={() => void load()} disabled={loading} disabledReason={loading ? ACCESS_INTERFACE_I18N.loading : undefined}>i18n:govoplan-access.reload.cce71553</Button><AdminIconButton label="i18n:govoplan-access.add_api_key.725d9988" icon={<Plus />} variant="primary" helpContextId="access.api-keys.action.create" helpModuleId="access" onClick={openCreate} disabled={!canCreate || !users.length} disabledReason={!canCreate ? ACCESS_INTERFACE_I18N.createPermissionRequired : !users.length ? ACCESS_INTERFACE_I18N.selectUserAndScopes : undefined} /></>}>
|
||||
<div className="admin-table-surface"><DataGrid id="admin-api-keys-v3" rows={keys} columns={columns} initialFit="container" getRowKey={(row) => row.id} emptyText="i18n:govoplan-access.no_api_keys_found.1f377128" /></div>
|
||||
</AdminPageLayout>
|
||||
|
||||
<Dialog variant="administration" size="wide" open={creating} title="i18n:govoplan-access.create_api_key.d7b30388" onClose={() => !busy && setCreating(false)} className="" footer={<><Button onClick={() => setCreating(false)} disabled={busy} disabledReason={busy ? ACCESS_INTERFACE_I18N.operationInProgress : undefined}>i18n:govoplan-access.cancel.77dfd213</Button><Button variant="primary" onClick={() => void save()} disabledReason={saveDisabledReason({ busy, permitted: canCreate, complete: Boolean(draft.name.trim() && draft.userId && draft.scopes.length) })}>{busy ? "i18n:govoplan-access.creating.94d7d8ee" : "i18n:govoplan-access.create_key.e028cb09"}</Button></>}>
|
||||
<Dialog variant="administration" size="wide" open={creating} title="i18n:govoplan-access.create_api_key.d7b30388" helpContextId="access.api-keys.action.create" helpModuleId="access" onClose={() => !busy && setCreating(false)} className="" footer={<><Button onClick={() => setCreating(false)} disabled={busy} disabledReason={busy ? ACCESS_INTERFACE_I18N.operationInProgress : undefined}>i18n:govoplan-access.cancel.77dfd213</Button><Button variant="primary" helpContextId="access.api-keys.action.create" helpModuleId="access" onClick={() => void save()} disabledReason={saveDisabledReason({ busy, permitted: canCreate, complete: Boolean(draft.name.trim() && draft.userId && draft.scopes.length) })}>{busy ? "i18n:govoplan-access.creating.94d7d8ee" : "i18n:govoplan-access.create_key.e028cb09"}</Button></>}>
|
||||
<FormGrid columns={2} gap="small" collapseAt="workspace" className="">
|
||||
<FormField label="i18n:govoplan-access.name.709a2322"><input value={draft.name} onChange={(event) => setDraft({ ...draft, name: event.target.value })} /></FormField>
|
||||
<FormField label="i18n:govoplan-access.owner.89ff3122"><select value={draft.userId} onChange={(event) => {const userId = event.target.value;const user = users.find((item) => item.id === userId);const allowed = new Set(permissions.filter((permission) => user?.effective_scopes.some((scope) => scopeGrants(scope, permission.scope))).map((permission) => permission.scope));setDraft({ ...draft, userId, scopes: draft.scopes.filter((scope) => allowed.has(scope)) });}}><option value="">i18n:govoplan-access.select_user.b8a1d9de</option>{users.map((user) => <option key={user.id} value={user.id}>{user.display_name || user.email} — {user.email}</option>)}</select></FormField>
|
||||
<FormField label="i18n:govoplan-access.expiry.ba8f571e"><DateTimeField value={draft.expiresAt} onChange={(value) => setDraft({ ...draft, expiresAt: value })} /></FormField>
|
||||
<FormField label="i18n:govoplan-access.name.709a2322" helpContextId="access.api-keys.field.name" helpModuleId="access"><input value={draft.name} onChange={(event) => setDraft({ ...draft, name: event.target.value })} /></FormField>
|
||||
<FormField label="i18n:govoplan-access.owner.89ff3122" helpContextId="access.api-keys.field.owner" helpModuleId="access"><select value={draft.userId} onChange={(event) => {const userId = event.target.value;const user = users.find((item) => item.id === userId);const allowed = new Set(permissions.filter((permission) => user?.effective_scopes.some((scope) => scopeGrants(scope, permission.scope))).map((permission) => permission.scope));setDraft({ ...draft, userId, scopes: draft.scopes.filter((scope) => allowed.has(scope)) });}}><option value="">i18n:govoplan-access.select_user.b8a1d9de</option>{users.map((user) => <option key={user.id} value={user.id}>{user.display_name || user.email} — {user.email}</option>)}</select></FormField>
|
||||
<FormField label="i18n:govoplan-access.expiry.ba8f571e" helpContextId="access.api-keys.field.expiry" helpModuleId="access"><DateTimeField value={draft.expiresAt} onChange={(value) => setDraft({ ...draft, expiresAt: value })} /></FormField>
|
||||
</FormGrid>
|
||||
<div className="form-field"><span className="form-label">i18n:govoplan-access.allowed_scopes.d94515ff</span><AdminSelectionList options={allowedPermissions.map((permission) => ({ id: permission.scope, label: permission.label, description: i18nMessage("i18n:govoplan-access.value_value.0e2772ed", { value0: permission.scope, value1: permission.description }) }))} selected={draft.scopes} onChange={(scopes) => setDraft({ ...draft, scopes })} emptyText="i18n:govoplan-access.the_selected_user_has_no_tenant_permissions_avai.96985ec7" /></div>
|
||||
<div className="form-field" data-help-context-id="access.api-keys.field.scopes" data-help-module-id="access"><span className="form-label">i18n:govoplan-access.allowed_scopes.d94515ff</span><AdminSelectionList options={allowedPermissions.map((permission) => ({ id: permission.scope, label: permission.label, description: i18nMessage("i18n:govoplan-access.value_value.0e2772ed", { value0: permission.scope, value1: permission.description }) }))} selected={draft.scopes} onChange={(scopes) => setDraft({ ...draft, scopes })} emptyText="i18n:govoplan-access.the_selected_user_has_no_tenant_permissions_avai.96985ec7" /></div>
|
||||
</Dialog>
|
||||
|
||||
<Dialog variant="administration" size="wide" open={Boolean(viewing)} title="i18n:govoplan-access.api_key_details.f70c16be" onClose={() => setViewing(null)} className="" footer={<Button onClick={() => setViewing(null)}>i18n:govoplan-access.close.bbfa773e</Button>}>
|
||||
<Dialog variant="administration" size="wide" open={Boolean(viewing)} title="i18n:govoplan-access.api_key_details.f70c16be" helpContextId="access.api-keys.action.inspect" helpModuleId="access" onClose={() => setViewing(null)} className="" footer={<Button onClick={() => setViewing(null)}>i18n:govoplan-access.close.bbfa773e</Button>}>
|
||||
{viewing && <><DescriptionList>
|
||||
<DescriptionItem term={<>i18n:govoplan-access.name.709a2322</>}>{viewing.name}</DescriptionItem><DescriptionItem term={<>i18n:govoplan-access.prefix.90eceb01</>}>{viewing.prefix}…</DescriptionItem>
|
||||
<DescriptionItem term={<>i18n:govoplan-access.owner.89ff3122</>}>{viewing.user_email}</DescriptionItem><DescriptionItem term={<>i18n:govoplan-access.status.bae7d5be</>}>{viewing.revoked_at ? "i18n:govoplan-access.revoked.85f17ac0" : "i18n:govoplan-access.active.a733b809"}</DescriptionItem>
|
||||
@@ -175,11 +175,11 @@ export default function ApiKeysPanel({ settings, auth, canCreate, canRevoke }: {
|
||||
</DescriptionList><h3>i18n:govoplan-access.scopes.c23540e5</h3><div className="admin-scope-list">{viewing.scopes.map((scope) => <code key={scope}>{scope}</code>)}</div></>}
|
||||
</Dialog>
|
||||
|
||||
<Dialog variant="administration" size="large" open={Boolean(secret)} title="i18n:govoplan-access.api_key_secret.00b16050" onClose={() => setSecret(null)} className="" footer={<Button variant="primary" onClick={() => setSecret(null)}>i18n:govoplan-access.i_have_recorded_it.7522da18</Button>}>
|
||||
<Dialog variant="administration" size="large" open={Boolean(secret)} title="i18n:govoplan-access.api_key_secret.00b16050" helpContextId="access.api-keys.secret" helpModuleId="access" onClose={() => setSecret(null)} className="" footer={<Button variant="primary" helpContextId="access.api-keys.secret" helpModuleId="access" onClick={() => setSecret(null)}>i18n:govoplan-access.i_have_recorded_it.7522da18</Button>}>
|
||||
{secret && <><p>i18n:govoplan-access.the_secret_for.c60737ef <strong>{secret.name}</strong> i18n:govoplan-access.is_shown_once.af2b1235</p><code className="admin-secret">{secret.value}</code><p className="muted small-note">i18n:govoplan-access.store_it_in_a_secret_manager_only_its_prefix_and.796ac588</p></>}
|
||||
</Dialog>
|
||||
|
||||
<ConfirmDialog open={Boolean(revoking)} title="i18n:govoplan-access.revoke_api_key.3160aa7e" message={i18nMessage("i18n:govoplan-access.revoke_value_existing_clients_will_immediately_l.c70f07fd", { value0: revoking?.name })} confirmLabel="i18n:govoplan-access.revoke_key.acb203e7" tone="danger" busy={busy} onCancel={() => setRevoking(null)} onConfirm={() => void revoke()} />
|
||||
<ConfirmDialog open={Boolean(revoking)} title="i18n:govoplan-access.revoke_api_key.3160aa7e" message={i18nMessage("i18n:govoplan-access.revoke_value_existing_clients_will_immediately_l.c70f07fd", { value0: revoking?.name })} confirmLabel="i18n:govoplan-access.revoke_key.acb203e7" tone="danger" busy={busy} helpContextId="access.api-keys.confirm-revoke" helpModuleId="access" onCancel={() => setRevoking(null)} onConfirm={() => void revoke()} />
|
||||
</>);
|
||||
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ export default function ServiceAccountsPanel({
|
||||
resizable: false,
|
||||
align: "right",
|
||||
render: (row) => <TableActionGroup actions={[
|
||||
{ id: "manage", label: `Manage ${row.name}`, icon: <Search />, onClick: () => void openManager(row) }
|
||||
{ id: "manage", label: `Manage ${row.name}`, icon: <Search />, helpContextId: "access.service-accounts.action.manage", helpModuleId: "access", onClick: () => void openManager(row) }
|
||||
]} />
|
||||
}
|
||||
], []);
|
||||
@@ -274,6 +274,8 @@ export default function ServiceAccountsPanel({
|
||||
id: "rotate",
|
||||
label: `Rotate ${row.name}`,
|
||||
icon: <RefreshCw />,
|
||||
helpContextId: "access.service-accounts.action.rotate-credential",
|
||||
helpModuleId: "access",
|
||||
applicable: !row.revoked_at,
|
||||
disabled: !canWrite || !managing?.is_active,
|
||||
disabledReason: !canWrite ? ACCESS_INTERFACE_I18N.writePermissionRequired : !managing?.is_active ? "Activate the service account first." : undefined,
|
||||
@@ -284,6 +286,8 @@ export default function ServiceAccountsPanel({
|
||||
label: `Revoke ${row.name}`,
|
||||
icon: <Trash2 />,
|
||||
variant: "danger",
|
||||
helpContextId: "access.service-accounts.action.revoke-credential",
|
||||
helpModuleId: "access",
|
||||
applicable: !row.revoked_at,
|
||||
disabled: !canWrite,
|
||||
disabledReason: !canWrite ? ACCESS_INTERFACE_I18N.writePermissionRequired : undefined,
|
||||
@@ -445,10 +449,12 @@ export default function ServiceAccountsPanel({
|
||||
loading={loading}
|
||||
error={error}
|
||||
success={success}
|
||||
helpContextId="access.admin.service-accounts"
|
||||
helpModuleId="access"
|
||||
actions={<>
|
||||
<DocumentationHelpLink reference={ACCESS_REFERENCE_DOCUMENTATION} />
|
||||
<Button onClick={() => void load()} disabled={loading}>Reload</Button>
|
||||
<AdminIconButton label="Add service account" icon={<Plus />} variant="primary" onClick={openCreateAccount} disabled={!canWrite} disabledReason={!canWrite ? ACCESS_INTERFACE_I18N.writePermissionRequired : undefined} />
|
||||
<Button helpContextId="access.service-accounts.action.reload" helpModuleId="access" onClick={() => void load()} disabled={loading}>Reload</Button>
|
||||
<AdminIconButton label="Add service account" icon={<Plus />} variant="primary" helpContextId="access.service-accounts.action.create" helpModuleId="access" onClick={openCreateAccount} disabled={!canWrite} disabledReason={!canWrite ? ACCESS_INTERFACE_I18N.writePermissionRequired : undefined} />
|
||||
</>}
|
||||
>
|
||||
<div className="admin-table-surface">
|
||||
@@ -459,15 +465,17 @@ export default function ServiceAccountsPanel({
|
||||
<Dialog variant="administration" size="wide"
|
||||
open={Boolean(accountEditor)}
|
||||
title={accountEditor === "create" ? "Create service account" : "Edit service account"}
|
||||
helpContextId="access.service-accounts.account-editor"
|
||||
helpModuleId="access"
|
||||
onClose={() => !busy && setAccountEditor(null)}
|
||||
className=""
|
||||
footer={<><Button onClick={() => setAccountEditor(null)} disabled={busy}>Cancel</Button><Button variant="primary" onClick={() => void saveAccount()} disabled={!canWrite || busy || !accountDraft.name.trim()}>{busy ? "Saving..." : "Save"}</Button></>}
|
||||
footer={<><Button onClick={() => setAccountEditor(null)} disabled={busy}>Cancel</Button><Button variant="primary" helpContextId="access.service-accounts.action.save" helpModuleId="access" onClick={() => void saveAccount()} disabled={!canWrite || busy || !accountDraft.name.trim()}>{busy ? "Saving..." : "Save"}</Button></>}
|
||||
>
|
||||
<FormGrid columns={2} gap="small" collapseAt="workspace" className="">
|
||||
<FormField label="Name"><input value={accountDraft.name} onChange={(event) => setAccountDraft({ ...accountDraft, name: event.target.value })} /></FormField>
|
||||
<FormField label="Description"><input value={accountDraft.description} onChange={(event) => setAccountDraft({ ...accountDraft, description: event.target.value })} /></FormField>
|
||||
<FormField label="Name" helpContextId="access.service-accounts.field.name" helpModuleId="access"><input value={accountDraft.name} onChange={(event) => setAccountDraft({ ...accountDraft, name: event.target.value })} /></FormField>
|
||||
<FormField label="Description" helpContextId="access.service-accounts.field.description" helpModuleId="access"><input value={accountDraft.description} onChange={(event) => setAccountDraft({ ...accountDraft, description: event.target.value })} /></FormField>
|
||||
</FormGrid>
|
||||
<div className="form-field">
|
||||
<div className="form-field" data-help-context-id="access.service-accounts.field.scope-ceiling" data-help-module-id="access">
|
||||
<span className="form-label">Scope ceiling</span>
|
||||
<AdminSelectionList options={grantablePermissions.map((permission) => ({ id: permission.scope, label: permission.label, description: `${permission.scope} - ${permission.description}` }))} selected={accountDraft.scopes} onChange={(scopes) => setAccountDraft({ ...accountDraft, scopes })} emptyText="No tenant scopes can be delegated by your current account." />
|
||||
</div>
|
||||
@@ -476,6 +484,8 @@ export default function ServiceAccountsPanel({
|
||||
<Dialog variant="administration" size="wide"
|
||||
open={Boolean(managing)}
|
||||
title={managing?.name ?? "Service account"}
|
||||
helpContextId="access.service-accounts.action.manage"
|
||||
helpModuleId="access"
|
||||
onClose={() => !busy && setManaging(null)}
|
||||
className=""
|
||||
footer={<Button onClick={() => setManaging(null)} disabled={busy}>Close</Button>}
|
||||
@@ -488,13 +498,13 @@ export default function ServiceAccountsPanel({
|
||||
<MetricCard label="Revision" value={managing.revision} />
|
||||
</MetricGrid>
|
||||
<ActionToolbar className="admin-toolbar-row">
|
||||
<Button onClick={openEditAccount} disabled={!canWrite || busy}><Pencil aria-hidden="true" /> Edit</Button>
|
||||
<Button onClick={() => void setActive(!managing.is_active)} disabled={!canWrite || busy}>{managing.is_active ? <ShieldOff aria-hidden="true" /> : <RefreshCw aria-hidden="true" />} {managing.is_active ? "Deactivate" : "Activate"}</Button>
|
||||
<Button variant="danger" onClick={() => setRetiring(true)} disabled={!canWrite || busy || !managing.is_active}><Trash2 aria-hidden="true" /> Retire</Button>
|
||||
<AdminIconButton label="Create credential" icon={<KeyRound />} variant="primary" onClick={() => openCredentialEditor("create")} disabled={!canWrite || !managing.is_active} disabledReason={!canWrite ? ACCESS_INTERFACE_I18N.writePermissionRequired : !managing.is_active ? "Activate the service account first." : undefined} />
|
||||
<Button helpContextId="access.service-accounts.action.edit" helpModuleId="access" onClick={openEditAccount} disabled={!canWrite || busy}><Pencil aria-hidden="true" /> Edit</Button>
|
||||
<Button helpContextId="access.service-accounts.action.activation" helpModuleId="access" onClick={() => void setActive(!managing.is_active)} disabled={!canWrite || busy}>{managing.is_active ? <ShieldOff aria-hidden="true" /> : <RefreshCw aria-hidden="true" />} {managing.is_active ? "Deactivate" : "Activate"}</Button>
|
||||
<Button variant="danger" helpContextId="access.service-accounts.action.retire" helpModuleId="access" onClick={() => setRetiring(true)} disabled={!canWrite || busy || !managing.is_active}><Trash2 aria-hidden="true" /> Retire</Button>
|
||||
<AdminIconButton label="Create credential" icon={<KeyRound />} variant="primary" helpContextId="access.service-accounts.action.create-credential" helpModuleId="access" onClick={() => openCredentialEditor("create")} disabled={!canWrite || !managing.is_active} disabledReason={!canWrite ? ACCESS_INTERFACE_I18N.writePermissionRequired : !managing.is_active ? "Activate the service account first." : undefined} />
|
||||
</ActionToolbar>
|
||||
<ActionToolbar className="admin-toolbar-row">
|
||||
<ToggleSwitch label="Show revoked credentials" checked={showRevoked} onChange={setShowRevoked} />
|
||||
<ToggleSwitch label="Show revoked credentials" checked={showRevoked} helpContextId="access.service-accounts.field.show-revoked" helpModuleId="access" onChange={setShowRevoked} />
|
||||
</ActionToolbar>
|
||||
<div className="admin-table-surface">
|
||||
<DataGrid id="admin-service-account-credentials-v1" rows={visibleCredentials} columns={credentialColumns} initialFit="container" getRowKey={(row) => row.id} emptyText="No credentials found." />
|
||||
@@ -506,27 +516,29 @@ export default function ServiceAccountsPanel({
|
||||
<Dialog variant="administration" size="wide"
|
||||
open={Boolean(credentialEditor)}
|
||||
title={credentialEditor?.mode === "rotate" ? "Rotate credential" : "Create credential"}
|
||||
helpContextId="access.service-accounts.credential-editor"
|
||||
helpModuleId="access"
|
||||
onClose={() => !busy && setCredentialEditor(null)}
|
||||
className=""
|
||||
footer={<><Button onClick={() => setCredentialEditor(null)} disabled={busy}>Cancel</Button><Button variant="primary" onClick={() => void saveCredential()} disabled={!canWrite || busy || !credentialDraft.name.trim() || credentialDraft.scopes.length === 0}>{busy ? "Saving..." : credentialEditor?.mode === "rotate" ? "Rotate" : "Create"}</Button></>}
|
||||
footer={<><Button onClick={() => setCredentialEditor(null)} disabled={busy}>Cancel</Button><Button variant="primary" helpContextId="access.service-accounts.action.save-credential" helpModuleId="access" onClick={() => void saveCredential()} disabled={!canWrite || busy || !credentialDraft.name.trim() || credentialDraft.scopes.length === 0}>{busy ? "Saving..." : credentialEditor?.mode === "rotate" ? "Rotate" : "Create"}</Button></>}
|
||||
>
|
||||
{credentialEditor?.mode === "rotate" && <p className="muted small-note">Rotation creates a new secret and revokes the previous credential in the same transaction.</p>}
|
||||
<FormGrid columns={2} gap="small" collapseAt="workspace" className="">
|
||||
<FormField label="Name"><input value={credentialDraft.name} onChange={(event) => setCredentialDraft({ ...credentialDraft, name: event.target.value })} /></FormField>
|
||||
<FormField label="Expiry"><DateTimeField value={credentialDraft.expiresAt} onChange={(value) => setCredentialDraft({ ...credentialDraft, expiresAt: value })} /></FormField>
|
||||
<FormField label="Name" helpContextId="access.service-accounts.field.credential-name" helpModuleId="access"><input value={credentialDraft.name} onChange={(event) => setCredentialDraft({ ...credentialDraft, name: event.target.value })} /></FormField>
|
||||
<FormField label="Expiry" helpContextId="access.service-accounts.field.credential-expiry" helpModuleId="access"><DateTimeField value={credentialDraft.expiresAt} onChange={(value) => setCredentialDraft({ ...credentialDraft, expiresAt: value })} /></FormField>
|
||||
</FormGrid>
|
||||
<div className="form-field">
|
||||
<div className="form-field" data-help-context-id="access.service-accounts.field.credential-scopes" data-help-module-id="access">
|
||||
<span className="form-label">Credential scopes</span>
|
||||
<AdminSelectionList options={credentialPermissions.map((permission) => ({ id: permission.scope, label: permission.label, description: `${permission.scope} - ${permission.description}` }))} selected={credentialDraft.scopes} onChange={(scopes) => setCredentialDraft({ ...credentialDraft, scopes })} emptyText="The service account has no credential scopes available." />
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<Dialog variant="administration" size="large" open={Boolean(secret)} title="Service-account secret" onClose={() => setSecret(null)} className="" footer={<Button variant="primary" onClick={() => setSecret(null)}>I have recorded it</Button>}>
|
||||
<Dialog variant="administration" size="large" open={Boolean(secret)} title="Service-account secret" helpContextId="access.service-accounts.secret" helpModuleId="access" onClose={() => setSecret(null)} className="" footer={<Button variant="primary" helpContextId="access.service-accounts.secret" helpModuleId="access" onClick={() => setSecret(null)}>I have recorded it</Button>}>
|
||||
{secret && <><p>The secret for <strong>{secret.name}</strong> is shown once.</p><code className="admin-secret">{secret.value}</code><p className="muted small-note">Store it in a secret manager. GovOPlaN retains only a one-way hash and the visible prefix.</p></>}
|
||||
</Dialog>
|
||||
|
||||
<ConfirmDialog open={Boolean(revoking)} title="Revoke credential" message={`Revoke ${revoking?.name ?? "this credential"}? Existing clients using it will immediately lose access.`} confirmLabel="Revoke credential" tone="danger" busy={busy} onCancel={() => setRevoking(null)} onConfirm={() => void revokeCredential()} />
|
||||
<ConfirmDialog open={retiring} title="Retire service account" message={`Retire ${managing?.name ?? "this service account"} and revoke all ${managing?.active_credential_count ?? 0} active credentials?`} confirmLabel="Retire and revoke" tone="danger" busy={busy} onCancel={() => setRetiring(false)} onConfirm={() => void retire()} />
|
||||
<ConfirmDialog open={Boolean(revoking)} title="Revoke credential" message={`Revoke ${revoking?.name ?? "this credential"}? Existing clients using it will immediately lose access.`} confirmLabel="Revoke credential" tone="danger" busy={busy} helpContextId="access.service-accounts.confirm-revoke-credential" helpModuleId="access" onCancel={() => setRevoking(null)} onConfirm={() => void revokeCredential()} />
|
||||
<ConfirmDialog open={retiring} title="Retire service account" message={`Retire ${managing?.name ?? "this service account"} and revoke all ${managing?.active_credential_count ?? 0} active credentials?`} confirmLabel="Retire and revoke" tone="danger" busy={busy} helpContextId="access.service-accounts.confirm-retire" helpModuleId="access" onCancel={() => setRetiring(false)} onConfirm={() => void retire()} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user