Changeset 0.16.0 (#74)
This commit is contained in:
@@ -516,6 +516,7 @@
|
||||
<button class="admin-tab" data-ttab="usage" onclick="UI.switchTeamTab('usage')">Usage</button>
|
||||
<button class="admin-tab" data-ttab="activity" onclick="UI.switchTeamTab('activity')">Activity</button>
|
||||
<button class="admin-tab" data-ttab="knowledge" onclick="UI.switchTeamTab('knowledge')">Knowledge</button>
|
||||
<button class="admin-tab" data-ttab="groups" onclick="UI.switchTeamTab('groups')">Groups</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Members tab -->
|
||||
@@ -578,6 +579,11 @@
|
||||
<div class="team-tab-content" id="teamTabKnowledge" style="display:none">
|
||||
<div id="teamKBContent"></div>
|
||||
</div>
|
||||
<!-- Groups tab -->
|
||||
<div class="team-tab-content" id="teamTabGroups" style="display:none">
|
||||
<p class="text-muted" style="font-size:12px;margin:0 0 8px">Groups assigned to this team. Contact a system admin to create or modify groups.</p>
|
||||
<div id="settingsTeamGroups"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -651,6 +657,43 @@
|
||||
<div id="adminMemberList"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-section-content" id="adminGroupsTab" style="display:none">
|
||||
<div class="admin-toolbar">
|
||||
<button class="btn-small btn-primary" id="adminAddGroupBtn">+ New Group</button>
|
||||
</div>
|
||||
<div id="adminAddGroupForm" style="display:none" class="admin-inline-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Group Name</label><input type="text" id="adminGroupName" placeholder="Engineering"></div>
|
||||
<div class="form-group"><label>Scope</label>
|
||||
<select id="adminGroupScope">
|
||||
<option value="global">Global</option>
|
||||
<option value="team">Team</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" id="adminGroupTeamRow" style="display:none">
|
||||
<label>Team</label><select id="adminGroupTeam"><option value="">Select team...</option></select>
|
||||
</div>
|
||||
<div class="form-group"><label>Description</label><input type="text" id="adminGroupDesc" placeholder="Access group for engineering resources"></div>
|
||||
<div class="form-row"><button class="btn-small btn-primary" id="adminCreateGroupBtn">Create</button><button class="btn-small" id="adminCancelGroupBtn">Cancel</button></div>
|
||||
</div>
|
||||
<div id="adminGroupList"></div>
|
||||
<div id="adminGroupDetail" style="display:none">
|
||||
<button class="notes-back-btn" id="adminGroupBackBtn">← Back to groups</button>
|
||||
<h3 id="adminGroupDetailName" style="margin:8px 0 4px;font-size:15px"></h3>
|
||||
<p id="adminGroupDetailMeta" class="text-muted" style="font-size:12px;margin:0 0 12px"></p>
|
||||
<div class="admin-toolbar">
|
||||
<button class="btn-small btn-primary" id="adminAddGroupMemberBtn">+ Add Member</button>
|
||||
</div>
|
||||
<div id="adminAddGroupMemberForm" style="display:none" class="admin-inline-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>User</label><select id="adminGroupMemberUser"></select></div>
|
||||
</div>
|
||||
<div class="form-row"><button class="btn-small btn-primary" id="adminAddGroupMemberSubmit">Add</button><button class="btn-small" id="adminCancelGroupMemberBtn">Cancel</button></div>
|
||||
</div>
|
||||
<div id="adminGroupMemberList"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-section-content" id="adminRolesTab" style="display:none">
|
||||
<p style="color:var(--text-muted);margin:0 0 12px">Configure system model roles. Each role has a primary and optional fallback model.</p>
|
||||
<div id="adminRolesContent">
|
||||
|
||||
@@ -400,6 +400,42 @@ async function settingsDeleteTeamPreset(teamId, presetId, name) {
|
||||
|
||||
// ── Team Provider handlers — now managed by UI primitives in ui-settings.js ──
|
||||
|
||||
// ── Group Actions ───────────────────────────
|
||||
|
||||
async function deleteGroup(id, name) {
|
||||
if (!await showConfirm(`Delete group "${name}"? Members will lose access to any resources granted via this group.`)) return;
|
||||
try { await API.adminDeleteGroup(id); UI.toast('Group deleted'); await UI.loadAdminGroups(); }
|
||||
catch (e) { UI.toast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
async function removeGroupMember(groupId, userId, label) {
|
||||
if (!await showConfirm(`Remove ${label} from group?`)) return;
|
||||
try {
|
||||
await API.adminRemoveGroupMember(groupId, userId);
|
||||
UI.toast('Member removed');
|
||||
await UI.loadGroupMembers(groupId);
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
async function saveGrant(resourceType, resourceId) {
|
||||
const scopeSel = document.getElementById(`grantScope_${resourceId}`);
|
||||
if (!scopeSel) return;
|
||||
const scope = scopeSel.value;
|
||||
const groupsDiv = document.getElementById(`grantGroups_${resourceId}`);
|
||||
const selectedGroups = [...(groupsDiv?.querySelectorAll('.grant-group-cb:checked') || [])].map(cb => cb.value);
|
||||
|
||||
if (scope === 'groups' && selectedGroups.length === 0) {
|
||||
return UI.toast('Select at least one group', 'error');
|
||||
}
|
||||
|
||||
try {
|
||||
await API.adminSetGrant(resourceType, resourceId, scope, selectedGroups);
|
||||
// Remove the picker
|
||||
document.querySelectorAll('.grant-picker-active').forEach(el => el.remove());
|
||||
UI.toast(scope === 'team_only' ? 'Access reverted to team only' : `Access set to ${scope}`);
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
// ── Admin Listeners (extracted from initListeners) ──
|
||||
|
||||
function _initAdminListeners() {
|
||||
@@ -509,6 +545,66 @@ function _initAdminListeners() {
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
});
|
||||
|
||||
// ── Group management ────────────────────────
|
||||
document.getElementById('adminAddGroupBtn')?.addEventListener('click', async () => {
|
||||
document.getElementById('adminAddGroupForm').style.display = '';
|
||||
document.getElementById('adminGroupName').focus();
|
||||
// Populate team dropdown
|
||||
try {
|
||||
const resp = await API.adminListTeams();
|
||||
const teams = (resp.data || []).filter(t => t.is_active);
|
||||
const sel = document.getElementById('adminGroupTeam');
|
||||
sel.innerHTML = '<option value="">Select team...</option>' +
|
||||
teams.map(t => `<option value="${t.id}">${esc(t.name)}</option>`).join('');
|
||||
} catch (e) { /* proceed without teams */ }
|
||||
});
|
||||
document.getElementById('adminGroupScope')?.addEventListener('change', function() {
|
||||
document.getElementById('adminGroupTeamRow').style.display = this.value === 'team' ? '' : 'none';
|
||||
});
|
||||
document.getElementById('adminCancelGroupBtn')?.addEventListener('click', () => {
|
||||
document.getElementById('adminAddGroupForm').style.display = 'none';
|
||||
document.getElementById('adminGroupName').value = '';
|
||||
document.getElementById('adminGroupDesc').value = '';
|
||||
});
|
||||
document.getElementById('adminCreateGroupBtn')?.addEventListener('click', async () => {
|
||||
const name = document.getElementById('adminGroupName').value.trim();
|
||||
const desc = document.getElementById('adminGroupDesc').value.trim();
|
||||
const scope = document.getElementById('adminGroupScope').value;
|
||||
const teamId = scope === 'team' ? document.getElementById('adminGroupTeam').value : null;
|
||||
if (!name) return UI.toast('Group name required', 'error');
|
||||
if (scope === 'team' && !teamId) return UI.toast('Select a team for team-scoped groups', 'error');
|
||||
try {
|
||||
await API.adminCreateGroup(name, desc, scope, teamId);
|
||||
document.getElementById('adminAddGroupForm').style.display = 'none';
|
||||
document.getElementById('adminGroupName').value = '';
|
||||
document.getElementById('adminGroupDesc').value = '';
|
||||
UI.toast('Group created');
|
||||
await UI.loadAdminGroups(true);
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
});
|
||||
document.getElementById('adminGroupBackBtn')?.addEventListener('click', () => {
|
||||
document.getElementById('adminGroupDetail').style.display = 'none';
|
||||
document.getElementById('adminGroupList').style.display = '';
|
||||
UI.loadAdminGroups(true);
|
||||
});
|
||||
document.getElementById('adminAddGroupMemberBtn')?.addEventListener('click', async () => {
|
||||
document.getElementById('adminAddGroupMemberForm').style.display = '';
|
||||
await UI.loadGroupMemberDropdown(UI._groupEditId);
|
||||
});
|
||||
document.getElementById('adminCancelGroupMemberBtn')?.addEventListener('click', () => {
|
||||
document.getElementById('adminAddGroupMemberForm').style.display = 'none';
|
||||
});
|
||||
document.getElementById('adminAddGroupMemberSubmit')?.addEventListener('click', async () => {
|
||||
const userId = document.getElementById('adminGroupMemberUser').value;
|
||||
if (!userId) return UI.toast('Select a user', 'error');
|
||||
try {
|
||||
await API.adminAddGroupMember(UI._groupEditId, userId);
|
||||
document.getElementById('adminAddGroupMemberForm').style.display = 'none';
|
||||
UI.toast('Member added');
|
||||
await UI.loadGroupMembers(UI._groupEditId);
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
});
|
||||
|
||||
// ── Extension management ────────────────────
|
||||
document.getElementById('adminInstallExtBtn')?.addEventListener('click', () => {
|
||||
document.getElementById('adminInstallExtForm').style.display = '';
|
||||
|
||||
@@ -413,6 +413,30 @@ const API = {
|
||||
adminUpdateMember(teamId, memberId, role) { return this._put(`/api/v1/admin/teams/${teamId}/members/${memberId}`, { role }); },
|
||||
adminRemoveMember(teamId, memberId) { return this._del(`/api/v1/admin/teams/${teamId}/members/${memberId}`); },
|
||||
|
||||
// ── Admin Groups ────────────────────────
|
||||
adminListGroups() { return this._get('/api/v1/admin/groups'); },
|
||||
adminCreateGroup(name, description, scope, teamId) {
|
||||
const body = { name, description, scope };
|
||||
if (teamId) body.team_id = teamId;
|
||||
return this._post('/api/v1/admin/groups', body);
|
||||
},
|
||||
adminGetGroup(id) { return this._get(`/api/v1/admin/groups/${id}`); },
|
||||
adminUpdateGroup(id, updates) { return this._put(`/api/v1/admin/groups/${id}`, updates); },
|
||||
adminDeleteGroup(id) { return this._del(`/api/v1/admin/groups/${id}`); },
|
||||
adminListGroupMembers(groupId) { return this._get(`/api/v1/admin/groups/${groupId}/members`); },
|
||||
adminAddGroupMember(groupId, userId) { return this._post(`/api/v1/admin/groups/${groupId}/members`, { user_id: userId }); },
|
||||
adminRemoveGroupMember(groupId, userId) { return this._del(`/api/v1/admin/groups/${groupId}/members/${userId}`); },
|
||||
|
||||
// ── Admin Resource Grants ───────────────
|
||||
adminGetGrant(type, id) { return this._get(`/api/v1/admin/grants/${type}/${id}`); },
|
||||
adminSetGrant(type, id, grantScope, grantedGroups) {
|
||||
return this._put(`/api/v1/admin/grants/${type}/${id}`, { grant_scope: grantScope, granted_groups: grantedGroups || [] });
|
||||
},
|
||||
adminDeleteGrant(type, id) { return this._del(`/api/v1/admin/grants/${type}/${id}`); },
|
||||
|
||||
// ── User Groups ─────────────────────────
|
||||
listMyGroups() { return this._get('/api/v1/groups/mine'); },
|
||||
|
||||
// ── User Teams ──────────────────────────
|
||||
listMyTeams() { return this._get('/api/v1/teams/mine'); },
|
||||
|
||||
@@ -425,6 +449,7 @@ const API = {
|
||||
teamCreatePreset(teamId, preset) { return this._post(`/api/v1/teams/${teamId}/presets`, preset); },
|
||||
teamDeletePreset(teamId, presetId) { return this._del(`/api/v1/teams/${teamId}/presets/${presetId}`); },
|
||||
teamListAvailableModels(teamId) { return this._get(`/api/v1/teams/${teamId}/models`); },
|
||||
teamListGroups(teamId) { return this._get(`/api/v1/teams/${teamId}/groups`); },
|
||||
|
||||
// ── Team Providers ──────────────────────
|
||||
teamListProviders(teamId) { return this._get(`/api/v1/teams/${teamId}/providers`); },
|
||||
|
||||
@@ -182,13 +182,14 @@ const KnowledgeUI = (() => {
|
||||
const chunks = kb.chunk_count === 1 ? '1 chunk' : `${kb.chunk_count} chunks`;
|
||||
const size = kb.total_bytes > 0 ? ` · ${_formatSize(kb.total_bytes)}` : '';
|
||||
html += `
|
||||
<div class="kb-manage-item" data-kb-id="${kb.id}">
|
||||
<div class="kb-manage-item" data-kb-id="${kb.id}" data-grant-kb="${kb.id}">
|
||||
<div class="kb-manage-info">
|
||||
<span class="kb-manage-name">${scope} ${_esc(kb.name)}</span>
|
||||
<span class="kb-manage-desc">${_esc(kb.description || '')}</span>
|
||||
<span class="kb-manage-stats">${docs} · ${chunks}${size}${status}</span>
|
||||
</div>
|
||||
<div class="kb-manage-actions">
|
||||
${kb.scope !== 'personal' ? `<button class="btn-small" data-kb-grant="${kb.id}" data-kb-name="${_esc(kb.name)}" title="Access">🔒</button>` : ''}
|
||||
<button class="btn-small" data-kb-docs="${kb.id}" title="Documents">📄</button>
|
||||
<button class="btn-small btn-danger" data-kb-del="${kb.id}" title="Delete">✕</button>
|
||||
</div>
|
||||
@@ -211,6 +212,14 @@ const KnowledgeUI = (() => {
|
||||
panel.querySelectorAll('[data-kb-del]').forEach(btn => {
|
||||
btn.addEventListener('click', () => _deleteKB(btn.dataset.kbDel));
|
||||
});
|
||||
// Wire grant access buttons (admin only, non-personal KBs)
|
||||
panel.querySelectorAll('[data-kb-grant]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
if (typeof UI !== 'undefined' && UI.openGrantPicker) {
|
||||
UI.openGrantPicker('knowledge_base', btn.dataset.kbGrant, btn.dataset.kbName, `[data-grant-kb="${btn.dataset.kbGrant}"]`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Create KB Dialog ─────────────────────
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -301,6 +301,24 @@ Object.assign(UI, {
|
||||
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
|
||||
},
|
||||
|
||||
async loadTeamManageGroups(teamId) {
|
||||
const el = document.getElementById('settingsTeamGroups');
|
||||
if (!el) return;
|
||||
el.innerHTML = '<div class="loading">Loading...</div>';
|
||||
try {
|
||||
const resp = await API.teamListGroups(teamId);
|
||||
const groups = resp.data || [];
|
||||
el.innerHTML = groups.map(g => `
|
||||
<div class="admin-user-row" style="padding:6px 0">
|
||||
<div class="admin-user-info">
|
||||
<div><strong>${esc(g.name)}</strong></div>
|
||||
<div class="text-muted" style="font-size:11px">${esc(g.description || 'No description')} · ${g.member_count} member${g.member_count !== 1 ? 's' : ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('') || '<div class="empty-hint">No groups assigned to this team yet</div>';
|
||||
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
|
||||
},
|
||||
|
||||
async loadMyUsage() {
|
||||
const el = document.getElementById('myUsageTotals')?.parentElement;
|
||||
if (!el) return;
|
||||
|
||||
Reference in New Issue
Block a user