Major changes: logic add to panel, local variables, panel histor, users management...

This commit is contained in:
Martino Ferrari
2026-06-18 17:37:04 +02:00
parent 71430bc3b0
commit aba394b84d
54 changed files with 6104 additions and 1166 deletions
+181
View File
@@ -0,0 +1,181 @@
import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import type { PanelACL, Grant, Folder } from './lib/types';
interface Props {
ifaceId: string;
ifaceName: string;
folders: Folder[];
onClose: () => void;
onSaved: () => void;
}
// ShareDialog edits a single panel's sharing settings: its folder, public
// visibility, and explicit per-user / per-group grants. Only the owner can
// reach it (the caller is gated upstream in InterfaceList).
export default function ShareDialog({ ifaceId, ifaceName, folders, onClose, onSaved }: Props) {
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
const [owner, setOwner] = useState('');
const [folder, setFolder] = useState('');
const [pub, setPub] = useState<'' | 'read' | 'write'>('');
const [grants, setGrants] = useState<Grant[]>([]);
const [userGroups, setUserGroups] = useState<string[]>([]);
// New-grant row state.
const [newKind, setNewKind] = useState<'user' | 'group'>('user');
const [newName, setNewName] = useState('');
const [newPerm, setNewPerm] = useState<'read' | 'write'>('read');
useEffect(() => {
Promise.all([
fetch(`/api/v1/interfaces/${encodeURIComponent(ifaceId)}/acl`).then(r => r.ok ? r.json() : null),
fetch('/api/v1/usergroups').then(r => r.ok ? r.json() : []),
])
.then(([acl, groups]: [PanelACL | null, string[]]) => {
if (acl) {
setOwner(acl.owner || '');
setFolder(acl.folder || '');
setPub(acl.public || '');
setGrants(Array.isArray(acl.grants) ? acl.grants : []);
}
setUserGroups(Array.isArray(groups) ? groups : []);
})
.catch(() => setError('Failed to load sharing settings.'))
.finally(() => setLoading(false));
}, [ifaceId]);
function addGrant() {
const name = newName.trim();
if (!name) return;
// Replace an existing grant for the same kind+name rather than duplicating.
const next = grants.filter(g => !(g.kind === newKind && g.name === name));
next.push({ kind: newKind, name, perm: newPerm });
setGrants(next);
setNewName('');
}
function removeGrant(idx: number) {
setGrants(grants.filter((_, i) => i !== idx));
}
async function save() {
setSaving(true);
setError('');
try {
const res = await fetch(`/api/v1/interfaces/${encodeURIComponent(ifaceId)}/acl`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ folder, public: pub, grants }),
});
if (!res.ok) {
const msg = await res.json().catch(() => null);
throw new Error(msg?.error || `Save failed (${res.status})`);
}
onSaved();
onClose();
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
setSaving(false);
}
}
return (
<div class="wizard-backdrop" onClick={onClose}>
<div class="wizard" onClick={e => e.stopPropagation()} style="max-width: 560px;">
<div class="wizard-header">
<span>Share {ifaceName || ifaceId}</span>
<button class="icon-btn" onClick={onClose}></button>
</div>
<div class="wizard-body">
{loading ? (
<p class="hint">Loading</p>
) : (
<div>
{owner && (
<p class="hint" style="padding: 0 0 0.75rem 0;">Owner: <strong>{owner}</strong></p>
)}
<div class="wizard-section-title">Folder</div>
<div class="wizard-field">
<select class="prop-select" value={folder} onChange={e => setFolder((e.target as HTMLSelectElement).value)}>
<option value="">(root no folder)</option>
{folders.map(f => (
<option key={f.id} value={f.id}>{f.name}</option>
))}
</select>
</div>
<div class="wizard-section-title" style="margin-top: 1rem;">Public access</div>
<div class="wizard-field">
<select class="prop-select" value={pub} onChange={e => setPub((e.target as HTMLSelectElement).value as any)}>
<option value="">Private (only owner & shares)</option>
<option value="read">Everyone can view</option>
<option value="write">Everyone can edit</option>
</select>
</div>
<div class="wizard-section-title" style="margin-top: 1rem;">Shared with</div>
{grants.length === 0 ? (
<p class="hint" style="padding: 0 0 0.5rem 0;">Not shared with anyone yet.</p>
) : (
<ul class="iface-list" style="margin-bottom: 0.5rem;">
{grants.map((g, idx) => (
<li key={`${g.kind}:${g.name}`} class="iface-item" style="cursor: default;">
<span>
<span style="font-size: 0.65rem; background: #1e293b; color: #94a3b8; padding: 0 4px; border-radius: 3px; border: 1px solid #334155; margin-right: 6px;">
{g.kind}
</span>
<span style="color: #e2e8f0;">{g.name}</span>
<span style="color: #64748b; margin-left: 6px;">({g.perm})</span>
</span>
<button class="icon-btn iface-delete" title="Remove" onClick={() => removeGrant(idx)}></button>
</li>
))}
</ul>
)}
<div class="wizard-field wizard-field-row" style="gap: 0.5rem; align-items: center;">
<select class="prop-select" style="flex: 0 0 80px;" value={newKind} onChange={e => { setNewKind((e.target as HTMLSelectElement).value as any); setNewName(''); }}>
<option value="user">User</option>
<option value="group">Group</option>
</select>
{newKind === 'group' ? (
<select class="prop-select" style="flex: 1;" value={newName} onChange={e => setNewName((e.target as HTMLSelectElement).value)}>
<option value="">Select group</option>
{userGroups.map(g => <option key={g} value={g}>{g}</option>)}
</select>
) : (
<input
class="prop-input"
style="flex: 1;"
placeholder="username"
value={newName}
onInput={e => setNewName((e.target as HTMLInputElement).value)}
onKeyDown={e => e.key === 'Enter' && addGrant()}
/>
)}
<select class="prop-select" style="flex: 0 0 80px;" value={newPerm} onChange={e => setNewPerm((e.target as HTMLSelectElement).value as any)}>
<option value="read">read</option>
<option value="write">write</option>
</select>
<button class="panel-btn" onClick={addGrant} disabled={!newName.trim()}>Add</button>
</div>
{error && <p class="wizard-error" style="margin-top: 0.75rem;">{error}</p>}
</div>
)}
</div>
<div class="wizard-footer">
<button class="toolbar-btn" onClick={onClose}>Cancel</button>
<button class="toolbar-btn toolbar-btn-primary" onClick={save} disabled={saving || loading}>
{saving ? 'Saving…' : 'Save'}
</button>
</div>
</div>
</div>
);
}