DEV Community

Rahul
Rahul

Posted on Fully Autonomous

CCIL Test Studio source 16: src/pages/Admin.jsx

import React, { useRef, useState } from 'react';
import Icon from '../Icon.jsx';
import { canonicalTeamName, teamNames, upgradeDemoState } from '../seed.js';
import { addTeam, hashPassword } from '../access.js';
import { useStudio, canApprove, uid, log, download } from '../store.jsx';
import { Badge, Button, Empty, Field, Heading, Modal, Note, Tabs } from '../ui.jsx';

export default function Admin({ navigate }) {
  const { state: s, mutate, notify, reset } = useStudio();
  const [tab, setTab] = useState('Teams & access'), [team, setTeam] = useState('');
  const [teamPassword, setTeamPassword] = useState(''), [adminPassword, setAdminPassword] = useState(''), [busy, setBusy] = useState(false);
  const [source, setSource] = useState(''), [consumer, setConsumer] = useState(s.teams.find(t => t !== s.session.team) || '');
  const [settings, setSettings] = useState({ ...s.settings }), [dialog, setDialog] = useState(''), [error, setError] = useState('');
  const file = useRef(null), administrator = canApprove(s);
  async function createTeam(event) {
    event.preventDefault(); if (!administrator || busy) return;
    setError(''); setBusy(true);
    try {
      if (teamPassword.length < 6 || adminPassword.length < 6) throw new Error('Use at least six characters for each password.');
      const teamName = canonicalTeamName(team);
      const [userHash, administratorHash] = await Promise.all([hashPassword(teamPassword), hashPassword(adminPassword)]);
      if (!teamName || s.teams.some(t => t.toLowerCase() === teamName.toLowerCase())) throw new Error('This team already exists.');
      if (userHash === administratorHash) throw new Error('Team and administrator passwords must be different.');
      mutate(d => { addTeam(d, teamName, userHash, administratorHash); log(d, `Added team ${teamName}`); });
      setTeam(''); setTeamPassword(''); setAdminPassword(''); notify('Team created. Its shared password can now be used on the sign-in screen.');
    } catch (failure) { setError(failure.message); } finally { setBusy(false); }
  }
  async function importBackup(event) {
    const uploaded = event.target.files?.[0]; event.target.value = ''; if (!uploaded) return;
    try {
      if (uploaded.size > 10 * 1024 * 1024) throw new Error('Use a backup under 10 MB.');
      const parsed = JSON.parse(await uploaded.text());
      const keys = ['teams', 'workspaces', 'baselines', 'documents', 'crs', 'questions', 'suites', 'templates', 'candidates', 'shares', 'audit', 'jobs'];
      if (parsed.schema !== 1 || keys.some(key => !Array.isArray(parsed[key])) || !parsed.session || !parsed.teams.includes(parsed.session.team) || !parsed.threads || !parsed.settings) throw new Error('This file is not a compatible backup.');
      const restored = upgradeDemoState(parsed);
      mutate(d => { for (const key of Object.keys(d)) delete d[key]; Object.assign(d, restored); d.jobs = d.jobs.map(j => j.status === 'running' ? { ...j, status: 'Interrupted', stage: 'Imported run. Retry to continue.' } : j); });
      notify('Backup restored.'); setError('');
    } catch (failure) { setError(failure.message); }
  }
  return <><Heading eyebrow={`${s.session.team} · SHARED STUDIO`} title="Administration" description="Team access, source sharing, and connection settings."/>
    <Tabs value={tab} onChange={setTab} items={['Teams & access', 'Knowledge sharing', 'Connections', 'Data & audit']}/>
    {tab === 'Teams & access' ? <div className="detail-grid"><section className="card padded"><h2>Configured teams</h2>
      {s.teams.map(t => <div className="history-row" key={t}><span className="team-avatar">{t[0]}</span><span><strong>{t}</strong>{teamNames[t] && <small>{teamNames[t]}</small>}<small>{s.workspaces.filter(w => w.team === t).length} workspaces · {s.baselines.filter(b => b.team === t).length} System Baselines</small></span><Badge tone="green">Active</Badge></div>)}
      <h3 className="form-section-label">Add a team</h3>
      {administrator ? <form onSubmit={createTeam}><Field label="New team name"><input required maxLength={40} value={team} onChange={e => setTeam(e.target.value)} placeholder="Team name"/></Field><Field label="Shared team password"><input type="password" autoComplete="new-password" required minLength={6} value={teamPassword} onChange={e => setTeamPassword(e.target.value)}/></Field><Field label="Administrator password"><input type="password" autoComplete="new-password" required minLength={6} value={adminPassword} onChange={e => setAdminPassword(e.target.value)}/></Field><p className="supporting-copy">Use different passwords. Both access levels sign in through the same team login.</p>{error && <p className="form-error" role="alert">{error}</p>}<div className="modal-actions"><Button variant="primary" type="submit" icon="plus" disabled={busy}>{busy ? 'Adding…' : 'Add team'}</Button></div></form> : <Note>Only an Administrator can add a new team. All existing teams use shared team sign-in.</Note>}
    </section><section className="card padded"><h2>Two access levels</h2><div className="role-row"><Icon name="users" size={19}/><div><strong>User</strong><p>Create System Baselines and workspaces, upload documents, resolve questions, generate and edit tests, manage templates, and request baseline updates.</p></div></div><div className="role-row"><Icon name="shield" size={19}/><div><strong>Administrator</strong><p>Everything a User can do, plus add teams and approve or reject System Baseline updates.</p></div></div><Note>Everyone signed in to a team shares the same working capabilities. Only System Baseline updates need approval.</Note><Button onClick={() => { mutate(d => { d.session.authenticated = false; }); navigate('login'); }}>Sign out / change access</Button></section></div>
    : tab === 'Knowledge sharing' ? <section className="card padded"><h2>Explicit source sharing</h2><p className="supporting-copy">Share a source with another team while preserving its ownership and provenance.</p><div className="table-scroll"><table><thead><tr><th>SOURCE</th><th>OWNER</th><th>CONSUMER</th><th>STATUS</th><th>ACTION</th></tr></thead><tbody>{s.shares.map(g => <tr key={g.id}><td>{s.documents.find(d => d.id === g.sourceId)?.name || g.sourceId}</td><td>{g.owner}</td><td>{g.consumer}</td><td><Badge tone={g.status === 'Active' ? 'green' : 'neutral'}>{g.status}</Badge></td><td><Button onClick={() => { mutate(d => { d.shares.find(item => item.id === g.id).status = g.status === 'Active' ? 'Revoked' : 'Active'; log(d, `Updated sharing for ${g.sourceId}`); }); notify('Sharing updated.'); }}>{g.status === 'Active' ? 'Revoke' : 'Restore'}</Button></td></tr>)}</tbody></table></div><form className="sharing-form margin-top" onSubmit={e => { e.preventDefault(); if (!source || !consumer) return; mutate(d => { d.shares.push({ id: uid('share'), sourceId: source, owner: s.session.team, consumer, status: 'Active' }); log(d, `Shared source with ${consumer}`); }); setSource(''); notify('Sharing grant saved.'); }}><Field label="Source owned by current team"><select required value={source} onChange={e => setSource(e.target.value)}><option value="">Choose a source</option>{s.documents.filter(d => d.team === s.session.team).map(d => <option key={d.id} value={d.id}>{d.name}</option>)}</select></Field><Field label="Consuming team"><select value={consumer} onChange={e => setConsumer(e.target.value)}>{s.teams.filter(t => t !== s.session.team).map(t => <option key={t}>{t}</option>)}</select></Field><Button type="submit" variant="primary" disabled={!source || s.shares.some(g => g.sourceId === source && g.consumer === consumer)}>Save sharing grant</Button></form></section>
    : tab === 'Connections' ? <section className="card padded settings-card"><div className="section-heading inset"><h2>Model connection configuration</h2></div><form onSubmit={e => { e.preventDefault(); mutate(d => { d.settings = settings; log(d, 'Saved connection settings'); }); notify('Connection settings saved.'); }}>{[['Model gateway URL', 'gateway', 'https://your-internal-gateway'], ['Generation / reasoning model ID', 'reasoningModel', 'Configured endpoint model identifier'], ['Classification / extraction model ID', 'extractionModel', 'Configured endpoint model identifier'], ['Embedding model ID', 'embeddingModel', 'Pinned embedding profile identifier']].map(([label, key, placeholder]) => <Field label={label} key={key}><input type={key === 'gateway' ? 'url' : 'text'} value={settings[key]} onChange={e => setSettings(v => ({ ...v, [key]: e.target.value }))} placeholder={placeholder}/></Field>)}<Field label="Bounded retry limit"><input type="number" min={0} max={5} value={settings.retryLimit} onChange={e => setSettings(v => ({ ...v, retryLimit: Number(e.target.value) }))}/></Field><div className="modal-actions"><Button type="submit" variant="primary">Save connection settings</Button></div></form></section>
    : <><section className="card padded"><h2>Team workspace</h2><p className="supporting-copy">Back up your workspace records and review activity.</p><div className="row-actions margin-top"><Button icon="external" onClick={() => download('ccil-studio-backup.json', JSON.stringify(s, null, 2), 'application/json')}>Export metadata backup</Button><Button icon="upload" onClick={() => file.current.click()}>Import metadata backup</Button><Button onClick={() => setDialog('reset')}>Reset application records</Button><input ref={file} className="sr-only" type="file" accept=".json" onChange={importBackup}/></div>{error && <p className="form-error" role="alert">{error}</p>}<Note>Metadata backups include source text and conversations. Original file bytes are not included.</Note></section><section className="card margin-top"><div className="section-heading"><h2>Audit trail</h2><span className="count-pill">{s.audit.length}</span></div>{s.audit.slice(0, 40).map(a => <div className="activity-row" key={a.id}><Icon name="clock" size={17}/><div><strong>{a.action.replace(/Team Baseline/g, 'System Baseline').replace(/local demo/gi, 'studio')}</strong><p>{a.actor === 'Demo fixture' ? 'System' : a.actor} · {a.team || 'System'}</p></div><small>{new Date(a.at).toLocaleString('en-IN')}</small></div>)}</section></>}
    {dialog === 'reset' && <Modal title="Reset application records?" close={() => setDialog('')}><p className="modal-intro">This replaces application records with synthetic samples. Export a metadata backup first to retain your changes.</p><Note>Original uploaded files are retained in browser storage.</Note><div className="modal-actions"><Button onClick={() => setDialog('')}>Cancel</Button><Button variant="primary" onClick={() => { reset(); setDialog(''); navigate('login'); }}>Reset application records</Button></div></Modal>}
  </>;
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)