feat(web-ui): reorganize dashboard views into grouped navigation with new email, access, and network subviews
This commit is contained in:
@@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-04-08 - 13.4.0 - feat(web-ui)
|
||||||
|
reorganize dashboard views into grouped navigation with new email, access, and network subviews
|
||||||
|
|
||||||
|
- Restructures the ops dashboard and router to use grouped top-level sections with subviews for overview, network, email, access, and security.
|
||||||
|
- Adds dedicated Email Security and API Tokens views and exposes Remote Ingress and VPN under Network subnavigation.
|
||||||
|
- Updates refresh and initial view handling to work with nested subviews, including remote ingress and VPN refresh behavior.
|
||||||
|
- Moves overview, configuration, email, API token, and remote ingress components into feature directories and standardizes shared view styling.
|
||||||
|
|
||||||
## 2026-04-08 - 13.3.0 - feat(web-ui)
|
## 2026-04-08 - 13.3.0 - feat(web-ui)
|
||||||
reorganize network and security views into tabbed subviews with route-aware navigation
|
reorganize network and security views into tabbed subviews with route-aware navigation
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
"@api.global/typedserver": "^8.4.6",
|
"@api.global/typedserver": "^8.4.6",
|
||||||
"@api.global/typedsocket": "^4.1.2",
|
"@api.global/typedsocket": "^4.1.2",
|
||||||
"@apiclient.xyz/cloudflare": "^7.1.0",
|
"@apiclient.xyz/cloudflare": "^7.1.0",
|
||||||
"@design.estate/dees-catalog": "^3.67.1",
|
"@design.estate/dees-catalog": "^3.68.0",
|
||||||
"@design.estate/dees-element": "^2.2.4",
|
"@design.estate/dees-element": "^2.2.4",
|
||||||
"@push.rocks/lik": "^6.4.0",
|
"@push.rocks/lik": "^6.4.0",
|
||||||
"@push.rocks/projectinfo": "^5.1.0",
|
"@push.rocks/projectinfo": "^5.1.0",
|
||||||
|
|||||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -24,8 +24,8 @@ importers:
|
|||||||
specifier: ^7.1.0
|
specifier: ^7.1.0
|
||||||
version: 7.1.0
|
version: 7.1.0
|
||||||
'@design.estate/dees-catalog':
|
'@design.estate/dees-catalog':
|
||||||
specifier: ^3.67.1
|
specifier: ^3.68.0
|
||||||
version: 3.67.1(@tiptap/pm@2.27.2)
|
version: 3.68.0(@tiptap/pm@2.27.2)
|
||||||
'@design.estate/dees-element':
|
'@design.estate/dees-element':
|
||||||
specifier: ^2.2.4
|
specifier: ^2.2.4
|
||||||
version: 2.2.4
|
version: 2.2.4
|
||||||
@@ -353,8 +353,8 @@ packages:
|
|||||||
'@configvault.io/interfaces@1.0.17':
|
'@configvault.io/interfaces@1.0.17':
|
||||||
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
|
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
|
||||||
|
|
||||||
'@design.estate/dees-catalog@3.67.1':
|
'@design.estate/dees-catalog@3.68.0':
|
||||||
resolution: {integrity: sha512-8zaVNP70IbcB6pEmLoBxVA5WD0N5gQr12ylTdILtvds6rftKLCI1i2jx4RBztIy4FpZv0wIewJBtRvSUjK8Ysw==}
|
resolution: {integrity: sha512-4jTq/pZmhLFS2jGsF8I+bqLV+P4O9bBAyNtF5Ga1omNCwZFQmITiwPZ2brOGvVFaVrMDi8VdY4I7FTMofF7Diw==}
|
||||||
|
|
||||||
'@design.estate/dees-comms@1.0.30':
|
'@design.estate/dees-comms@1.0.30':
|
||||||
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
|
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
|
||||||
@@ -4315,7 +4315,7 @@ snapshots:
|
|||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
'@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.3)
|
'@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.3)
|
||||||
'@cloudflare/workers-types': 4.20260405.1
|
'@cloudflare/workers-types': 4.20260405.1
|
||||||
'@design.estate/dees-catalog': 3.67.1(@tiptap/pm@2.27.2)
|
'@design.estate/dees-catalog': 3.68.0(@tiptap/pm@2.27.2)
|
||||||
'@design.estate/dees-comms': 1.0.30
|
'@design.estate/dees-comms': 1.0.30
|
||||||
'@push.rocks/lik': 6.4.0
|
'@push.rocks/lik': 6.4.0
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
@@ -4844,7 +4844,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
|
|
||||||
'@design.estate/dees-catalog@3.67.1(@tiptap/pm@2.27.2)':
|
'@design.estate/dees-catalog@3.68.0(@tiptap/pm@2.27.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@design.estate/dees-domtools': 2.5.4
|
'@design.estate/dees-domtools': 2.5.4
|
||||||
'@design.estate/dees-element': 2.2.4
|
'@design.estate/dees-element': 2.2.4
|
||||||
@@ -6900,7 +6900,7 @@ snapshots:
|
|||||||
|
|
||||||
'@serve.zone/catalog@2.12.3(@tiptap/pm@2.27.2)':
|
'@serve.zone/catalog@2.12.3(@tiptap/pm@2.27.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@design.estate/dees-catalog': 3.67.1(@tiptap/pm@2.27.2)
|
'@design.estate/dees-catalog': 3.68.0(@tiptap/pm@2.27.2)
|
||||||
'@design.estate/dees-domtools': 2.5.4
|
'@design.estate/dees-domtools': 2.5.4
|
||||||
'@design.estate/dees-element': 2.2.4
|
'@design.estate/dees-element': 2.2.4
|
||||||
'@design.estate/dees-wcctools': 3.8.0
|
'@design.estate/dees-wcctools': 3.8.0
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '13.3.0',
|
version: '13.4.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '13.3.0',
|
version: '13.4.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export const configStatePart = await appState.getStatePart<IConfigState>(
|
|||||||
// Determine initial view from URL path
|
// Determine initial view from URL path
|
||||||
const getInitialView = (): string => {
|
const getInitialView = (): string => {
|
||||||
const path = typeof window !== 'undefined' ? window.location.pathname : '/';
|
const path = typeof window !== 'undefined' ? window.location.pathname : '/';
|
||||||
const validViews = ['overview', 'network', 'emails', 'logs', 'apitokens', 'configuration', 'security', 'certificates', 'remoteingress', 'vpn'];
|
const validViews = ['overview', 'network', 'email', 'logs', 'access', 'security', 'certificates'];
|
||||||
const segments = path.split('/').filter(Boolean);
|
const segments = path.split('/').filter(Boolean);
|
||||||
const view = segments[0];
|
const view = segments[0];
|
||||||
return validViews.includes(view) ? view : 'overview';
|
return validViews.includes(view) ? view : 'overview';
|
||||||
@@ -444,20 +444,6 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If switching to apitokens view, ensure we fetch token data
|
|
||||||
if (viewName === 'apitokens' && currentState.activeView !== 'apitokens') {
|
|
||||||
setTimeout(() => {
|
|
||||||
routeManagementStatePart.dispatchAction(fetchApiTokensAction, null);
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If switching to remoteingress view, ensure we fetch edge data
|
|
||||||
if (viewName === 'remoteingress' && currentState.activeView !== 'remoteingress') {
|
|
||||||
setTimeout(() => {
|
|
||||||
remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
activeView: viewName,
|
activeView: viewName,
|
||||||
@@ -1930,6 +1916,7 @@ async function dispatchCombinedRefreshActionInner() {
|
|||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
if (!context.identity) return;
|
if (!context.identity) return;
|
||||||
const currentView = uiStatePart.getState()!.activeView;
|
const currentView = uiStatePart.getState()!.activeView;
|
||||||
|
const currentSubview = uiStatePart.getState()!.activeSubview;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Always fetch basic stats for dashboard widgets
|
// Always fetch basic stats for dashboard widgets
|
||||||
@@ -2041,8 +2028,8 @@ async function dispatchCombinedRefreshActionInner() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh remote ingress data if on remoteingress view
|
// Refresh remote ingress data if on the Network → Remote Ingress subview
|
||||||
if (currentView === 'remoteingress') {
|
if (currentView === 'network' && currentSubview === 'remoteingress') {
|
||||||
try {
|
try {
|
||||||
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -2050,8 +2037,8 @@ async function dispatchCombinedRefreshActionInner() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh VPN data if on vpn view
|
// Refresh VPN data if on the Network → VPN subview
|
||||||
if (currentView === 'vpn') {
|
if (currentView === 'network' && currentSubview === 'vpn') {
|
||||||
try {
|
try {
|
||||||
await vpnStatePart.dispatchAction(fetchVpnAction, null);
|
await vpnStatePart.dispatchAction(fetchVpnAction, null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
1
ts_web/elements/access/index.ts
Normal file
1
ts_web/elements/access/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './ops-view-apitokens.js';
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as appstate from '../appstate.js';
|
import * as appstate from '../../appstate.js';
|
||||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
||||||
import { viewHostCss } from './shared/css.js';
|
import { viewHostCss } from '../shared/css.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DeesElement,
|
DeesElement,
|
||||||
2
ts_web/elements/email/index.ts
Normal file
2
ts_web/elements/email/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './ops-view-emails.js';
|
||||||
|
export * from './ops-view-email-security.js';
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as appstate from '../../appstate.js';
|
import * as appstate from '../../appstate.js';
|
||||||
|
import { viewHostCss } from '../shared/css.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DeesElement,
|
DeesElement,
|
||||||
@@ -13,12 +14,12 @@ import { type IStatsTile } from '@design.estate/dees-catalog';
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'ops-view-security-emailsecurity': OpsViewSecurityEmailsecurity;
|
'ops-view-email-security': OpsViewEmailSecurity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@customElement('ops-view-security-emailsecurity')
|
@customElement('ops-view-email-security')
|
||||||
export class OpsViewSecurityEmailsecurity extends DeesElement {
|
export class OpsViewEmailSecurity extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
accessor statsState: appstate.IStatsState = appstate.statsStatePart.getState()!;
|
accessor statsState: appstate.IStatsState = appstate.statsStatePart.getState()!;
|
||||||
|
|
||||||
@@ -34,8 +35,8 @@ export class OpsViewSecurityEmailsecurity extends DeesElement {
|
|||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
|
viewHostCss,
|
||||||
css`
|
css`
|
||||||
:host { display: block; }
|
|
||||||
h2 {
|
h2 {
|
||||||
margin: 32px 0 16px 0;
|
margin: 32px 0 16px 0;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element';
|
import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element';
|
||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import * as appstate from '../appstate.js';
|
import * as appstate from '../../appstate.js';
|
||||||
import * as shared from './shared/index.js';
|
import * as shared from '../shared/index.js';
|
||||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
export * from './ops-dashboard.js';
|
export * from './ops-dashboard.js';
|
||||||
export * from './ops-view-overview.js';
|
export * from './overview/index.js';
|
||||||
export * from './network/index.js';
|
export * from './network/index.js';
|
||||||
export * from './ops-view-emails.js';
|
export * from './email/index.js';
|
||||||
export * from './ops-view-logs.js';
|
export * from './ops-view-logs.js';
|
||||||
export * from './ops-view-config.js';
|
export * from './access/index.js';
|
||||||
export * from './ops-view-apitokens.js';
|
|
||||||
export * from './security/index.js';
|
export * from './security/index.js';
|
||||||
export * from './ops-view-certificates.js';
|
export * from './ops-view-certificates.js';
|
||||||
export * from './ops-view-remoteingress.js';
|
export * from './shared/index.js';
|
||||||
export * from './ops-view-vpn.js';
|
|
||||||
export * from './shared/index.js';
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export * from './ops-view-network.js';
|
|
||||||
export * from './ops-view-network-activity.js';
|
export * from './ops-view-network-activity.js';
|
||||||
export * from './ops-view-routes.js';
|
export * from './ops-view-routes.js';
|
||||||
export * from './ops-view-sourceprofiles.js';
|
export * from './ops-view-sourceprofiles.js';
|
||||||
export * from './ops-view-networktargets.js';
|
export * from './ops-view-networktargets.js';
|
||||||
export * from './ops-view-targetprofiles.js';
|
export * from './ops-view-targetprofiles.js';
|
||||||
|
export * from './ops-view-remoteingress.js';
|
||||||
|
export * from './ops-view-vpn.js';
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element';
|
import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element';
|
||||||
import * as appstate from '../../appstate.js';
|
import * as appstate from '../../appstate.js';
|
||||||
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
||||||
|
import { viewHostCss } from '../shared/css.js';
|
||||||
import { type IStatsTile } from '@design.estate/dees-catalog';
|
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -175,8 +176,8 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
|
viewHostCss,
|
||||||
css`
|
css`
|
||||||
:host { display: block; }
|
|
||||||
.networkContainer {
|
.networkContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
import * as appstate from '../../appstate.js';
|
|
||||||
import { appRouter } from '../../router.js';
|
|
||||||
import { viewHostCss } from '../shared/css.js';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
customElement,
|
|
||||||
html,
|
|
||||||
state,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
type TemplateResult,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
// Side-effect imports register the subview custom elements
|
|
||||||
import './ops-view-network-activity.js';
|
|
||||||
import './ops-view-routes.js';
|
|
||||||
import './ops-view-sourceprofiles.js';
|
|
||||||
import './ops-view-networktargets.js';
|
|
||||||
import './ops-view-targetprofiles.js';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
'ops-view-network': OpsViewNetwork;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type TNetworkTab = 'activity' | 'routes' | 'sourceprofiles' | 'networktargets' | 'targetprofiles';
|
|
||||||
|
|
||||||
@customElement('ops-view-network')
|
|
||||||
export class OpsViewNetwork extends DeesElement {
|
|
||||||
@state()
|
|
||||||
accessor selectedTab: TNetworkTab = 'activity';
|
|
||||||
|
|
||||||
private tabLabelMap: Record<TNetworkTab, string> = {
|
|
||||||
'activity': 'Network Activity',
|
|
||||||
'routes': 'Routes',
|
|
||||||
'sourceprofiles': 'Source Profiles',
|
|
||||||
'networktargets': 'Network Targets',
|
|
||||||
'targetprofiles': 'Target Profiles',
|
|
||||||
};
|
|
||||||
|
|
||||||
private labelToTab: Record<string, TNetworkTab> = {
|
|
||||||
'Network Activity': 'activity',
|
|
||||||
'Routes': 'routes',
|
|
||||||
'Source Profiles': 'sourceprofiles',
|
|
||||||
'Network Targets': 'networktargets',
|
|
||||||
'Target Profiles': 'targetprofiles',
|
|
||||||
};
|
|
||||||
|
|
||||||
private static isNetworkTab(s: string | null): s is TNetworkTab {
|
|
||||||
return s === 'activity' || s === 'routes' || s === 'sourceprofiles'
|
|
||||||
|| s === 'networktargets' || s === 'targetprofiles';
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
// Read initial subview from state (URL-driven)
|
|
||||||
const initialState = appstate.uiStatePart.getState()!;
|
|
||||||
if (OpsViewNetwork.isNetworkTab(initialState.activeSubview)) {
|
|
||||||
this.selectedTab = initialState.activeSubview;
|
|
||||||
}
|
|
||||||
// Subscribe to future changes (back/forward navigation, direct URL entry)
|
|
||||||
const sub = appstate.uiStatePart.select((s) => s.activeSubview).subscribe((sub) => {
|
|
||||||
if (OpsViewNetwork.isNetworkTab(sub) && sub !== this.selectedTab) {
|
|
||||||
this.selectedTab = sub;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.rxSubscriptions.push(sub);
|
|
||||||
}
|
|
||||||
|
|
||||||
async firstUpdated() {
|
|
||||||
const toggle = this.shadowRoot!.querySelector('dees-input-multitoggle') as any;
|
|
||||||
if (toggle) {
|
|
||||||
const sub = toggle.changeSubject.subscribe(() => {
|
|
||||||
const tab = this.labelToTab[toggle.selectedOption];
|
|
||||||
if (tab && tab !== this.selectedTab) {
|
|
||||||
// Push URL → router updates state → subscription updates selectedTab
|
|
||||||
appRouter.navigateToView('network', tab);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.rxSubscriptions.push(sub);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
viewHostCss,
|
|
||||||
css`
|
|
||||||
dees-input-multitoggle {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
public render(): TemplateResult {
|
|
||||||
return html`
|
|
||||||
<dees-heading level="2">Network</dees-heading>
|
|
||||||
|
|
||||||
<dees-input-multitoggle
|
|
||||||
.type=${'single'}
|
|
||||||
.options=${['Network Activity', 'Routes', 'Source Profiles', 'Network Targets', 'Target Profiles']}
|
|
||||||
.selectedOption=${this.tabLabelMap[this.selectedTab]}
|
|
||||||
></dees-input-multitoggle>
|
|
||||||
|
|
||||||
${this.renderTabContent()}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderTabContent(): TemplateResult {
|
|
||||||
switch (this.selectedTab) {
|
|
||||||
case 'activity': return html`<ops-view-network-activity></ops-view-network-activity>`;
|
|
||||||
case 'routes': return html`<ops-view-routes></ops-view-routes>`;
|
|
||||||
case 'sourceprofiles': return html`<ops-view-sourceprofiles></ops-view-sourceprofiles>`;
|
|
||||||
case 'networktargets': return html`<ops-view-networktargets></ops-view-networktargets>`;
|
|
||||||
case 'targetprofiles': return html`<ops-view-targetprofiles></ops-view-targetprofiles>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
import * as appstate from '../../appstate.js';
|
import * as appstate from '../../appstate.js';
|
||||||
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
||||||
|
import { viewHostCss } from '../shared/css.js';
|
||||||
import { type IStatsTile } from '@design.estate/dees-catalog';
|
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -37,8 +38,8 @@ export class OpsViewNetworkTargets extends DeesElement {
|
|||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
|
viewHostCss,
|
||||||
css`
|
css`
|
||||||
:host { display: block; }
|
|
||||||
.targetsContainer {
|
.targetsContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import {
|
|||||||
state,
|
state,
|
||||||
cssManager,
|
cssManager,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
import * as appstate from '../appstate.js';
|
import * as appstate from '../../appstate.js';
|
||||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
||||||
import { viewHostCss } from './shared/css.js';
|
import { viewHostCss } from '../shared/css.js';
|
||||||
import { type IStatsTile } from '@design.estate/dees-catalog';
|
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -174,7 +174,7 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<dees-heading level="2">Remote Ingress</dees-heading>
|
<dees-heading level="hr">Remote Ingress</dees-heading>
|
||||||
|
|
||||||
${this.riState.newEdgeId ? html`
|
${this.riState.newEdgeId ? html`
|
||||||
<div class="secretDialog">
|
<div class="secretDialog">
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as appstate from '../../appstate.js';
|
import * as appstate from '../../appstate.js';
|
||||||
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
||||||
|
import { viewHostCss } from '../shared/css.js';
|
||||||
import { type IStatsTile } from '@design.estate/dees-catalog';
|
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -96,8 +97,8 @@ export class OpsViewRoutes extends DeesElement {
|
|||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
|
viewHostCss,
|
||||||
css`
|
css`
|
||||||
:host { display: block; }
|
|
||||||
.routesContainer {
|
.routesContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
import * as appstate from '../../appstate.js';
|
import * as appstate from '../../appstate.js';
|
||||||
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
||||||
|
import { viewHostCss } from '../shared/css.js';
|
||||||
import { type IStatsTile } from '@design.estate/dees-catalog';
|
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -37,8 +38,8 @@ export class OpsViewSourceProfiles extends DeesElement {
|
|||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
|
viewHostCss,
|
||||||
css`
|
css`
|
||||||
:host { display: block; }
|
|
||||||
.profilesContainer {
|
.profilesContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import * as appstate from '../../appstate.js';
|
import * as appstate from '../../appstate.js';
|
||||||
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
||||||
|
import { viewHostCss } from '../shared/css.js';
|
||||||
import { type IStatsTile } from '@design.estate/dees-catalog';
|
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -38,8 +39,8 @@ export class OpsViewTargetProfiles extends DeesElement {
|
|||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
|
viewHostCss,
|
||||||
css`
|
css`
|
||||||
:host { display: block; }
|
|
||||||
.profilesContainer {
|
.profilesContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import {
|
|||||||
state,
|
state,
|
||||||
cssManager,
|
cssManager,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import * as appstate from '../appstate.js';
|
import * as appstate from '../../appstate.js';
|
||||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
||||||
import { viewHostCss } from './shared/css.js';
|
import { viewHostCss } from '../shared/css.js';
|
||||||
import { type IStatsTile } from '@design.estate/dees-catalog';
|
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -223,7 +223,7 @@ export class OpsViewVpn extends DeesElement {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<dees-heading level="2">VPN</dees-heading>
|
<dees-heading level="hr">VPN</dees-heading>
|
||||||
<div class="vpnContainer">
|
<div class="vpnContainer">
|
||||||
|
|
||||||
${this.vpnState.newClientConfig ? html`
|
${this.vpnState.newClientConfig ? html`
|
||||||
@@ -11,18 +11,45 @@ import {
|
|||||||
state,
|
state,
|
||||||
type TemplateResult
|
type TemplateResult
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
import type { IView } from '@design.estate/dees-catalog';
|
||||||
|
|
||||||
// Import view components
|
// Top-level / flat views
|
||||||
import { OpsViewOverview } from './ops-view-overview.js';
|
|
||||||
import { OpsViewNetwork } from './network/ops-view-network.js';
|
|
||||||
import { OpsViewEmails } from './ops-view-emails.js';
|
|
||||||
import { OpsViewLogs } from './ops-view-logs.js';
|
import { OpsViewLogs } from './ops-view-logs.js';
|
||||||
import { OpsViewConfig } from './ops-view-config.js';
|
|
||||||
import { OpsViewApiTokens } from './ops-view-apitokens.js';
|
|
||||||
import { OpsViewSecurity } from './security/ops-view-security.js';
|
|
||||||
import { OpsViewCertificates } from './ops-view-certificates.js';
|
import { OpsViewCertificates } from './ops-view-certificates.js';
|
||||||
import { OpsViewRemoteIngress } from './ops-view-remoteingress.js';
|
|
||||||
import { OpsViewVpn } from './ops-view-vpn.js';
|
// Overview group
|
||||||
|
import { OpsViewOverview } from './overview/ops-view-overview.js';
|
||||||
|
import { OpsViewConfig } from './overview/ops-view-config.js';
|
||||||
|
|
||||||
|
// Network group
|
||||||
|
import { OpsViewNetworkActivity } from './network/ops-view-network-activity.js';
|
||||||
|
import { OpsViewRoutes } from './network/ops-view-routes.js';
|
||||||
|
import { OpsViewSourceProfiles } from './network/ops-view-sourceprofiles.js';
|
||||||
|
import { OpsViewNetworkTargets } from './network/ops-view-networktargets.js';
|
||||||
|
import { OpsViewTargetProfiles } from './network/ops-view-targetprofiles.js';
|
||||||
|
import { OpsViewRemoteIngress } from './network/ops-view-remoteingress.js';
|
||||||
|
import { OpsViewVpn } from './network/ops-view-vpn.js';
|
||||||
|
|
||||||
|
// Email group
|
||||||
|
import { OpsViewEmails } from './email/ops-view-emails.js';
|
||||||
|
import { OpsViewEmailSecurity } from './email/ops-view-email-security.js';
|
||||||
|
|
||||||
|
// Access group
|
||||||
|
import { OpsViewApiTokens } from './access/ops-view-apitokens.js';
|
||||||
|
|
||||||
|
// Security group
|
||||||
|
import { OpsViewSecurityOverview } from './security/ops-view-security-overview.js';
|
||||||
|
import { OpsViewSecurityBlocked } from './security/ops-view-security-blocked.js';
|
||||||
|
import { OpsViewSecurityAuthentication } from './security/ops-view-security-authentication.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended IView with explicit URL slug. Without an explicit `slug`, the URL
|
||||||
|
* slug is derived from `name.toLowerCase().replace(/\s+/g, '')`.
|
||||||
|
*/
|
||||||
|
interface ITabbedView extends IView {
|
||||||
|
slug?: string;
|
||||||
|
subViews?: ITabbedView[];
|
||||||
|
}
|
||||||
|
|
||||||
@customElement('ops-dashboard')
|
@customElement('ops-dashboard')
|
||||||
export class OpsDashboard extends DeesElement {
|
export class OpsDashboard extends DeesElement {
|
||||||
@@ -46,27 +73,36 @@ export class OpsDashboard extends DeesElement {
|
|||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store viewTabs as a property to maintain object references
|
// Store viewTabs as a property to maintain object references (used for === selectedView identity)
|
||||||
private viewTabs = [
|
private viewTabs: ITabbedView[] = [
|
||||||
{
|
{
|
||||||
name: 'Overview',
|
name: 'Overview',
|
||||||
iconName: 'lucide:layoutDashboard',
|
iconName: 'lucide:layoutDashboard',
|
||||||
element: OpsViewOverview,
|
subViews: [
|
||||||
},
|
{ slug: 'stats', name: 'Stats', iconName: 'lucide:activity', element: OpsViewOverview },
|
||||||
{
|
{ slug: 'configuration', name: 'Configuration', iconName: 'lucide:settings', element: OpsViewConfig },
|
||||||
name: 'Configuration',
|
],
|
||||||
iconName: 'lucide:settings',
|
|
||||||
element: OpsViewConfig,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Network',
|
name: 'Network',
|
||||||
iconName: 'lucide:network',
|
iconName: 'lucide:network',
|
||||||
element: OpsViewNetwork,
|
subViews: [
|
||||||
|
{ slug: 'activity', name: 'Network Activity', iconName: 'lucide:activity', element: OpsViewNetworkActivity },
|
||||||
|
{ slug: 'routes', name: 'Routes', iconName: 'lucide:route', element: OpsViewRoutes },
|
||||||
|
{ slug: 'sourceprofiles', name: 'Source Profiles', iconName: 'lucide:shieldCheck', element: OpsViewSourceProfiles },
|
||||||
|
{ slug: 'networktargets', name: 'Network Targets', iconName: 'lucide:server', element: OpsViewNetworkTargets },
|
||||||
|
{ slug: 'targetprofiles', name: 'Target Profiles', iconName: 'lucide:target', element: OpsViewTargetProfiles },
|
||||||
|
{ slug: 'remoteingress', name: 'Remote Ingress', iconName: 'lucide:globe', element: OpsViewRemoteIngress },
|
||||||
|
{ slug: 'vpn', name: 'VPN', iconName: 'lucide:shield', element: OpsViewVpn },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Emails',
|
name: 'Email',
|
||||||
iconName: 'lucide:mail',
|
iconName: 'lucide:mail',
|
||||||
element: OpsViewEmails,
|
subViews: [
|
||||||
|
{ slug: 'log', name: 'Email Log', iconName: 'lucide:scrollText', element: OpsViewEmails },
|
||||||
|
{ slug: 'security', name: 'Email Security', iconName: 'lucide:shieldCheck', element: OpsViewEmailSecurity },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Logs',
|
name: 'Logs',
|
||||||
@@ -74,32 +110,48 @@ export class OpsDashboard extends DeesElement {
|
|||||||
element: OpsViewLogs,
|
element: OpsViewLogs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ApiTokens',
|
name: 'Access',
|
||||||
iconName: 'lucide:key',
|
iconName: 'lucide:keyRound',
|
||||||
element: OpsViewApiTokens,
|
subViews: [
|
||||||
|
{ slug: 'apitokens', name: 'API Tokens', iconName: 'lucide:key', element: OpsViewApiTokens },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Security',
|
name: 'Security',
|
||||||
iconName: 'lucide:shield',
|
iconName: 'lucide:shield',
|
||||||
element: OpsViewSecurity,
|
subViews: [
|
||||||
|
{ slug: 'overview', name: 'Overview', iconName: 'lucide:eye', element: OpsViewSecurityOverview },
|
||||||
|
{ slug: 'blocked', name: 'Blocked IPs', iconName: 'lucide:shieldBan', element: OpsViewSecurityBlocked },
|
||||||
|
{ slug: 'authentication', name: 'Authentication', iconName: 'lucide:lock', element: OpsViewSecurityAuthentication },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Certificates',
|
name: 'Certificates',
|
||||||
iconName: 'lucide:badgeCheck',
|
iconName: 'lucide:badgeCheck',
|
||||||
element: OpsViewCertificates,
|
element: OpsViewCertificates,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'RemoteIngress',
|
|
||||||
iconName: 'lucide:globe',
|
|
||||||
element: OpsViewRemoteIngress,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'VPN',
|
|
||||||
iconName: 'lucide:shield',
|
|
||||||
element: OpsViewVpn,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** URL slug for a view (explicit `slug` field, or lowercased name with spaces stripped). */
|
||||||
|
private slugFor(view: ITabbedView): string {
|
||||||
|
return view.slug ?? view.name.toLowerCase().replace(/\s+/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Find the parent group of a subview, or undefined for top-level views. */
|
||||||
|
private findParent(view: ITabbedView): ITabbedView | undefined {
|
||||||
|
return this.viewTabs.find((v) => v.subViews?.includes(view));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Look up a view (or subview) by its URL slug pair. */
|
||||||
|
private findViewBySlug(viewSlug: string, subSlug: string | null): ITabbedView | undefined {
|
||||||
|
const top = this.viewTabs.find((v) => this.slugFor(v) === viewSlug);
|
||||||
|
if (!top) return undefined;
|
||||||
|
if (subSlug && top.subViews) {
|
||||||
|
return top.subViews.find((sv) => this.slugFor(sv) === subSlug) ?? top;
|
||||||
|
}
|
||||||
|
return top;
|
||||||
|
}
|
||||||
|
|
||||||
private get globalMessages() {
|
private get globalMessages() {
|
||||||
const messages: Array<{ id: string; type: string; message: string; dismissible?: boolean }> = [];
|
const messages: Array<{ id: string; type: string; message: string; dismissible?: boolean }> = [];
|
||||||
const config = this.configState.config;
|
const config = this.configState.config;
|
||||||
@@ -115,17 +167,19 @@ export class OpsDashboard extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current view tab based on the UI state's activeView.
|
* Get the current view tab based on the UI state's activeView/activeSubview.
|
||||||
* Used to pass the correct selectedView to dees-simple-appdash on initial render.
|
* Used to pass the correct selectedView to dees-simple-appdash on initial render.
|
||||||
*/
|
*/
|
||||||
private get currentViewTab() {
|
private get currentViewTab(): ITabbedView {
|
||||||
return this.viewTabs.find(t => t.name.toLowerCase() === this.uiState.activeView) || this.viewTabs[0];
|
return (
|
||||||
|
this.findViewBySlug(this.uiState.activeView, this.uiState.activeSubview) ?? this.viewTabs[0]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
document.title = 'DCRouter OpsServer';
|
document.title = 'DCRouter OpsServer';
|
||||||
|
|
||||||
// Subscribe to login state
|
// Subscribe to login state
|
||||||
const loginSubscription = appstate.loginStatePart
|
const loginSubscription = appstate.loginStatePart
|
||||||
.select((stateArg) => stateArg)
|
.select((stateArg) => stateArg)
|
||||||
@@ -138,7 +192,7 @@ export class OpsDashboard extends DeesElement {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.rxSubscriptions.push(loginSubscription);
|
this.rxSubscriptions.push(loginSubscription);
|
||||||
|
|
||||||
// Subscribe to config state (for global warnings)
|
// Subscribe to config state (for global warnings)
|
||||||
const configSubscription = appstate.configStatePart
|
const configSubscription = appstate.configStatePart
|
||||||
.select((stateArg) => stateArg)
|
.select((stateArg) => stateArg)
|
||||||
@@ -153,38 +207,27 @@ export class OpsDashboard extends DeesElement {
|
|||||||
.subscribe((uiState) => {
|
.subscribe((uiState) => {
|
||||||
this.uiState = uiState;
|
this.uiState = uiState;
|
||||||
// Sync appdash view when state changes (e.g., from URL navigation)
|
// Sync appdash view when state changes (e.g., from URL navigation)
|
||||||
this.syncAppdashView(uiState.activeView);
|
this.syncAppdashView(uiState.activeView, uiState.activeSubview);
|
||||||
});
|
});
|
||||||
this.rxSubscriptions.push(uiSubscription);
|
this.rxSubscriptions.push(uiSubscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync the dees-simple-appdash view selection with the current state.
|
* Sync the dees-simple-appdash view selection with the current state.
|
||||||
* This is needed when the URL changes and we need to update the UI.
|
* This is needed when the URL changes externally (back/forward, deep link).
|
||||||
*/
|
*/
|
||||||
private syncAppdashView(viewName: string): void {
|
private syncAppdashView(viewSlug: string, subviewSlug: string | null): void {
|
||||||
const appDash = this.shadowRoot?.querySelector('dees-simple-appdash') as any;
|
const appDash = this.shadowRoot?.querySelector('dees-simple-appdash') as any;
|
||||||
if (!appDash) return;
|
if (!appDash) return;
|
||||||
|
|
||||||
const targetTab = this.viewTabs.find(t => t.name.toLowerCase() === viewName);
|
const targetView = this.findViewBySlug(viewSlug, subviewSlug);
|
||||||
if (!targetTab) return;
|
if (!targetView) return;
|
||||||
|
|
||||||
// Check if we need to switch (avoid unnecessary updates)
|
if (appDash.selectedView === targetView) return;
|
||||||
if (appDash.selectedView === targetTab) return;
|
|
||||||
|
|
||||||
// Update the selected view programmatically
|
// Use loadView to update both selectedView and the mounted element.
|
||||||
appDash.selectedView = targetTab;
|
// It will dispatch view-select; our handler skips when state already matches.
|
||||||
|
appDash.loadView(targetView);
|
||||||
// Update the displayed content
|
|
||||||
const content = appDash.shadowRoot?.querySelector('.appcontent');
|
|
||||||
if (content) {
|
|
||||||
if (appDash.currentView) {
|
|
||||||
appDash.currentView.remove();
|
|
||||||
}
|
|
||||||
const view = new targetTab.element();
|
|
||||||
content.appendChild(view);
|
|
||||||
appDash.currentView = view;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
@@ -226,7 +269,7 @@ export class OpsDashboard extends DeesElement {
|
|||||||
public async firstUpdated() {
|
public async firstUpdated() {
|
||||||
const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any;
|
const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any;
|
||||||
simpleLogin.addEventListener('login', (e: Event) => {
|
simpleLogin.addEventListener('login', (e: Event) => {
|
||||||
// Handle logout event
|
// Handle login event
|
||||||
const detail = (e as CustomEvent).detail;
|
const detail = (e as CustomEvent).detail;
|
||||||
this.login(detail.data.username, detail.data.password);
|
this.login(detail.data.username, detail.data.password);
|
||||||
});
|
});
|
||||||
@@ -235,9 +278,24 @@ export class OpsDashboard extends DeesElement {
|
|||||||
const appDash = this.shadowRoot!.querySelector('dees-simple-appdash');
|
const appDash = this.shadowRoot!.querySelector('dees-simple-appdash');
|
||||||
if (appDash) {
|
if (appDash) {
|
||||||
appDash.addEventListener('view-select', (e: Event) => {
|
appDash.addEventListener('view-select', (e: Event) => {
|
||||||
const viewName = (e as CustomEvent).detail.view.name.toLowerCase();
|
const view = (e as CustomEvent).detail.view as ITabbedView;
|
||||||
// Use router for navigation instead of direct state update
|
const parent = this.findParent(view);
|
||||||
appRouter.navigateToView(viewName);
|
const currentState = appstate.uiStatePart.getState();
|
||||||
|
if (parent) {
|
||||||
|
const parentSlug = this.slugFor(parent);
|
||||||
|
const subSlug = this.slugFor(view);
|
||||||
|
// Skip if already on this exact subview — preserves URL on initial mount
|
||||||
|
if (currentState?.activeView === parentSlug && currentState?.activeSubview === subSlug) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
appRouter.navigateToView(parentSlug, subSlug);
|
||||||
|
} else {
|
||||||
|
const slug = this.slugFor(view);
|
||||||
|
if (currentState?.activeView === slug && !currentState?.activeSubview) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
appRouter.navigateToView(slug);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle logout event
|
// Handle logout event
|
||||||
@@ -283,12 +341,12 @@ export class OpsDashboard extends DeesElement {
|
|||||||
const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any;
|
const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any;
|
||||||
const form = simpleLogin.shadowRoot!.querySelector('dees-form') as any;
|
const form = simpleLogin.shadowRoot!.querySelector('dees-form') as any;
|
||||||
form.setStatus('pending', 'Logging in...');
|
form.setStatus('pending', 'Logging in...');
|
||||||
|
|
||||||
const state = await appstate.loginStatePart.dispatchAction(appstate.loginAction, {
|
const state = await appstate.loginStatePart.dispatchAction(appstate.loginAction, {
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (state.identity) {
|
if (state.identity) {
|
||||||
console.log('Login successful');
|
console.log('Login successful');
|
||||||
this.loginState = state;
|
this.loginState = state;
|
||||||
@@ -302,4 +360,4 @@ export class OpsDashboard extends DeesElement {
|
|||||||
form!.reset();
|
form!.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
ts_web/elements/overview/index.ts
Normal file
2
ts_web/elements/overview/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './ops-view-overview.js';
|
||||||
|
export * from './ops-view-config.js';
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import * as shared from './shared/index.js';
|
import * as shared from '../shared/index.js';
|
||||||
import * as appstate from '../appstate.js';
|
import * as appstate from '../../appstate.js';
|
||||||
import { appRouter } from '../router.js';
|
import { appRouter } from '../../router.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DeesElement,
|
DeesElement,
|
||||||
@@ -181,7 +181,7 @@ export class OpsViewConfig extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const actions: IConfigSectionAction[] = [
|
const actions: IConfigSectionAction[] = [
|
||||||
{ label: 'View Emails', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'emails' } },
|
{ label: 'View Emails', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'email', subview: 'log' } },
|
||||||
];
|
];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@@ -305,7 +305,7 @@ export class OpsViewConfig extends DeesElement {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const actions: IConfigSectionAction[] = [
|
const actions: IConfigSectionAction[] = [
|
||||||
{ label: 'View Remote Ingress', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'remoteingress' } },
|
{ label: 'View Remote Ingress', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'network', subview: 'remoteingress' } },
|
||||||
];
|
];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import * as shared from './shared/index.js';
|
import * as shared from '../shared/index.js';
|
||||||
import * as appstate from '../appstate.js';
|
import * as appstate from '../../appstate.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DeesElement,
|
DeesElement,
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
export * from './ops-view-security.js';
|
|
||||||
export * from './ops-view-security-overview.js';
|
export * from './ops-view-security-overview.js';
|
||||||
export * from './ops-view-security-blocked.js';
|
export * from './ops-view-security-blocked.js';
|
||||||
export * from './ops-view-security-authentication.js';
|
export * from './ops-view-security-authentication.js';
|
||||||
export * from './ops-view-security-emailsecurity.js';
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as appstate from '../../appstate.js';
|
import * as appstate from '../../appstate.js';
|
||||||
|
import { viewHostCss } from '../shared/css.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DeesElement,
|
DeesElement,
|
||||||
@@ -34,8 +35,8 @@ export class OpsViewSecurityAuthentication extends DeesElement {
|
|||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
|
viewHostCss,
|
||||||
css`
|
css`
|
||||||
:host { display: block; }
|
|
||||||
h2 {
|
h2 {
|
||||||
margin: 32px 0 16px 0;
|
margin: 32px 0 16px 0;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as appstate from '../../appstate.js';
|
import * as appstate from '../../appstate.js';
|
||||||
|
import { viewHostCss } from '../shared/css.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DeesElement,
|
DeesElement,
|
||||||
@@ -34,8 +35,8 @@ export class OpsViewSecurityBlocked extends DeesElement {
|
|||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
|
viewHostCss,
|
||||||
css`
|
css`
|
||||||
:host { display: block; }
|
|
||||||
dees-statsgrid {
|
dees-statsgrid {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as appstate from '../../appstate.js';
|
import * as appstate from '../../appstate.js';
|
||||||
|
import { viewHostCss } from '../shared/css.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DeesElement,
|
DeesElement,
|
||||||
@@ -34,8 +35,8 @@ export class OpsViewSecurityOverview extends DeesElement {
|
|||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
|
viewHostCss,
|
||||||
css`
|
css`
|
||||||
:host { display: block; }
|
|
||||||
h2 {
|
h2 {
|
||||||
margin: 32px 0 16px 0;
|
margin: 32px 0 16px 0;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
import * as appstate from '../../appstate.js';
|
|
||||||
import { appRouter } from '../../router.js';
|
|
||||||
import { viewHostCss } from '../shared/css.js';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
customElement,
|
|
||||||
html,
|
|
||||||
state,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
type TemplateResult,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
// Side-effect imports register the subview custom elements
|
|
||||||
import './ops-view-security-overview.js';
|
|
||||||
import './ops-view-security-blocked.js';
|
|
||||||
import './ops-view-security-authentication.js';
|
|
||||||
import './ops-view-security-emailsecurity.js';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
'ops-view-security': OpsViewSecurity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type TSecurityTab = 'overview' | 'blocked' | 'authentication' | 'emailsecurity';
|
|
||||||
|
|
||||||
@customElement('ops-view-security')
|
|
||||||
export class OpsViewSecurity extends DeesElement {
|
|
||||||
@state()
|
|
||||||
accessor selectedTab: TSecurityTab = 'overview';
|
|
||||||
|
|
||||||
private tabLabelMap: Record<TSecurityTab, string> = {
|
|
||||||
'overview': 'Overview',
|
|
||||||
'blocked': 'Blocked IPs',
|
|
||||||
'authentication': 'Authentication',
|
|
||||||
'emailsecurity': 'Email Security',
|
|
||||||
};
|
|
||||||
|
|
||||||
private labelToTab: Record<string, TSecurityTab> = {
|
|
||||||
'Overview': 'overview',
|
|
||||||
'Blocked IPs': 'blocked',
|
|
||||||
'Authentication': 'authentication',
|
|
||||||
'Email Security': 'emailsecurity',
|
|
||||||
};
|
|
||||||
|
|
||||||
private static isSecurityTab(s: string | null): s is TSecurityTab {
|
|
||||||
return s === 'overview' || s === 'blocked' || s === 'authentication' || s === 'emailsecurity';
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
// Read initial subview from state (URL-driven)
|
|
||||||
const initialState = appstate.uiStatePart.getState()!;
|
|
||||||
if (OpsViewSecurity.isSecurityTab(initialState.activeSubview)) {
|
|
||||||
this.selectedTab = initialState.activeSubview;
|
|
||||||
}
|
|
||||||
// Subscribe to future changes (back/forward navigation, direct URL entry)
|
|
||||||
const sub = appstate.uiStatePart.select((s) => s.activeSubview).subscribe((sub) => {
|
|
||||||
if (OpsViewSecurity.isSecurityTab(sub) && sub !== this.selectedTab) {
|
|
||||||
this.selectedTab = sub;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.rxSubscriptions.push(sub);
|
|
||||||
}
|
|
||||||
|
|
||||||
async firstUpdated() {
|
|
||||||
const toggle = this.shadowRoot!.querySelector('dees-input-multitoggle') as any;
|
|
||||||
if (toggle) {
|
|
||||||
const sub = toggle.changeSubject.subscribe(() => {
|
|
||||||
const tab = this.labelToTab[toggle.selectedOption];
|
|
||||||
if (tab && tab !== this.selectedTab) {
|
|
||||||
// Push URL → router updates state → subscription updates selectedTab
|
|
||||||
appRouter.navigateToView('security', tab);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.rxSubscriptions.push(sub);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
viewHostCss,
|
|
||||||
css`
|
|
||||||
dees-input-multitoggle {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
public render(): TemplateResult {
|
|
||||||
return html`
|
|
||||||
<dees-heading level="2">Security</dees-heading>
|
|
||||||
|
|
||||||
<dees-input-multitoggle
|
|
||||||
.type=${'single'}
|
|
||||||
.options=${['Overview', 'Blocked IPs', 'Authentication', 'Email Security']}
|
|
||||||
.selectedOption=${this.tabLabelMap[this.selectedTab]}
|
|
||||||
></dees-input-multitoggle>
|
|
||||||
|
|
||||||
${this.renderTabContent()}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderTabContent(): TemplateResult {
|
|
||||||
switch (this.selectedTab) {
|
|
||||||
case 'overview': return html`<ops-view-security-overview></ops-view-security-overview>`;
|
|
||||||
case 'blocked': return html`<ops-view-security-blocked></ops-view-security-blocked>`;
|
|
||||||
case 'authentication': return html`<ops-view-security-authentication></ops-view-security-authentication>`;
|
|
||||||
case 'emailsecurity': return html`<ops-view-security-emailsecurity></ops-view-security-emailsecurity>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,17 +4,23 @@ import * as appstate from './appstate.js';
|
|||||||
const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
|
const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
|
||||||
|
|
||||||
// Flat top-level views (no subviews)
|
// Flat top-level views (no subviews)
|
||||||
const flatViews = ['overview', 'configuration', 'emails', 'logs', 'apitokens', 'certificates', 'remoteingress', 'vpn'] as const;
|
const flatViews = ['logs', 'certificates'] as const;
|
||||||
|
|
||||||
// Tabbed views and their valid subviews
|
// Tabbed views and their valid subviews
|
||||||
const subviewMap: Record<string, readonly string[]> = {
|
const subviewMap: Record<string, readonly string[]> = {
|
||||||
network: ['activity', 'routes', 'sourceprofiles', 'networktargets', 'targetprofiles'] as const,
|
overview: ['stats', 'configuration'] as const,
|
||||||
security: ['overview', 'blocked', 'authentication', 'emailsecurity'] as const,
|
network: ['activity', 'routes', 'sourceprofiles', 'networktargets', 'targetprofiles', 'remoteingress', 'vpn'] as const,
|
||||||
|
email: ['log', 'security'] as const,
|
||||||
|
access: ['apitokens'] as const,
|
||||||
|
security: ['overview', 'blocked', 'authentication'] as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Default subview when user visits the bare parent URL
|
// Default subview when user visits the bare parent URL
|
||||||
const defaultSubview: Record<string, string> = {
|
const defaultSubview: Record<string, string> = {
|
||||||
|
overview: 'stats',
|
||||||
network: 'activity',
|
network: 'activity',
|
||||||
|
email: 'log',
|
||||||
|
access: 'apitokens',
|
||||||
security: 'overview',
|
security: 'overview',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user