Changeset 0.37.6 (#218)

Co-authored-by: gobha <jasafpro@gmail.com>
Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
2026-03-21 16:21:59 +00:00
committed by xcaliber
parent 74f3cb84e9
commit 5f1c733002
30 changed files with 2959 additions and 131 deletions

View File

@@ -0,0 +1,254 @@
/**
* Admin > People > Groups
*/
const { html } = window;
const { useState, useEffect, useCallback } = hooks;
export default function GroupsSection() {
const [groups, setGroups] = useState([]);
const [loading, setLoading] = useState(true);
const [detail, setDetail] = useState(null);
const [members, setMembers] = useState([]);
const [allPerms, setAllPerms] = useState([]);
const [allUsers, setAllUsers] = useState([]);
const [allModels, setAllModels] = useState([]);
const [groupData, setGroupData] = useState({});
const [showCreate, setShowCreate] = useState(false);
const [showAddMember, setShowAddMember] = useState(false);
const loadGroups = useCallback(async () => {
try {
const data = await sw.api.admin.groups.list();
setGroups(Array.isArray(data) ? data : data.data || []);
} catch (e) { sw.toast(e.message, 'error'); }
finally { setLoading(false); }
}, []);
useEffect(() => { loadGroups(); }, []);
async function openDetail(group) {
setDetail(group);
setGroupData({
permissions: group.permissions || [],
token_budget_daily: group.token_budget_daily ?? '',
token_budget_monthly: group.token_budget_monthly ?? '',
allowed_models: group.allowed_models || [],
});
try {
const [m, p, u, models] = await Promise.all([
sw.api.admin.groups.members(group.id),
sw.api.admin.permissions.list(),
sw.api.admin.users.list(),
sw.api.admin.models.list().catch(() => []),
]);
setMembers(Array.isArray(m) ? m : m.data || []);
setAllPerms(p.permissions || []);
setAllUsers(Array.isArray(u) ? u : u.users || []);
setAllModels(Array.isArray(models) ? models : models.models || models.data || []);
} catch (e) { sw.toast(e.message, 'error'); }
}
async function createGroup(e) {
e.preventDefault();
const form = e.target;
try {
await sw.api.admin.groups.create({
name: form.name.value.trim(),
description: form.desc.value.trim(),
scope: form.scope.value,
});
sw.toast('Group created', 'success');
setShowCreate(false);
loadGroups();
} catch (e) { sw.toast(e.message, 'error'); }
}
async function savePermsAndBudgets() {
try {
await sw.api.admin.groups.update(detail.id, {
permissions: groupData.permissions,
token_budget_daily: groupData.token_budget_daily || null,
token_budget_monthly: groupData.token_budget_monthly || null,
allowed_models: groupData.allowed_models.length ? groupData.allowed_models : null,
});
sw.toast('Saved', 'success');
} catch (e) { sw.toast(e.message, 'error'); }
}
function togglePerm(perm) {
setGroupData(prev => {
const perms = prev.permissions.includes(perm)
? prev.permissions.filter(p => p !== perm)
: [...prev.permissions, perm];
return { ...prev, permissions: perms };
});
}
function toggleModel(modelId) {
setGroupData(prev => {
const list = prev.allowed_models.includes(modelId)
? prev.allowed_models.filter(m => m !== modelId)
: [...prev.allowed_models, modelId];
return { ...prev, allowed_models: list };
});
}
async function addMember(e) {
e.preventDefault();
const uid = e.target.userId.value;
if (!uid) return;
try {
await sw.api.admin.groups.addMember(detail.id, uid);
sw.toast('Member added', 'success');
setShowAddMember(false);
const m = await sw.api.admin.groups.members(detail.id);
setMembers(Array.isArray(m) ? m : m.data || []);
} catch (e) { sw.toast(e.message, 'error'); }
}
async function removeMember(uid) {
try {
await sw.api.admin.groups.removeMember(detail.id, uid);
sw.toast('Member removed', 'success');
setMembers(prev => prev.filter(m => (m.user_id || m.id) !== uid));
} catch (e) { sw.toast(e.message, 'error'); }
}
async function deleteGroup() {
const ok = await sw.confirm('Delete this group?', true);
if (!ok) return;
try {
await sw.api.admin.groups.del(detail.id);
sw.toast('Group deleted', 'success');
setDetail(null);
loadGroups();
} catch (e) { sw.toast(e.message, 'error'); }
}
if (loading) return html`<div class="settings-placeholder">Loading\u2026</div>`;
if (detail) return html`
<div>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
<button class="btn-small" onClick=${() => setDetail(null)}>\u2190 Back</button>
<h4 style="margin:0;">${detail.name}</h4>
<span class="text-muted" style="font-size:12px;">${detail.scope}</span>
<div style="flex:1"></div>
<button class="btn-small btn-danger" onClick=${deleteGroup}>Delete</button>
</div>
<div class="settings-section" style="margin-bottom:16px;">
<h5 style="margin:0 0 8px;">Permissions</h5>
<div class="admin-permissions-grid">
${allPerms.map(p => html`
<label class="toggle-label" key=${p}>
<input type="checkbox" checked=${groupData.permissions.includes(p)}
onChange=${() => togglePerm(p)} />
<span class="toggle-track"></span>
<span>${p}</span>
</label>
`)}
</div>
</div>
<div class="settings-section" style="margin-bottom:16px;">
<h5 style="margin:0 0 8px;">Token Budgets</h5>
<div class="form-row" style="gap:12px;">
<div class="form-group" style="flex:1;"><label>Daily limit</label>
<input type="number" placeholder="Unlimited" min="0" value=${groupData.token_budget_daily}
onInput=${e => setGroupData(p => ({ ...p, token_budget_daily: e.target.value ? Number(e.target.value) : '' }))} />
</div>
<div class="form-group" style="flex:1;"><label>Monthly limit</label>
<input type="number" placeholder="Unlimited" min="0" value=${groupData.token_budget_monthly}
onInput=${e => setGroupData(p => ({ ...p, token_budget_monthly: e.target.value ? Number(e.target.value) : '' }))} />
</div>
</div>
</div>
<div class="settings-section" style="margin-bottom:16px;">
<h5 style="margin:0 0 8px;">Allowed Models</h5>
<p class="text-muted" style="font-size:12px;margin:0 0 8px;">Leave empty for unrestricted.</p>
<div class="admin-models-grid">
${allModels.map(m => html`
<label class="toggle-label" key=${m.id || m.model_id}>
<input type="checkbox" checked=${groupData.allowed_models.includes(m.model_id || m.id)}
onChange=${() => toggleModel(m.model_id || m.id)} />
<span class="toggle-track"></span>
<span>${m.display_name || m.model_id || m.id}</span>
</label>
`)}
</div>
</div>
<div style="margin-bottom:16px;">
<button class="btn-small btn-primary" onClick=${savePermsAndBudgets}>Save Permissions & Budgets</button>
</div>
<hr style="border:none;border-top:1px solid var(--border);margin:16px 0;" />
<h5 style="margin:0 0 8px;">Members</h5>
<div class="admin-list">
${members.map(m => html`
<div class="admin-surface-row" key=${m.id || m.user_id}>
<span style="flex:1;">${m.username || m.user_id}</span>
<button class="btn-small btn-danger" onClick=${() => removeMember(m.user_id || m.id)}>Remove</button>
</div>
`)}
</div>
${showAddMember
? html`
<form class="admin-inline-form" style="margin-top:8px;" onSubmit=${addMember}>
<div class="form-row">
<div class="form-group"><label>User</label>
<select name="userId">
<option value="">Select\u2026</option>
${allUsers.map(u => html`<option key=${u.id} value=${u.id}>${u.username}</option>`)}
</select>
</div>
</div>
<div class="form-row" style="margin-top:8px;gap:8px;">
<button type="submit" class="btn-small btn-primary">Add</button>
<button type="button" class="btn-small" onClick=${() => setShowAddMember(false)}>Cancel</button>
</div>
</form>
`
: html`<button class="btn-small" style="margin-top:8px;" onClick=${() => setShowAddMember(true)}>+ Add Member</button>`
}
</div>
`;
return html`
<div>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
<div style="flex:1"></div>
<button class="btn-md btn-primary" onClick=${() => setShowCreate(true)}>+ Add</button>
</div>
${showCreate && html`
<form class="admin-inline-form" style="margin-bottom:16px;" onSubmit=${createGroup}>
<div class="form-row">
<div class="form-group"><label>Name</label><input name="name" placeholder="Group name" /></div>
<div class="form-group"><label>Description</label><input name="desc" placeholder="Optional" /></div>
<div class="form-group"><label>Scope</label>
<select name="scope"><option value="global">Global</option><option value="team">Team</option></select>
</div>
</div>
<div class="form-row" style="margin-top:8px;gap:8px;">
<button type="submit" class="btn-small btn-primary">Create</button>
<button type="button" class="btn-small" onClick=${() => setShowCreate(false)}>Cancel</button>
</div>
</form>
`}
<div class="admin-list">
${groups.length === 0 && html`<div class="empty-hint">No groups</div>`}
${groups.map(g => html`
<div class="admin-surface-row" key=${g.id} style="cursor:pointer;" onClick=${() => openDetail(g)}>
<strong style="flex:1;">${g.name}</strong>
<span class="text-muted" style="font-size:12px;">${g.scope}</span>
</div>
`)}
</div>
</div>
`;
}