Major changes: logic add to panel, local variables, panel histor, users management...
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user