Second pass — removes chat-switchboard version numbers from config field docs, section headers, file headers, and test comments. These were pre-fork version tags that don't apply to switchboard-core. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
200 lines
8.2 KiB
JavaScript
200 lines
8.2 KiB
JavaScript
/**
|
|
* AdminSurface — root admin surface
|
|
*
|
|
* Reads globals:
|
|
* __SECTION__ — active section name (string)
|
|
* __BASE__ — base path
|
|
* __CONFIG_SECTIONS__
|
|
*
|
|
* Layout: topbar (back + category tabs) + body (sidebar nav + content area).
|
|
* All 24+ sections are native Preact components loaded lazily.
|
|
*/
|
|
const { html } = window;
|
|
const { useState, useEffect, useCallback, useMemo } = hooks;
|
|
const { render } = preact;
|
|
|
|
import { ToastContainer } from '../../primitives/toast.js';
|
|
import { DialogStack } from '../../shell/dialog-stack.js';
|
|
import { UserMenu } from '../../shell/user-menu.js';
|
|
|
|
// ── Category → Section mapping ──────────────
|
|
const ADMIN_SECTIONS = {
|
|
people: ['users', 'teams', 'groups'],
|
|
workflows: ['workflows'],
|
|
system: ['settings', 'storage', 'packages', 'connections', 'broadcast', 'backup'],
|
|
monitoring: ['audit'],
|
|
};
|
|
|
|
const ADMIN_LABELS = {
|
|
users: 'Users', teams: 'Teams', groups: 'Groups',
|
|
workflows: 'Workflows',
|
|
settings: 'Settings', storage: 'Storage', packages: 'Packages',
|
|
connections: 'Connections', broadcast: 'Broadcast',
|
|
backup: 'Backup',
|
|
audit: 'Audit',
|
|
};
|
|
|
|
// ── Extension config sections ──────
|
|
// Packages declare config_section in their manifest targeting "admin".
|
|
// We merge them into the appropriate category and section module map.
|
|
const _configSections = window.__CONFIG_SECTIONS__ || [];
|
|
const _base = window.__BASE__ || '';
|
|
for (const cs of _configSections) {
|
|
const pkgId = cs.package_id;
|
|
const cat = cs.category || 'system';
|
|
// Add to category (create if new — unlikely but safe)
|
|
if (!ADMIN_SECTIONS[cat]) ADMIN_SECTIONS[cat] = [];
|
|
if (!ADMIN_SECTIONS[cat].includes(pkgId)) ADMIN_SECTIONS[cat].push(pkgId);
|
|
ADMIN_LABELS[pkgId] = cs.label;
|
|
}
|
|
|
|
const CATEGORY_META = {
|
|
people: { label: 'People', first: 'users', icon: 'M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2|C9 7 4' },
|
|
workflows: { label: 'Workflows', first: 'workflows', icon: 'P16 3 21 3 21 8|L4 20 21 3|P21 16 21 21 16 21|L15 15 21 21|L4 4 9 9' },
|
|
system: { label: 'System', first: 'settings', icon: 'C12 12 3|M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09' },
|
|
monitoring: { label: 'Monitoring', first: 'audit', icon: 'L18 20 18 10|L12 20 12 4|L6 20 6 14' },
|
|
};
|
|
|
|
// ── Lazy section imports ────────────────────
|
|
// Version query busts browser module cache on upgrades.
|
|
const _v = window.__VERSION__ ? `?v=${window.__VERSION__}` : '';
|
|
const sectionModules = {
|
|
users: () => import(`./users.js${_v}`),
|
|
teams: () => import(`./teams.js${_v}`),
|
|
groups: () => import(`./groups.js${_v}`),
|
|
workflows: () => import(`./workflows.js${_v}`),
|
|
settings: () => import(`./settings.js${_v}`),
|
|
storage: () => import(`./storage.js${_v}`),
|
|
packages: () => import(`./packages.js${_v}`),
|
|
connections: () => import(`./connections.js${_v}`),
|
|
broadcast: () => import(`./broadcast.js${_v}`),
|
|
backup: () => import(`./backup.js${_v}`),
|
|
audit: () => import(`./audit.js${_v}`),
|
|
};
|
|
|
|
for (const cs of _configSections) {
|
|
const pkgId = cs.package_id;
|
|
const component = cs.component || 'js/config.js';
|
|
sectionModules[pkgId] = () => import(`${_base}/surfaces/${pkgId}/${component}`);
|
|
}
|
|
|
|
// ── Derive category from section ────────────
|
|
function catForSection(section) {
|
|
for (const [cat, sections] of Object.entries(ADMIN_SECTIONS)) {
|
|
if (sections.includes(section)) return cat;
|
|
}
|
|
return 'people';
|
|
}
|
|
|
|
// ── Simple SVG icon renderer ────────────────
|
|
function CatIcon({ paths }) {
|
|
// paths is a compact format: pipe-separated draw commands
|
|
// C = circle, L = line, P = polyline, R = rect, G = polygon, M = path
|
|
const els = paths.split('|').map((p, i) => {
|
|
const t = p[0], d = p.slice(1).trim();
|
|
if (t === 'C') { const [cx, cy, r] = d.split(' '); return html`<circle cx=${cx} cy=${cy} r=${r} key=${i} />`; }
|
|
if (t === 'L') { const [x1, y1, x2, y2] = d.split(' '); return html`<line x1=${x1} y1=${y1} x2=${x2} y2=${y2} key=${i} />`; }
|
|
if (t === 'P') { return html`<polyline points=${d} key=${i} />`; }
|
|
if (t === 'R') { const [x, y, w, h, rx] = d.split(' '); return html`<rect x=${x} y=${y} width=${w} height=${h} rx=${rx} key=${i} />`; }
|
|
if (t === 'G') { return html`<polygon points=${d} key=${i} />`; }
|
|
if (t === 'M') { return html`<path d=${d} key=${i} />`; }
|
|
return html`<path d=${p} key=${i} />`;
|
|
});
|
|
return html`<svg width="13" height="13" viewBox="0 0 24 24" fill="none"
|
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${els}</svg>`;
|
|
}
|
|
|
|
function AdminSurface() {
|
|
const BASE = window.__BASE__ || '';
|
|
const section = window.__SECTION__ || 'users';
|
|
const activeCat = catForSection(section);
|
|
|
|
const [SectionComponent, setSectionComponent] = useState(null);
|
|
|
|
// Load section component
|
|
useEffect(() => {
|
|
setSectionComponent(null);
|
|
const loader = sectionModules[section];
|
|
if (loader) {
|
|
loader().then(mod => {
|
|
const Comp = mod.default || Object.values(mod)[0];
|
|
setSectionComponent(() => Comp);
|
|
}).catch(e => console.error('[admin] Failed to load section:', section, e));
|
|
}
|
|
}, [section]);
|
|
|
|
|
|
const navClick = useCallback((e) => {
|
|
e.preventDefault();
|
|
const href = e.currentTarget.getAttribute('href');
|
|
if (href) location.replace(href);
|
|
}, []);
|
|
|
|
const title = ADMIN_LABELS[section] || 'Admin';
|
|
const sidebarSections = ADMIN_SECTIONS[activeCat] || [];
|
|
|
|
return html`
|
|
<div class="surface-admin">
|
|
${/* Top Bar */``}
|
|
<div class="admin-topbar">
|
|
<span class="admin-topbar-title">
|
|
<img src="${BASE}/favicon.svg" alt="" width="18" height="18" style="vertical-align:-3px;" />
|
|
Administration
|
|
</span>
|
|
|
|
<div class="admin-category-tabs">
|
|
${Object.entries(CATEGORY_META).map(([cat, meta]) => html`
|
|
<a key=${cat} href="${BASE}/admin/${meta.first}"
|
|
class="admin-cat-btn ${activeCat === cat ? 'active' : ''}"
|
|
onClick=${navClick}>
|
|
<${CatIcon} paths=${meta.icon} />
|
|
${meta.label}
|
|
</a>
|
|
`)}
|
|
</div>
|
|
|
|
<div style="margin-left:auto;">
|
|
<${UserMenu} placement="down-right" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="admin-body">
|
|
${/* Left Nav */``}
|
|
<div class="admin-nav">
|
|
${sidebarSections.map(key => html`
|
|
<a key=${key} href="${BASE}/admin/${key}"
|
|
class="admin-nav-link ${section === key ? 'active' : ''}"
|
|
onClick=${navClick}>
|
|
${ADMIN_LABELS[key] || key}
|
|
</a>
|
|
`)}
|
|
</div>
|
|
|
|
${/* Content */``}
|
|
<div class="admin-content">
|
|
<div class="admin-content-header">
|
|
<h2>${title}</h2>
|
|
</div>
|
|
<div class="admin-content-body" id="adminMain">
|
|
${SectionComponent
|
|
? html`<${SectionComponent} />`
|
|
: html`<div class="settings-placeholder">Loading\u2026</div>`
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<${ToastContainer} />
|
|
<${DialogStack} />
|
|
`;
|
|
}
|
|
|
|
// ── Mount ────────────────────────────────────
|
|
const mount = document.getElementById('admin-mount');
|
|
if (mount) {
|
|
render(html`<${AdminSurface} />`, mount);
|
|
console.log('[admin] Surface mounted');
|
|
}
|
|
|
|
export { AdminSurface };
|