feat(ui): group dashboard navigation into sectioned routes and align view layouts with dcrouter

This commit is contained in:
2026-05-22 13:45:21 +00:00
parent 60fbb4be2b
commit b8dccac68d
13 changed files with 215 additions and 93 deletions
+87 -33
View File
@@ -1,7 +1,8 @@
import { DeesElement, customElement, html, css, cssManager, state, type TemplateResult } from '../plugins.js';
import { deesCatalog } from '../plugins.js';
import type { IView } from '@design.estate/dees-catalog';
import { NotificationManager } from '../state/notification-manager.js';
import { appRouter } from '../router.js';
import { appRouter, type TSubviewSlug, type TViewSlug } from '../router.js';
import { SipproxyViewOverview } from './sipproxy-view-overview.js';
import { SipproxyViewCalls } from './sipproxy-view-calls.js';
import { SipproxyViewPhone } from './sipproxy-view-phone.js';
@@ -12,25 +13,56 @@ import { SipproxyViewRoutes } from './sipproxy-view-routes.js';
import { SipproxyViewVoicemail } from './sipproxy-view-voicemail.js';
import { SipproxyViewIvr } from './sipproxy-view-ivr.js';
const VIEW_TABS = [
{ name: 'Overview', iconName: 'lucide:layoutDashboard', element: SipproxyViewOverview },
{ name: 'Calls', iconName: 'lucide:phone', element: SipproxyViewCalls },
{ name: 'Phone', iconName: 'lucide:headset', element: SipproxyViewPhone },
{ name: 'Routes', iconName: 'lucide:route', element: SipproxyViewRoutes },
{ name: 'Voicemail', iconName: 'lucide:voicemail', element: SipproxyViewVoicemail },
{ name: 'IVR', iconName: 'lucide:ListTree', element: SipproxyViewIvr },
{ name: 'Contacts', iconName: 'lucide:contactRound', element: SipproxyViewContacts },
{ name: 'Providers', iconName: 'lucide:server', element: SipproxyViewProviders },
{ name: 'Log', iconName: 'lucide:scrollText', element: SipproxyViewLog },
];
interface ITabbedView extends IView {
slug?: string;
subViews?: ITabbedView[];
}
// Map slug -> tab for routing.
const SLUG_TO_TAB = new Map(VIEW_TABS.map((t) => [t.name.toLowerCase(), t]));
const VIEW_TABS: ITabbedView[] = [
{
name: 'Overview',
slug: 'overview',
iconName: 'lucide:layoutDashboard',
subViews: [
{ name: 'Stats', slug: 'stats', iconName: 'lucide:activity', element: SipproxyViewOverview },
],
},
{
name: 'Telephony',
slug: 'telephony',
iconName: 'lucide:phoneCall',
subViews: [
{ name: 'Calls', slug: 'calls', iconName: 'lucide:phone', element: SipproxyViewCalls },
{ name: 'Phone', slug: 'phone', iconName: 'lucide:headset', element: SipproxyViewPhone },
{ name: 'Routes', slug: 'routes', iconName: 'lucide:route', element: SipproxyViewRoutes },
{ name: 'Voicemail', slug: 'voicemail', iconName: 'lucide:voicemail', element: SipproxyViewVoicemail },
{ name: 'IVR', slug: 'ivr', iconName: 'lucide:ListTree', element: SipproxyViewIvr },
],
},
{
name: 'Configuration',
slug: 'configuration',
iconName: 'lucide:settings',
subViews: [
{ name: 'Contacts', slug: 'contacts', iconName: 'lucide:contactRound', element: SipproxyViewContacts },
{ name: 'Providers', slug: 'providers', iconName: 'lucide:server', element: SipproxyViewProviders },
],
},
{
name: 'System',
slug: 'system',
iconName: 'lucide:serverCog',
subViews: [
{ name: 'Log', slug: 'log', iconName: 'lucide:scrollText', element: SipproxyViewLog },
],
},
];
@customElement('sipproxy-app')
export class SipproxyApp extends DeesElement {
private notificationManager = new NotificationManager();
private appdash: InstanceType<typeof deesCatalog.DeesSimpleAppDash> | null = null;
private viewTabs = VIEW_TABS;
public static styles = [
cssManager.defaultStyles,
@@ -42,24 +74,49 @@ export class SipproxyApp extends DeesElement {
private suppressViewSelectEvent = false;
private slugFor(view: ITabbedView): string {
return view.slug ?? view.name.toLowerCase().replace(/\s+/g, '');
}
private findParent(view: ITabbedView): ITabbedView | undefined {
return this.viewTabs.find((tab) => tab.subViews?.includes(view));
}
private findViewBySlug(viewSlug: string, subviewSlug: string): ITabbedView | undefined {
const top = this.viewTabs.find((tab) => this.slugFor(tab) === viewSlug);
if (!top) return undefined;
return top.subViews?.find((subview) => this.slugFor(subview) === subviewSlug) ?? top;
}
private get currentViewTab(): ITabbedView {
const currentRoute = appRouter.getCurrentRoute();
return this.findViewBySlug(currentRoute.view, currentRoute.subview) ?? this.viewTabs[0].subViews![0];
}
async firstUpdated() {
this.appdash = this.shadowRoot?.querySelector('dees-simple-appdash') as InstanceType<typeof deesCatalog.DeesSimpleAppDash>;
if (this.appdash) {
this.notificationManager.init(this.appdash);
// Listen for user tab selections sync URL.
// Listen for user tab selections and sync grouped URLs.
this.appdash.addEventListener('view-select', ((e: CustomEvent) => {
if (this.suppressViewSelectEvent) return;
const viewName: string = e.detail?.view?.name || e.detail?.name || '';
const slug = viewName.toLowerCase();
if (slug && slug !== appRouter.getCurrentView()) {
appRouter.navigateTo(slug as any, true);
const view = e.detail?.view as ITabbedView | undefined;
if (!view) return;
const parent = this.findParent(view);
if (!parent) return;
const parentSlug = this.slugFor(parent) as TViewSlug;
const subviewSlug = this.slugFor(view) as TSubviewSlug;
if (!appRouter.isCurrentRoute(parentSlug, subviewSlug)) {
appRouter.navigateToView(parentSlug, subviewSlug, true);
}
}) as EventListener);
// Wire up router -> appdash (for browser back/forward).
appRouter.setNavigateHandler((view) => {
const tab = SLUG_TO_TAB.get(view);
appRouter.setNavigateHandler((view, subview) => {
const tab = this.findViewBySlug(view, subview);
if (tab && this.appdash) {
this.suppressViewSelectEvent = true;
this.appdash.loadView(tab);
@@ -67,21 +124,17 @@ export class SipproxyApp extends DeesElement {
}
});
// Deep link: if URL isn't "overview", navigate to the right tab.
const initial = appRouter.getCurrentView();
if (initial !== 'overview') {
const tab = SLUG_TO_TAB.get(initial);
if (tab) {
this.suppressViewSelectEvent = true;
this.appdash.loadView(tab);
this.suppressViewSelectEvent = false;
}
const initialTab = this.currentViewTab;
if (initialTab) {
this.suppressViewSelectEvent = true;
this.appdash.loadView(initialTab);
this.suppressViewSelectEvent = false;
}
}
}
disconnectedCallback() {
super.disconnectedCallback();
public async disconnectedCallback() {
await super.disconnectedCallback();
this.notificationManager.destroy();
}
@@ -89,7 +142,8 @@ export class SipproxyApp extends DeesElement {
return html`
<dees-simple-appdash
.name=${'SipRouter'}
.viewTabs=${VIEW_TABS}
.viewTabs=${this.viewTabs}
.selectedView=${this.currentViewTab}
></dees-simple-appdash>
`;
}
+2
View File
@@ -942,6 +942,8 @@ export class SipproxyViewCalls extends DeesElement {
];
return html`
<dees-heading level="3">Calls</dees-heading>
<div class="view-section">
<dees-statsgrid .tiles=${tiles} .minTileWidth=${220} .gap=${16}></dees-statsgrid>
</div>
+3 -1
View File
@@ -268,7 +268,7 @@ export class SipproxyViewContacts extends DeesElement {
type: ['inRow'] as any,
actionFunc: async ({ item }: { item: IContact }) => {
appState.selectContact(item);
appRouter.navigateTo('phone' as any);
appRouter.navigateToView('telephony', 'phone');
},
},
{
@@ -343,6 +343,8 @@ export class SipproxyViewContacts extends DeesElement {
];
return html`
<dees-heading level="3">Contacts</dees-heading>
<div class="view-section">
<dees-statsgrid .tiles=${tiles} .minTileWidth=${220} .gap=${16}></dees-statsgrid>
</div>
+2
View File
@@ -624,6 +624,8 @@ export class SipproxyViewIvr extends DeesElement {
const menus = ivr.menus || [];
return html`
<dees-heading level="3">IVR</dees-heading>
<div class="view-section">
<dees-statsgrid
.tiles=${this.getStatsTiles()}
+5 -3
View File
@@ -1,6 +1,6 @@
import { DeesElement, customElement, html, css, cssManager, state, type TemplateResult } from '../plugins.js';
import { deesCatalog } from '../plugins.js';
import { appState, type IAppState } from '../state/appstate.js';
import { viewHostCss } from './shared/index.js';
@customElement('sipproxy-view-log')
export class SipproxyViewLog extends DeesElement {
@@ -11,9 +11,9 @@ export class SipproxyViewLog extends DeesElement {
public static styles = [
cssManager.defaultStyles,
viewHostCss,
css`
:host { display: block; padding: 1rem; height: 100%; }
dees-chart-log { height: calc(100vh - 120px); }
dees-chart-log { height: calc(100vh - 140px); }
`,
];
@@ -58,6 +58,8 @@ export class SipproxyViewLog extends DeesElement {
public render(): TemplateResult {
return html`
<dees-heading level="3">Log</dees-heading>
<dees-chart-log
label="SIP Trace Log"
mode="structured"
+5 -17
View File
@@ -11,22 +11,8 @@ export class SipproxyViewOverview extends DeesElement {
cssManager.defaultStyles,
viewHostCss,
css`
:host {
display: block;
padding: 24px;
}
.section-heading {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #64748b;
margin: 32px 0 12px;
}
.section-heading:first-of-type {
margin-top: 24px;
dees-statsgrid {
margin-bottom: 32px;
}
`,
];
@@ -141,13 +127,15 @@ export class SipproxyViewOverview extends DeesElement {
const onlineCount = allDevices.filter((d) => d.connected).length;
return html`
<dees-heading level="3">Stats</dees-heading>
<dees-statsgrid
.tiles=${tiles}
.minTileWidth=${220}
.gap=${16}
></dees-statsgrid>
<div class="section-heading">Devices</div>
<dees-heading level="hr">Devices</dees-heading>
<dees-table
heading1="Devices"
heading2="${onlineCount} of ${allDevices.length} online"
+2
View File
@@ -617,6 +617,8 @@ export class SipproxyViewPhone extends DeesElement {
public render(): TemplateResult {
return html`
<dees-heading level="3">Phone</dees-heading>
<div class="phone-layout">
${this.renderDialer()}
${this.renderPhoneStatus()}
@@ -780,6 +780,8 @@ export class SipproxyViewProviders extends DeesElement {
const providers = this.appData.providers || [];
return html`
<dees-heading level="3">Providers</dees-heading>
<div class="view-section">
<dees-statsgrid
.tiles=${this.getStatsTiles()}
+2
View File
@@ -339,6 +339,8 @@ export class SipproxyViewRoutes extends DeesElement {
];
return html`
<dees-heading level="3">Route Management</dees-heading>
<div class="view-section">
<dees-statsgrid .tiles=${tiles} .minTileWidth=${220} .gap=${16}></dees-statsgrid>
</div>
@@ -458,6 +458,8 @@ export class SipproxyViewVoicemail extends DeesElement {
public render(): TemplateResult {
return html`
<dees-heading level="3">Voicemail</dees-heading>
<div class="view-section">
<dees-table
heading1="Voiceboxes"