import { For, Show, createSignal } from "solid-js"; import type { ApiKey, InstanceSetting } from "@primora/api-client"; import { Badge } from "../components/Badge"; import { Modal, ModalFooter } from "../components/Modal"; import { Input } from "../components/Input"; import { IconPlus, IconKey, IconCopy, IconCheck, IconTrash, IconAlert, } from "../components/Icons"; interface OrgInput { name: string; slug: string; } const API_KEY_SCOPES: Array<{ value: string; label: string; hint: string }> = [ { value: "ingest", label: "ingest", hint: "telemetry ingest only" }, { value: "read", label: "read", hint: "read endpoints" }, { value: "write", label: "write", hint: "read + ingest + mutations" }, { value: "admin", label: "admin", hint: "full access" }, ]; interface SettingsPageProps { isPlatformAdmin: boolean; instanceSettings?: InstanceSetting[]; instancePending: boolean; instanceMessage: string; onSaveInstanceSetting: (key: string, value: unknown) => void; onResetInstanceSetting: (key: string) => void; apiKeys?: ApiKey[]; apiKeyName: string; apiKeyScopes: string[]; apiKeySecret: string; apiKeyMessage: string; apiKeyPending: boolean; organizationInput: OrgInput; organizationEditInput: OrgInput; organizationMessage: string; canUpdateOrganization: boolean; workspacePending: boolean; hasActiveOrganization: boolean; onApiKeyNameChange: (name: string) => void; onApiKeyScopeToggle: (scope: string) => void; onCreateApiKey: () => void; onDeleteApiKey: (id: string) => void; onOrganizationInputChange: (field: keyof OrgInput, value: string) => void; onOrganizationEditInputChange: (field: keyof OrgInput, value: string) => void; onCreateOrganization: (event: SubmitEvent) => void; onUpdateOrganization: (event: SubmitEvent) => void; onDeleteOrganization: () => void; onDismissSecret: () => void; formatDate: (value?: string | null) => string; } const INSTANCE_GROUPS: { prefix: string; title: string; hint: string }[] = [ { prefix: "auth.", title: "Authentication", hint: "Who can create accounts on this instance." }, { prefix: "mail.", title: "Mail", hint: "Outgoing email — Resend or SMTP. In-app values override .env." }, { prefix: "ratelimit.", title: "Rate limits", hint: "Requests per minute, enforced by the API." }, ]; const SETTING_LABELS: Record = { "auth.signup_enabled": "Public sign-up", "mail.from": "From address", "mail.resend_api_key": "Resend API key", "mail.smtp_host": "SMTP host", "mail.smtp_port": "SMTP port", "mail.smtp_user": "SMTP user", "mail.smtp_password": "SMTP password", "mail.smtp_secure": "SMTP TLS", "ratelimit.user_per_minute": "User requests / minute", "ratelimit.api_key_per_minute": "API key requests / minute", }; export function SettingsPage(props: SettingsPageProps) { const [tab, setTab] = createSignal<"api-keys" | "organization" | "instance">("api-keys"); const [copied, setCopied] = createSignal(false); const [createOrgOpen, setCreateOrgOpen] = createSignal(false); const [confirmDeleteOrg, setConfirmDeleteOrg] = createSignal(false); const copySecret = async () => { try { await navigator.clipboard.writeText(props.apiKeySecret); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { /* clipboard unavailable */ } }; const activeKeys = () => (props.apiKeys ?? []).filter((k) => !k.revoked_at); const revokedKeys = () => (props.apiKeys ?? []).filter((k) => k.revoked_at); return (
{/* ---------------- API keys ---------------- */}
{props.apiKeyMessage}
Create API key Keys authenticate requests with the X-API-Key header. The secret is shown once.
props.onApiKeyNameChange(e.currentTarget.value)} />
Scopes
{(scope) => ( )}
0} fallback={

No API keys

Create a key to authenticate API requests from your application.

} >
{(key) => ( )}
Name Prefix Scopes Last used Status
{key.name} {key.prefix}…
0 ? key.scopes : ["admin"]}> {(scope) => {scope}}
{key.last_used_at ? props.formatDate(key.last_used_at) : "Never"} {key.revoked_at ? "Revoked" : "Active"}
0}>
{revokedKeys().length} revoked key{revokedKeys().length === 1 ? "" : "s"} retained for audit.
{/* ---------------- Organization ---------------- */}
{props.organizationMessage}
Organization settings Rename the active organization or change its slug.
props.onOrganizationEditInputChange("name", e.currentTarget.value) } disabled={!props.canUpdateOrganization} required /> props.onOrganizationEditInputChange("slug", e.currentTarget.value) } disabled={!props.canUpdateOrganization} required />

You need the owner or admin role to edit this organization.

New organization

Organizations are top-level workspaces containing projects and members.

Danger zone

Deleting the organization permanently removes all projects, buckets, objects, members, invitations, API keys and audit history.

{/* ---------------- Instance (platform admin) ---------------- */}
{props.instanceMessage}

In-app values override the matching environment variables. Secrets are stored encrypted and never shown back.

{(group) => { const rows = () => (props.instanceSettings ?? []).filter((s) => s.key.startsWith(group.prefix)); return ( 0}>
{group.title} {group.hint}
{(setting) => ( props.onSaveInstanceSetting(setting.key, value)} onReset={() => props.onResetInstanceSetting(setting.key)} /> )}
); }}

Loading instance settings

{/* API key secret modal — shown while App holds a fresh secret */}
{props.apiKeySecret}
{/* create organization modal */} setCreateOrgOpen(false)} title="Create organization" >
{ props.onCreateOrganization(e); setCreateOrgOpen(false); }} class="space-y-4" > props.onOrganizationInputChange("name", e.currentTarget.value)} required /> props.onOrganizationInputChange("slug", e.currentTarget.value)} required />
{/* delete org confirm */} setConfirmDeleteOrg(false)} title="Delete organization" size="sm" >

This permanently deletes the organization and everything inside it. This cannot be undone.

); } /** Values arrive as JSON strings ("true", "240") — decode per declared type. */ function settingText(setting: InstanceSetting): string { if (setting.value === undefined || setting.value === null) return ""; return String(setting.value); } const SOURCE_BADGE: Record = { app: "success", env: "primary", default: "neutral", }; function SettingRow(props: { setting: InstanceSetting; pending: boolean; onSave: (value: unknown) => void; onReset: () => void; }) { const s = () => props.setting; const [draft, setDraft] = createSignal(settingText(props.setting)); const dirty = () => { if (s().type === "bool") return false; // toggles save instantly return draft() !== settingText(s()); }; const saveDraft = () => { if (s().type === "int") { const n = Number(draft()); if (!Number.isInteger(n) || n < 0) return; props.onSave(n); return; } props.onSave(draft()); }; return (
{SETTING_LABELS[s().key] ?? s().key}
{s().key}
{s().is_set ? "in-app" : s().source} setDraft(e.currentTarget.value)} disabled={props.pending} aria-label={SETTING_LABELS[s().key] ?? s().key} />
} > ); }