Changeset 0.16.0 (#74)
This commit is contained in:
@@ -6,14 +6,14 @@
|
||||
|
||||
// ── Category → Section mapping ────────────
|
||||
const ADMIN_SECTIONS = {
|
||||
people: ['users', 'teams'],
|
||||
people: ['users', 'teams', 'groups'],
|
||||
ai: ['providers', 'models', 'presets', 'roles', 'knowledgeBases'],
|
||||
system: ['settings', 'storage', 'extensions'],
|
||||
monitoring: ['usage', 'audit', 'stats'],
|
||||
};
|
||||
|
||||
const ADMIN_LABELS = {
|
||||
users: 'Users', teams: 'Teams',
|
||||
users: 'Users', teams: 'Teams', groups: 'Groups',
|
||||
providers: 'Providers', models: 'Models', presets: 'Personas', roles: 'Roles', knowledgeBases: 'Knowledge',
|
||||
settings: 'Settings', storage: 'Storage', extensions: 'Extensions',
|
||||
usage: 'Usage', audit: 'Audit', stats: 'Stats',
|
||||
@@ -23,6 +23,7 @@ const ADMIN_LABELS = {
|
||||
const ADMIN_LOADERS = {
|
||||
users: () => UI.loadAdminUsers(),
|
||||
teams: () => UI.loadAdminTeams(),
|
||||
groups: () => UI.loadAdminGroups(),
|
||||
roles: () => UI.loadAdminRoles(),
|
||||
providers: () => UI.loadAdminProviders(),
|
||||
models: () => UI.loadAdminModels(),
|
||||
@@ -187,6 +188,7 @@ Object.assign(UI, {
|
||||
KnowledgeUI.openTeamPanel(teamId);
|
||||
}
|
||||
}
|
||||
if (tab === 'groups') UI.loadTeamManageGroups(teamId);
|
||||
},
|
||||
|
||||
async loadAdminUsers(quiet) {
|
||||
@@ -612,13 +614,14 @@ Object.assign(UI, {
|
||||
}
|
||||
const creator = p.creator_name ? `<span class="text-muted" style="font-size:11px">by ${esc(p.creator_name)}</span>` : '';
|
||||
const status = p.is_active ? '' : '<span class="badge-pending">inactive</span>';
|
||||
return `<div class="admin-preset-row">
|
||||
return `<div class="admin-preset-row" data-grant-persona="${p.id}">
|
||||
<div class="preset-info">
|
||||
<strong>${avatarEl}${esc(p.name)}</strong> ${scopeBadge} ${creator} ${status}
|
||||
<div class="preset-meta">${esc(p.base_model_id)} · ${esc(p.provider_name || 'auto')}</div>
|
||||
${p.description ? `<div class="preset-desc">${esc(p.description)}</div>` : ''}
|
||||
</div>
|
||||
<div class="preset-actions">
|
||||
<button class="btn-small" onclick="UI.openGrantPicker('persona', '${p.id}', '${esc(p.name)}', '[data-grant-persona="${p.id}"]')" title="Manage access">🔒 Access</button>
|
||||
<button class="btn-edit" onclick="editAdminPreset('${p.id}')" title="Edit">✎</button>
|
||||
<button class="admin-model-toggle ${p.is_active ? 'enabled' : ''}" onclick="toggleAdminPreset('${p.id}', ${!p.is_active})">${p.is_active ? '✓ Active' : 'Inactive'}</button>
|
||||
<button class="btn-delete" onclick="deleteAdminPreset('${p.id}', '${esc(p.name)}')" title="Delete">✕</button>
|
||||
@@ -725,6 +728,146 @@ Object.assign(UI, {
|
||||
} catch (e) { sel.innerHTML = `<option value="">Error loading users</option>`; }
|
||||
},
|
||||
|
||||
// ── Groups Admin ────────────────────────
|
||||
_groupEditId: null,
|
||||
|
||||
async loadAdminGroups(quiet) {
|
||||
const el = document.getElementById('adminGroupList');
|
||||
const detail = document.getElementById('adminGroupDetail');
|
||||
if (!quiet) {
|
||||
el.innerHTML = '<div class="loading">Loading...</div>';
|
||||
detail.style.display = 'none';
|
||||
el.style.display = '';
|
||||
document.getElementById('adminAddGroupForm').style.display = 'none';
|
||||
}
|
||||
try {
|
||||
const resp = await API.adminListGroups();
|
||||
const groups = resp.data || [];
|
||||
el.innerHTML = groups.map(g => {
|
||||
const scopeBadge = g.scope === 'global'
|
||||
? '<span class="badge-admin">global</span>'
|
||||
: `<span class="badge-team">team</span>`;
|
||||
return `<div class="admin-user-row">
|
||||
<div class="admin-user-info">
|
||||
<div><strong>${esc(g.name)}</strong> ${scopeBadge}</div>
|
||||
<div class="text-muted">${esc(g.description || 'No description')} · ${g.member_count} member${g.member_count !== 1 ? 's' : ''}</div>
|
||||
</div>
|
||||
<div class="admin-user-actions">
|
||||
<button class="btn-small" onclick="UI.openGroupDetail('${g.id}', '${esc(g.name)}')">Members</button>
|
||||
<button class="btn-delete" onclick="deleteGroup('${g.id}', '${esc(g.name)}')" title="Delete">✕</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('') || '<div class="empty-hint">No groups yet — create one to control access to personas and knowledge bases</div>';
|
||||
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
|
||||
},
|
||||
|
||||
async openGroupDetail(groupId, groupName) {
|
||||
this._groupEditId = groupId;
|
||||
document.getElementById('adminGroupList').style.display = 'none';
|
||||
document.getElementById('adminAddGroupForm').style.display = 'none';
|
||||
const detail = document.getElementById('adminGroupDetail');
|
||||
detail.style.display = '';
|
||||
document.getElementById('adminGroupDetailName').textContent = groupName;
|
||||
document.getElementById('adminAddGroupMemberForm').style.display = 'none';
|
||||
|
||||
// Load group details for meta line
|
||||
try {
|
||||
const g = await API.adminGetGroup(groupId);
|
||||
const scope = g.scope === 'global' ? 'Global group' : 'Team-scoped group';
|
||||
document.getElementById('adminGroupDetailMeta').textContent = `${scope} · ${g.member_count} member${g.member_count !== 1 ? 's' : ''}`;
|
||||
} catch (e) { /* proceed */ }
|
||||
|
||||
await this.loadGroupMembers(groupId);
|
||||
},
|
||||
|
||||
async loadGroupMembers(groupId) {
|
||||
const el = document.getElementById('adminGroupMemberList');
|
||||
el.innerHTML = '<div class="loading">Loading...</div>';
|
||||
try {
|
||||
const resp = await API.adminListGroupMembers(groupId);
|
||||
const members = resp.data || [];
|
||||
el.innerHTML = members.map(m => `
|
||||
<div class="admin-user-row">
|
||||
<div class="admin-user-info">
|
||||
<div><strong>${esc(m.display_name || m.username || m.email)}</strong></div>
|
||||
<div class="text-muted">${esc(m.email)} · added ${new Date(m.added_at).toLocaleDateString()}</div>
|
||||
</div>
|
||||
<div class="admin-user-actions">
|
||||
<button class="btn-delete" onclick="removeGroupMember('${groupId}', '${m.user_id}', '${esc(m.username || m.email)}')" title="Remove">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('') || '<div class="empty-hint">No members — add users to this group</div>';
|
||||
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
|
||||
},
|
||||
|
||||
async loadGroupMemberDropdown(groupId) {
|
||||
const sel = document.getElementById('adminGroupMemberUser');
|
||||
sel.innerHTML = '<option value="">Loading...</option>';
|
||||
try {
|
||||
const resp = await API.adminListUsers(1, 200);
|
||||
const users = resp.users || resp.data || [];
|
||||
// Exclude existing members
|
||||
const memberResp = await API.adminListGroupMembers(groupId);
|
||||
const existingIds = new Set((memberResp.data || []).map(m => m.user_id));
|
||||
const available = users.filter(u => u.is_active && !existingIds.has(u.id));
|
||||
sel.innerHTML = '<option value="">Select user...</option>' +
|
||||
available.map(u => `<option value="${u.id}">${esc(u.username || u.email)}</option>`).join('');
|
||||
} catch (e) { sel.innerHTML = `<option value="">Error loading users</option>`; }
|
||||
},
|
||||
|
||||
// ── Grant Picker (shared for Personas + KBs) ──
|
||||
|
||||
async openGrantPicker(resourceType, resourceId, resourceName, anchorSelector) {
|
||||
// Fetch current grant + all groups for the picker
|
||||
const [grantResp, groupsResp] = await Promise.all([
|
||||
API.adminGetGrant(resourceType, resourceId),
|
||||
API.adminListGroups(),
|
||||
]);
|
||||
const grant = grantResp;
|
||||
const groups = (groupsResp.data || []);
|
||||
const currentScope = grant.grant_scope || 'team_only';
|
||||
const currentGroups = new Set(grant.granted_groups || []);
|
||||
|
||||
const html = `<div class="admin-inline-form" style="margin:8px 0;padding:10px;background:rgba(255,255,255,0.03);border-radius:8px">
|
||||
<div style="font-size:13px;font-weight:600;margin-bottom:8px">Access: ${esc(resourceName)}</div>
|
||||
<div class="form-group" style="margin-bottom:8px">
|
||||
<label style="font-size:12px">Scope</label>
|
||||
<select id="grantScope_${resourceId}" class="inline-select" style="width:auto">
|
||||
<option value="team_only" ${currentScope === 'team_only' ? 'selected' : ''}>Team Only (default)</option>
|
||||
<option value="global" ${currentScope === 'global' ? 'selected' : ''}>Global (all users)</option>
|
||||
<option value="groups" ${currentScope === 'groups' ? 'selected' : ''}>Specific Groups</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="grantGroups_${resourceId}" style="${currentScope === 'groups' ? '' : 'display:none'}">
|
||||
${groups.length ? groups.map(g => `<label class="checkbox-label" style="font-size:12px">
|
||||
<input type="checkbox" class="grant-group-cb" value="${g.id}" ${currentGroups.has(g.id) ? 'checked' : ''}> ${esc(g.name)}
|
||||
<span class="text-muted" style="font-size:11px">${g.scope === 'global' ? '(global)' : '(team)'}</span>
|
||||
</label>`).join('') : '<span class="text-muted" style="font-size:12px">No groups exist yet — create one in People → Groups</span>'}
|
||||
</div>
|
||||
<div class="form-row" style="margin-top:8px">
|
||||
<button class="btn-small btn-primary" onclick="saveGrant('${resourceType}', '${resourceId}')">Save</button>
|
||||
<button class="btn-small" onclick="this.closest('.admin-inline-form').remove()">Cancel</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Remove any existing picker, then insert after the anchor row
|
||||
document.querySelectorAll('.grant-picker-active').forEach(el => el.remove());
|
||||
const row = document.querySelector(anchorSelector);
|
||||
if (row) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'grant-picker-active';
|
||||
wrapper.innerHTML = html;
|
||||
row.after(wrapper);
|
||||
|
||||
// Wire scope toggle
|
||||
const scopeSel = document.getElementById(`grantScope_${resourceId}`);
|
||||
const groupsDiv = document.getElementById(`grantGroups_${resourceId}`);
|
||||
scopeSel?.addEventListener('change', () => {
|
||||
groupsDiv.style.display = scopeSel.value === 'groups' ? '' : 'none';
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async loadAdminSettings() {
|
||||
try {
|
||||
const data = await API.adminGetSettings();
|
||||
|
||||
Reference in New Issue
Block a user