Changeset 0.37.6 (#218)
Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
218
src/js/sw/surfaces/admin/index.js
Normal file
218
src/js/sw/surfaces/admin/index.js
Normal file
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* AdminSurface — root admin surface
|
||||
*
|
||||
* Reads globals:
|
||||
* __SECTION__ — active section name (string)
|
||||
* __BASE__ — base path
|
||||
*
|
||||
* 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';
|
||||
|
||||
// ── Category → Section mapping ──────────────
|
||||
const ADMIN_SECTIONS = {
|
||||
people: ['users', 'teams', 'groups'],
|
||||
ai: ['providers', 'models', 'personas', 'roles', 'knowledgeBases', 'memory'],
|
||||
workflows: ['workflows', 'tasks'],
|
||||
routing: ['health', 'routing', 'capabilities'],
|
||||
system: ['settings', 'storage', 'packages', 'channels', 'broadcast'],
|
||||
monitoring: ['dashboard', 'usage', 'audit', 'stats'],
|
||||
};
|
||||
|
||||
const ADMIN_LABELS = {
|
||||
users: 'Users', teams: 'Teams', groups: 'Groups',
|
||||
providers: 'Providers', models: 'Models', personas: 'Personas',
|
||||
roles: 'Roles', knowledgeBases: 'Knowledge', memory: 'Memory',
|
||||
health: 'Health', routing: 'Routing', capabilities: 'Capabilities',
|
||||
workflows: 'Workflows', tasks: 'Tasks',
|
||||
settings: 'Settings', storage: 'Storage', packages: 'Packages',
|
||||
channels: 'Channels', broadcast: 'Broadcast',
|
||||
dashboard: 'Dashboard', usage: 'Usage', audit: 'Audit', stats: 'Stats',
|
||||
};
|
||||
|
||||
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' },
|
||||
ai: { label: 'AI', first: 'providers', icon: 'R4 4 16 16 2|L9 9 9.01 9|L15 9 15.01 9|M8 14s1.5 2 4 2 4-2 4-2' },
|
||||
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' },
|
||||
routing: { label: 'Routing', first: 'health', icon: 'G13 2 3 14 12 14 11 22 21 10 12 10 13 2' },
|
||||
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: 'dashboard', icon: 'L18 20 18 10|L12 20 12 4|L6 20 6 14' },
|
||||
};
|
||||
|
||||
// ── Lazy section imports ────────────────────
|
||||
const sectionModules = {
|
||||
users: () => import('./users.js'),
|
||||
teams: () => import('./teams.js'),
|
||||
groups: () => import('./groups.js'),
|
||||
providers: () => import('./providers.js'),
|
||||
models: () => import('./models.js'),
|
||||
personas: () => import('./personas.js'),
|
||||
roles: () => import('./roles.js'),
|
||||
knowledgeBases: () => import('./knowledge.js'),
|
||||
memory: () => import('./memory.js'),
|
||||
workflows: () => import('./workflows.js'),
|
||||
tasks: () => import('./tasks.js'),
|
||||
health: () => import('./health.js'),
|
||||
routing: () => import('./routing.js'),
|
||||
capabilities: () => import('./capabilities.js'),
|
||||
settings: () => import('./settings.js'),
|
||||
storage: () => import('./storage.js'),
|
||||
packages: () => import('./packages.js'),
|
||||
channels: () => import('./channels.js'),
|
||||
broadcast: () => import('./broadcast.js'),
|
||||
dashboard: () => import('./dashboard.js'),
|
||||
usage: () => import('./usage.js'),
|
||||
audit: () => import('./audit.js'),
|
||||
stats: () => import('./stats.js'),
|
||||
};
|
||||
|
||||
// ── 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]);
|
||||
|
||||
// Back button with return URL stash
|
||||
useEffect(() => {
|
||||
const RETURN_KEY = 'sb_admin_return';
|
||||
if (!sessionStorage.getItem(RETURN_KEY)) {
|
||||
const ref = document.referrer;
|
||||
if (ref && ref.startsWith(location.origin) && !ref.includes('/admin')) {
|
||||
sessionStorage.setItem(RETURN_KEY, ref);
|
||||
} else {
|
||||
sessionStorage.setItem(RETURN_KEY, location.origin + BASE + '/');
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const backClick = useCallback((e) => {
|
||||
e.preventDefault();
|
||||
const RETURN_KEY = 'sb_admin_return';
|
||||
const returnURL = sessionStorage.getItem(RETURN_KEY);
|
||||
sessionStorage.removeItem(RETURN_KEY);
|
||||
location.href = returnURL || BASE + '/';
|
||||
}, []);
|
||||
|
||||
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">
|
||||
<a href="${BASE}/" class="admin-topbar-back" onClick=${backClick}>
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="19" y1="12" x2="5" y2="12"/>
|
||||
<polyline points="12 19 5 12 12 5"/>
|
||||
</svg>
|
||||
Back
|
||||
</a>
|
||||
<div class="admin-topbar-sep"></div>
|
||||
<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>
|
||||
|
||||
<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 };
|
||||
Reference in New Issue
Block a user