feat: migrate UI to @cameleer/design-system, add backend endpoints
Backend: - Add agent_events table (V5) and lifecycle event recording - Add route catalog endpoint (GET /routes/catalog) - Add route metrics endpoint (GET /routes/metrics) - Add agent events endpoint (GET /agents/events-log) - Enrich AgentInstanceResponse with tps, errorRate, activeRoutes, uptimeSeconds - Add TimescaleDB retention/compression policies (V6) Frontend: - Replace custom Mission Control UI with @cameleer/design-system components - Rebuild all pages: Dashboard, ExchangeDetail, RoutesMetrics, AgentHealth, AgentInstance, RBAC, AuditLog, OIDC, DatabaseAdmin, OpenSearchAdmin, Swagger - New LayoutShell with design system AppShell, Sidebar, TopBar, CommandPalette - Consume design system from Gitea npm registry (@cameleer/design-system@0.0.1) - Add .npmrc for scoped registry, update Dockerfile with REGISTRY_TOKEN arg CI: - Pass REGISTRY_TOKEN build-arg to UI Docker build step Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import { type FormEvent, useEffect, useState } from 'react';
|
||||
import { Navigate } from 'react-router';
|
||||
import { useAuthStore } from './auth-store';
|
||||
import { api } from '../api/client';
|
||||
import styles from './LoginPage.module.css';
|
||||
import { Card, Input, Button, Alert, FormField } from '@cameleer/design-system';
|
||||
|
||||
interface OidcInfo {
|
||||
clientId: string;
|
||||
@@ -50,62 +50,54 @@ export function LoginPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<form className={styles.card} onSubmit={handleSubmit}>
|
||||
<div className={styles.logo}>
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2" />
|
||||
<path d="M12 6v6l4 2" />
|
||||
</svg>
|
||||
cameleer3
|
||||
</div>
|
||||
<div className={styles.subtitle}>Sign in to access the observability dashboard</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: 'var(--surface-ground)' }}>
|
||||
<Card>
|
||||
<form onSubmit={handleSubmit} style={{ padding: '2rem', minWidth: 360 }}>
|
||||
<div style={{ textAlign: 'center', marginBottom: '1.5rem' }}>
|
||||
<h1 style={{ fontSize: '1.5rem', fontWeight: 600 }}>cameleer3</h1>
|
||||
<p style={{ color: 'var(--text-secondary)', marginTop: '0.25rem', fontSize: '0.875rem' }}>
|
||||
Sign in to access the observability dashboard
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{oidc && (
|
||||
<>
|
||||
<button
|
||||
className={styles.ssoButton}
|
||||
type="button"
|
||||
onClick={handleOidcLogin}
|
||||
disabled={oidcLoading}
|
||||
>
|
||||
{oidcLoading ? 'Redirecting...' : 'Sign in with SSO'}
|
||||
</button>
|
||||
<div className={styles.divider}>
|
||||
<span className={styles.dividerText}>or</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{oidc && (
|
||||
<>
|
||||
<Button variant="secondary" onClick={handleOidcLogin} disabled={oidcLoading} style={{ width: '100%', marginBottom: '1rem' }}>
|
||||
{oidcLoading ? 'Redirecting...' : 'Sign in with SSO'}
|
||||
</Button>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', margin: '1rem 0' }}>
|
||||
<hr style={{ flex: 1, border: 'none', borderTop: '1px solid var(--border)' }} />
|
||||
<span style={{ color: 'var(--text-tertiary)', fontSize: '0.75rem' }}>or</span>
|
||||
<hr style={{ flex: 1, border: 'none', borderTop: '1px solid var(--border)' }} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label}>Username</label>
|
||||
<input
|
||||
className={styles.input}
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
autoFocus
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
<FormField label="Username">
|
||||
<Input
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
autoFocus
|
||||
autoComplete="username"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label}>Password</label>
|
||||
<input
|
||||
className={styles.input}
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
<FormField label="Password">
|
||||
<Input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<button className={styles.submit} type="submit" disabled={loading || !username || !password}>
|
||||
{loading ? 'Signing in...' : 'Sign In'}
|
||||
</button>
|
||||
<Button variant="primary" disabled={loading || !username || !password} style={{ width: '100%', marginTop: '0.5rem' }}>
|
||||
{loading ? 'Signing in...' : 'Sign In'}
|
||||
</Button>
|
||||
|
||||
{error && <div className={styles.error}>{error}</div>}
|
||||
</form>
|
||||
{error && <div style={{ marginTop: '1rem' }}><Alert variant="error">{error}</Alert></div>}
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user