importReact,{useRef,useState}from'react';importIconfrom'../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';exportdefaultfunctionAdmin({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('');constfile=useRef(null),administrator=canApprove(s);asyncfunctioncreateTeam(event){event.preventDefault();if (!administrator||busy)return;setError('');setBusy(true);try{if (teamPassword.length<6||adminPassword.length<6)thrownewError('Use at least six characters for each password.');constteamName=canonicalTeamName(team);const[userHash,administratorHash]=awaitPromise.all([hashPassword(teamPassword),hashPassword(adminPassword)]);if (!teamName||s.teams.some(t=>t.toLowerCase()===teamName.toLowerCase()))thrownewError('This team already exists.');if (userHash===administratorHash)thrownewError('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 system password can now be used on the sign-in screen.');}catch (failure){setError(failure.message);}finally{setBusy(false);}}asyncfunctionimportBackup(event){constuploaded=event.target.files?.[0];event.target.value='';if (!uploaded)return;try{if (uploaded.size>10*1024*1024)thrownewError('Use a backup under 10 MB.');constparsed=JSON.parse(awaituploaded.text());constkeys=['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)thrownewError('This file is not a compatible backup.');constrestored=upgradeDemoState(parsed);mutate(d=>{for (constkeyofObject.keys(d))deleted[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<><Headingeyebrow={`${s.session.team} · SHARED STUDIO`}title="Administration"description="Team access, source sharing, and connection settings."/><Tabsvalue={tab}onChange={setTab}items={['Teams & access','Knowledge sharing','Connections','Data & audit']}/>{tab==='Teams & access'?<divclassName="detail-grid"><sectionclassName="card padded"><h2>Configured teams</h2>{s.teams.map(t=><divclassName="history-row"key={t}><spanclassName="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><Badgetone="green">Active</Badge></div>)}<h3className="form-section-label">Add a team</h3>{administrator?<formonSubmit={createTeam}><Fieldlabel="New team name"><inputrequiredmaxLength={40}value={team}onChange={e=>setTeam(e.target.value)}placeholder="Team name"/></Field><Fieldlabel="System password"><inputtype="password"autoComplete="new-password"requiredminLength={6}value={teamPassword}onChange={e=>setTeamPassword(e.target.value)}/></Field><Fieldlabel="Administrator system password"><inputtype="password"autoComplete="new-password"requiredminLength={6}value={adminPassword}onChange={e=>setAdminPassword(e.target.value)}/></Field><pclassName="supporting-copy">Use different passwords. Both access levels sign in through the same team login.</p>{error&&<pclassName="form-error"role="alert">{error}</p>}<divclassName="modal-actions"><Buttonvariant="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><sectionclassName="card padded"><h2>Two access levels</h2><divclassName="role-row"><Iconname="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><divclassName="role-row"><Iconname="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><ButtononClick={()=>{mutate(d=>{d.session.authenticated=false;});navigate('login');}}>Sign out / change access</Button></section></div>:tab==='Knowledge sharing'?<sectionclassName="card padded"><h2>Explicit source sharing</h2><pclassName="supporting-copy">Share a source with another team while preserving its ownership and provenance.</p><divclassName="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=><trkey={g.id}><td>{s.documents.find(d=>d.id===g.sourceId)?.name||g.sourceId}</td><td>{g.owner}</td><td>{g.consumer}</td><td><Badgetone={g.status==='Active'?'green':'neutral'}>{g.status}</Badge></td><td><ButtononClick={()=>{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><formclassName="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.');}}><Fieldlabel="Source owned by current team"><selectrequiredvalue={source}onChange={e=>setSource(e.target.value)}><optionvalue="">Choose a source</option>{s.documents.filter(d=>d.team===s.session.team).map(d=><optionkey={d.id}value={d.id}>{d.name}</option>)}</select></Field><Fieldlabel="Consuming team"><selectvalue={consumer}onChange={e=>setConsumer(e.target.value)}>{s.teams.filter(t=>t!==s.session.team).map(t=><optionkey={t}>{t}</option>)}</select></Field><Buttontype="submit"variant="primary"disabled={!source||s.shares.some(g=>g.sourceId===source&&g.consumer===consumer)}>Save sharing grant</Button></form></section>:tab==='Connections'?<sectionclassName="card padded settings-card"><divclassName="section-heading inset"><h2>Model connection configuration</h2></div><formonSubmit={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])=><Fieldlabel={label}key={key}><inputtype={key==='gateway'?'url':'text'}value={settings[key]}onChange={e=>setSettings(v=>({...v,[key]:e.target.value}))}placeholder={placeholder}/></Field>)}<Fieldlabel="Bounded retry limit"><inputtype="number"min={0}max={5}value={settings.retryLimit}onChange={e=>setSettings(v=>({...v,retryLimit:Number(e.target.value)}))}/></Field><divclassName="modal-actions"><Buttontype="submit"variant="primary">Save connection settings</Button></div></form></section>:<><sectionclassName="card padded"><h2>Team workspace</h2><pclassName="supporting-copy">Back up your workspace records and review activity.</p><divclassName="row-actions margin-top"><Buttonicon="external"onClick={()=>download('ccil-studio-backup.json',JSON.stringify(s,null,2),'application/json')}>Export metadata backup</Button><Buttonicon="upload"onClick={()=>file.current.click()}>Import metadata backup</Button><ButtononClick={()=>setDialog('reset')}>Reset application records</Button><inputref={file}className="sr-only"type="file"accept=".json"onChange={importBackup}/></div>{error&&<pclassName="form-error"role="alert">{error}</p>}<Note>Metadata backups include source text and conversations. Original file bytes are not included.</Note></section><sectionclassName="card margin-top"><divclassName="section-heading"><h2>Audit trail</h2><spanclassName="count-pill">{s.audit.length}</span></div>{s.audit.slice(0,40).map(a=><divclassName="activity-row"key={a.id}><Iconname="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>{newDate(a.at).toLocaleString('en-IN')}</small></div>)}</section></>}{dialog==='reset'&&<Modaltitle="Reset application records?"close={()=>setDialog('')}><pclassName="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><divclassName="modal-actions"><ButtononClick={()=>setDialog('')}>Cancel</Button><Buttonvariant="primary"onClick={()=>{reset();setDialog('');navigate('login');}}>Reset application records</Button></div></Modal>}</>;}
Top comments (0)
Subscribe
We're a place where coders share, stay up-to-date and grow their careers.
Top comments (0)