feat: replace UI with design system example pages wired to real API
Migrate all page components from the @cameleer/design-system v0.0.3 example UI, replacing mock data with real backend API hooks. This brings richer visuals (KpiStrip, GroupCard, RouteFlow, ProcessorTimeline, DateRangePicker, expandable rows) while preserving all existing API integration, auth, and routing infrastructure. Pages migrated: Dashboard, RoutesMetrics, RouteDetail, ExchangeDetail, AgentHealth, AgentInstance, OidcConfig, AuditLog, RBAC (Users/Groups/Roles). Also enhanced LayoutShell CommandPalette with real search data from catalog. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,110 +1,226 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button, Input, Toggle, FormField, Card, Alert, SectionHeader, Tag, ConfirmDialog } from '@cameleer/design-system';
|
||||
import {
|
||||
Button, Input, Toggle, FormField, SectionHeader, Tag, ConfirmDialog, Alert,
|
||||
} from '@cameleer/design-system';
|
||||
import { useToast } from '@cameleer/design-system';
|
||||
import { adminFetch } from '../../api/queries/admin/admin-api';
|
||||
import styles from './OidcConfigPage.module.css';
|
||||
|
||||
interface OidcConfig {
|
||||
interface OidcFormData {
|
||||
enabled: boolean;
|
||||
autoSignup: boolean;
|
||||
issuerUri: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
rolesClaim: string;
|
||||
defaultRoles: string[];
|
||||
autoSignup: boolean;
|
||||
displayNameClaim: string;
|
||||
defaultRoles: string[];
|
||||
}
|
||||
|
||||
const EMPTY_CONFIG: OidcFormData = {
|
||||
enabled: false,
|
||||
autoSignup: true,
|
||||
issuerUri: '',
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
rolesClaim: 'roles',
|
||||
displayNameClaim: 'name',
|
||||
defaultRoles: ['VIEWER'],
|
||||
};
|
||||
|
||||
export default function OidcConfigPage() {
|
||||
const [config, setConfig] = useState<OidcConfig | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [form, setForm] = useState<OidcFormData | null>(null);
|
||||
const [newRole, setNewRole] = useState('');
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
adminFetch<OidcConfig>('/oidc')
|
||||
.then(setConfig)
|
||||
.catch(() => setConfig({ enabled: false, issuerUri: '', clientId: '', clientSecret: '', rolesClaim: 'roles', defaultRoles: ['VIEWER'], autoSignup: true, displayNameClaim: 'name' }));
|
||||
adminFetch<OidcFormData>('/oidc')
|
||||
.then(setForm)
|
||||
.catch(() => setForm(EMPTY_CONFIG));
|
||||
}, []);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!config) return;
|
||||
function update<K extends keyof OidcFormData>(key: K, value: OidcFormData[K]) {
|
||||
setForm((prev) => prev ? { ...prev, [key]: value } : prev);
|
||||
}
|
||||
|
||||
function addRole() {
|
||||
if (!form) return;
|
||||
const role = newRole.trim().toUpperCase();
|
||||
if (role && !form.defaultRoles.includes(role)) {
|
||||
update('defaultRoles', [...form.defaultRoles, role]);
|
||||
setNewRole('');
|
||||
}
|
||||
}
|
||||
|
||||
function removeRole(role: string) {
|
||||
if (!form) return;
|
||||
update('defaultRoles', form.defaultRoles.filter((r) => r !== role));
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form) return;
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
await adminFetch('/oidc', { method: 'PUT', body: JSON.stringify(config) });
|
||||
setSuccess(true);
|
||||
setTimeout(() => setSuccess(false), 3000);
|
||||
await adminFetch('/oidc', { method: 'PUT', body: JSON.stringify(form) });
|
||||
toast({ title: 'Settings saved', description: 'OIDC configuration updated successfully.', variant: 'success' });
|
||||
} catch (e: any) {
|
||||
setError(e.message);
|
||||
toast({ title: 'Save failed', description: e.message, variant: 'error' });
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
async function handleTest() {
|
||||
if (!form) return;
|
||||
setTesting(true);
|
||||
setError(null);
|
||||
try {
|
||||
await adminFetch('/oidc', { method: 'DELETE' });
|
||||
setConfig({ enabled: false, issuerUri: '', clientId: '', clientSecret: '', rolesClaim: 'roles', defaultRoles: ['VIEWER'], autoSignup: true, displayNameClaim: 'name' });
|
||||
const result = await adminFetch<{ status: string; authorizationEndpoint?: string }>('/oidc/test', { method: 'POST' });
|
||||
toast({ title: 'Connection test', description: `OIDC provider responded: ${result.status}`, variant: 'success' });
|
||||
} catch (e: any) {
|
||||
setError(e.message);
|
||||
toast({ title: 'Connection test failed', description: e.message, variant: 'error' });
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!config) return null;
|
||||
async function handleDelete() {
|
||||
setDeleteOpen(false);
|
||||
setError(null);
|
||||
try {
|
||||
await adminFetch('/oidc', { method: 'DELETE' });
|
||||
setForm(EMPTY_CONFIG);
|
||||
toast({ title: 'Configuration deleted', description: 'OIDC configuration has been removed.', variant: 'warning' });
|
||||
} catch (e: any) {
|
||||
setError(e.message);
|
||||
toast({ title: 'Delete failed', description: e.message, variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
if (!form) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 style={{ marginBottom: '1rem' }}>OIDC Configuration</h2>
|
||||
<Card>
|
||||
<div style={{ padding: '1.5rem', display: 'grid', gap: '1rem' }}>
|
||||
<Toggle checked={config.enabled} onChange={(e) => setConfig({ ...config, enabled: e.target.checked })} label="Enable OIDC" />
|
||||
<FormField label="Issuer URI"><Input value={config.issuerUri} onChange={(e) => setConfig({ ...config, issuerUri: e.target.value })} /></FormField>
|
||||
<FormField label="Client ID"><Input value={config.clientId} onChange={(e) => setConfig({ ...config, clientId: e.target.value })} /></FormField>
|
||||
<FormField label="Client Secret"><Input type="password" value={config.clientSecret} onChange={(e) => setConfig({ ...config, clientSecret: e.target.value })} /></FormField>
|
||||
<FormField label="Roles Claim"><Input value={config.rolesClaim} onChange={(e) => setConfig({ ...config, rolesClaim: e.target.value })} /></FormField>
|
||||
<FormField label="Display Name Claim"><Input value={config.displayNameClaim} onChange={(e) => setConfig({ ...config, displayNameClaim: e.target.value })} /></FormField>
|
||||
<Toggle checked={config.autoSignup} onChange={(e) => setConfig({ ...config, autoSignup: e.target.checked })} label="Auto Signup" />
|
||||
<div className={styles.page}>
|
||||
<div className={styles.toolbar}>
|
||||
<Button size="sm" variant="secondary" onClick={handleTest} disabled={!form.issuerUri || testing}>
|
||||
{testing ? 'Testing...' : 'Test Connection'}
|
||||
</Button>
|
||||
<Button size="sm" variant="primary" onClick={handleSave} disabled={saving}>
|
||||
{saving ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
<h3>Default Roles</h3>
|
||||
<div className={styles.tagRow}>
|
||||
{(config.defaultRoles || []).map(role => (
|
||||
<Tag key={role} label={role} onRemove={() => {
|
||||
setConfig(prev => ({ ...prev!, defaultRoles: prev!.defaultRoles.filter(r => r !== role) }));
|
||||
}} />
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.addRow}>
|
||||
<Input placeholder="Add role..." value={newRole} onChange={e => setNewRole(e.target.value)} />
|
||||
<Button onClick={() => {
|
||||
if (newRole.trim() && !config.defaultRoles?.includes(newRole.trim())) {
|
||||
setConfig(prev => ({ ...prev!, defaultRoles: [...(prev!.defaultRoles || []), newRole.trim()] }));
|
||||
setNewRole('');
|
||||
}
|
||||
}}>Add</Button>
|
||||
</div>
|
||||
</div>
|
||||
{error && <div style={{ marginBottom: 16 }}><Alert variant="error">{error}</Alert></div>}
|
||||
|
||||
<div style={{ display: 'flex', gap: '0.75rem' }}>
|
||||
<Button variant="primary" onClick={handleSave} disabled={saving}>{saving ? 'Saving...' : 'Save'}</Button>
|
||||
<Button variant="danger" onClick={() => setDeleteOpen(true)}>Delete Configuration</Button>
|
||||
</div>
|
||||
|
||||
{error && <Alert variant="error">{error}</Alert>}
|
||||
{success && <Alert variant="success">Configuration saved</Alert>}
|
||||
<section className={styles.section}>
|
||||
<SectionHeader>Behavior</SectionHeader>
|
||||
<div className={styles.toggleRow}>
|
||||
<Toggle
|
||||
label="Enabled"
|
||||
checked={form.enabled}
|
||||
onChange={(e) => update('enabled', e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<div className={styles.toggleRow}>
|
||||
<Toggle
|
||||
label="Auto Sign-Up"
|
||||
checked={form.autoSignup}
|
||||
onChange={(e) => update('autoSignup', e.target.checked)}
|
||||
/>
|
||||
<span className={styles.hint}>Automatically create accounts for new OIDC users</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<ConfirmDialog
|
||||
open={deleteOpen}
|
||||
onClose={() => setDeleteOpen(false)}
|
||||
onConfirm={handleDelete}
|
||||
title="Delete OIDC Configuration"
|
||||
message="Delete OIDC configuration? All OIDC users will lose access."
|
||||
confirmText="DELETE"
|
||||
/>
|
||||
<section className={styles.section}>
|
||||
<SectionHeader>Provider Settings</SectionHeader>
|
||||
<FormField label="Issuer URI" htmlFor="issuer">
|
||||
<Input
|
||||
id="issuer"
|
||||
type="url"
|
||||
placeholder="https://idp.example.com/realms/my-realm"
|
||||
value={form.issuerUri}
|
||||
onChange={(e) => update('issuerUri', e.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Client ID" htmlFor="client-id">
|
||||
<Input
|
||||
id="client-id"
|
||||
value={form.clientId}
|
||||
onChange={(e) => update('clientId', e.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Client Secret" htmlFor="client-secret">
|
||||
<Input
|
||||
id="client-secret"
|
||||
type="password"
|
||||
value={form.clientSecret}
|
||||
onChange={(e) => update('clientSecret', e.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<SectionHeader>Claim Mapping</SectionHeader>
|
||||
<FormField label="Roles Claim" htmlFor="roles-claim" hint="JSON path to roles in the ID token">
|
||||
<Input
|
||||
id="roles-claim"
|
||||
value={form.rolesClaim}
|
||||
onChange={(e) => update('rolesClaim', e.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Display Name Claim" htmlFor="name-claim" hint="Claim used for user display name">
|
||||
<Input
|
||||
id="name-claim"
|
||||
value={form.displayNameClaim}
|
||||
onChange={(e) => update('displayNameClaim', e.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<SectionHeader>Default Roles</SectionHeader>
|
||||
<div className={styles.tagList}>
|
||||
{form.defaultRoles.map((role) => (
|
||||
<Tag key={role} label={role} color="primary" onRemove={() => removeRole(role)} />
|
||||
))}
|
||||
{form.defaultRoles.length === 0 && (
|
||||
<span className={styles.noRoles}>No default roles configured</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.addRoleRow}>
|
||||
<Input
|
||||
placeholder="Add role..."
|
||||
value={newRole}
|
||||
onChange={(e) => setNewRole(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addRole(); } }}
|
||||
className={styles.roleInput}
|
||||
/>
|
||||
<Button size="sm" variant="secondary" onClick={addRole} disabled={!newRole.trim()}>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<SectionHeader>Danger Zone</SectionHeader>
|
||||
<Button size="sm" variant="danger" onClick={() => setDeleteOpen(true)}>
|
||||
Delete OIDC Configuration
|
||||
</Button>
|
||||
<ConfirmDialog
|
||||
open={deleteOpen}
|
||||
onClose={() => setDeleteOpen(false)}
|
||||
onConfirm={handleDelete}
|
||||
message="Delete OIDC configuration? All users signed in via OIDC will lose access."
|
||||
confirmText="delete oidc"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user