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 Source:https://dev.to/iamry02/ccil-test-studio-source-16-srcpagesadminjsx-28hh

Related posts