feat(routes): add route edit and delete actions to the ops routes view
This commit is contained in:
@@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-04-02 - 12.4.0 - feat(routes)
|
||||||
|
add route edit and delete actions to the ops routes view
|
||||||
|
|
||||||
|
- introduces an update route action in web app state and refreshes merged routes after changes
|
||||||
|
- adds edit and delete handlers with modal-based confirmation and route form inputs for programmatic routes
|
||||||
|
- enables realtime chart window configuration in network and overview dashboards
|
||||||
|
- bumps @serve.zone/catalog to ^2.11.0
|
||||||
|
|
||||||
## 2026-04-02 - 12.3.0 - feat(docs,ops-dashboard)
|
## 2026-04-02 - 12.3.0 - feat(docs,ops-dashboard)
|
||||||
document unified database and reusable security profile and network target management
|
document unified database and reusable security profile and network target management
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
"@push.rocks/smartunique": "^3.0.9",
|
"@push.rocks/smartunique": "^3.0.9",
|
||||||
"@push.rocks/smartvpn": "1.19.1",
|
"@push.rocks/smartvpn": "1.19.1",
|
||||||
"@push.rocks/taskbuffer": "^8.0.2",
|
"@push.rocks/taskbuffer": "^8.0.2",
|
||||||
"@serve.zone/catalog": "^2.10.0",
|
"@serve.zone/catalog": "^2.11.0",
|
||||||
"@serve.zone/interfaces": "^5.3.0",
|
"@serve.zone/interfaces": "^5.3.0",
|
||||||
"@serve.zone/remoteingress": "^4.15.3",
|
"@serve.zone/remoteingress": "^4.15.3",
|
||||||
"@tsclass/tsclass": "^9.5.0",
|
"@tsclass/tsclass": "^9.5.0",
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -102,8 +102,8 @@ importers:
|
|||||||
specifier: ^8.0.2
|
specifier: ^8.0.2
|
||||||
version: 8.0.2
|
version: 8.0.2
|
||||||
'@serve.zone/catalog':
|
'@serve.zone/catalog':
|
||||||
specifier: ^2.10.0
|
specifier: ^2.11.0
|
||||||
version: 2.10.0(@tiptap/pm@2.27.2)
|
version: 2.11.0(@tiptap/pm@2.27.2)
|
||||||
'@serve.zone/interfaces':
|
'@serve.zone/interfaces':
|
||||||
specifier: ^5.3.0
|
specifier: ^5.3.0
|
||||||
version: 5.3.0
|
version: 5.3.0
|
||||||
@@ -1583,8 +1583,8 @@ packages:
|
|||||||
'@selderee/plugin-htmlparser2@0.11.0':
|
'@selderee/plugin-htmlparser2@0.11.0':
|
||||||
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
||||||
|
|
||||||
'@serve.zone/catalog@2.10.0':
|
'@serve.zone/catalog@2.11.0':
|
||||||
resolution: {integrity: sha512-/y3gDrf3UHXaDhLJtqJTeHSXOCKGQ4ou6Dd80tMxQYm8/I/OJmifkgerLKP05WdbMyj0pLp33QhjLElJrpME8Q==}
|
resolution: {integrity: sha512-4DFDewp1PFRhw5P+yQAoAw+i6gG2lfR3h+uPgbNxB5jCfW14eNDXi3nuwTMBQWRHL9jv8o0BokASjV9A0+q66g==}
|
||||||
|
|
||||||
'@serve.zone/interfaces@5.3.0':
|
'@serve.zone/interfaces@5.3.0':
|
||||||
resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==}
|
resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==}
|
||||||
@@ -6904,7 +6904,7 @@ snapshots:
|
|||||||
domhandler: 5.0.3
|
domhandler: 5.0.3
|
||||||
selderee: 0.11.0
|
selderee: 0.11.0
|
||||||
|
|
||||||
'@serve.zone/catalog@2.10.0(@tiptap/pm@2.27.2)':
|
'@serve.zone/catalog@2.11.0(@tiptap/pm@2.27.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@design.estate/dees-catalog': 3.50.2(@tiptap/pm@2.27.2)
|
'@design.estate/dees-catalog': 3.50.2(@tiptap/pm@2.27.2)
|
||||||
'@design.estate/dees-domtools': 2.5.4
|
'@design.estate/dees-domtools': 2.5.4
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '12.3.0',
|
version: '12.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: '12.3.0',
|
version: '12.4.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1441,6 +1441,37 @@ export const createRouteAction = routeManagementStatePart.createAction<{
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const updateRouteAction = routeManagementStatePart.createAction<{
|
||||||
|
id: string;
|
||||||
|
route?: any;
|
||||||
|
enabled?: boolean;
|
||||||
|
metadata?: any;
|
||||||
|
}>(async (statePartArg, dataArg, actionContext): Promise<IRouteManagementState> => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState()!;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_UpdateRoute
|
||||||
|
>('/typedrequest', 'updateRoute');
|
||||||
|
|
||||||
|
await request.fire({
|
||||||
|
identity: context.identity!,
|
||||||
|
id: dataArg.id,
|
||||||
|
route: dataArg.route,
|
||||||
|
enabled: dataArg.enabled,
|
||||||
|
metadata: dataArg.metadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await actionContext!.dispatch(fetchMergedRoutesAction, null);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to update route',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const deleteRouteAction = routeManagementStatePart.createAction<string>(
|
export const deleteRouteAction = routeManagementStatePart.createAction<string>(
|
||||||
async (statePartArg, routeId, actionContext): Promise<IRouteManagementState> => {
|
async (statePartArg, routeId, actionContext): Promise<IRouteManagementState> => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
|
|||||||
@@ -287,27 +287,17 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
{
|
{
|
||||||
name: 'Inbound',
|
name: 'Inbound',
|
||||||
data: this.trafficDataIn,
|
data: this.trafficDataIn,
|
||||||
color: '#22c55e', // Green for download
|
color: '#22c55e',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Outbound',
|
name: 'Outbound',
|
||||||
data: this.trafficDataOut,
|
data: this.trafficDataOut,
|
||||||
color: '#8b5cf6', // Purple for upload
|
color: '#8b5cf6',
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
.stacked=${false}
|
.realtimeMode=${true}
|
||||||
|
.rollingWindow=${300000}
|
||||||
.yAxisFormatter=${(val: number) => `${val} Mbit/s`}
|
.yAxisFormatter=${(val: number) => `${val} Mbit/s`}
|
||||||
.tooltipFormatter=${(point: any) => {
|
|
||||||
const mbps = point.y || 0;
|
|
||||||
const seriesName = point.series?.name || 'Throughput';
|
|
||||||
const timestamp = new Date(point.x).toLocaleTimeString();
|
|
||||||
return `
|
|
||||||
<div style="padding: 8px;">
|
|
||||||
<div style="font-weight: bold; margin-bottom: 4px;">${timestamp}</div>
|
|
||||||
<div>${seriesName}: ${mbps.toFixed(2)} Mbit/s</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}}
|
|
||||||
></dees-chart-area>
|
></dees-chart-area>
|
||||||
|
|
||||||
<!-- Top IPs Section -->
|
<!-- Top IPs Section -->
|
||||||
|
|||||||
@@ -121,11 +121,15 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
<dees-chart-area
|
<dees-chart-area
|
||||||
.label=${'Email Traffic (24h)'}
|
.label=${'Email Traffic (24h)'}
|
||||||
.series=${this.getEmailTrafficSeries()}
|
.series=${this.getEmailTrafficSeries()}
|
||||||
|
.realtimeMode=${true}
|
||||||
|
.rollingWindow=${86400000}
|
||||||
.yAxisFormatter=${(val: number) => `${val}`}
|
.yAxisFormatter=${(val: number) => `${val}`}
|
||||||
></dees-chart-area>
|
></dees-chart-area>
|
||||||
<dees-chart-area
|
<dees-chart-area
|
||||||
.label=${'DNS Queries (24h)'}
|
.label=${'DNS Queries (24h)'}
|
||||||
.series=${this.getDnsQuerySeries()}
|
.series=${this.getDnsQuerySeries()}
|
||||||
|
.realtimeMode=${true}
|
||||||
|
.rollingWindow=${86400000}
|
||||||
.yAxisFormatter=${(val: number) => `${val}`}
|
.yAxisFormatter=${(val: number) => `${val}`}
|
||||||
></dees-chart-area>
|
></dees-chart-area>
|
||||||
<dees-chart-log
|
<dees-chart-log
|
||||||
|
|||||||
@@ -204,7 +204,10 @@ export class OpsViewRoutes extends DeesElement {
|
|||||||
? html`
|
? html`
|
||||||
<sz-route-list-view
|
<sz-route-list-view
|
||||||
.routes=${szRoutes}
|
.routes=${szRoutes}
|
||||||
|
.showActionsFilter=${(route: any) => route.tags?.includes('programmatic') ?? false}
|
||||||
@route-click=${(e: CustomEvent) => this.handleRouteClick(e)}
|
@route-click=${(e: CustomEvent) => this.handleRouteClick(e)}
|
||||||
|
@route-edit=${(e: CustomEvent) => this.handleRouteEdit(e)}
|
||||||
|
@route-delete=${(e: CustomEvent) => this.handleRouteDelete(e)}
|
||||||
></sz-route-list-view>
|
></sz-route-list-view>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
@@ -337,6 +340,162 @@ export class OpsViewRoutes extends DeesElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async handleRouteEdit(e: CustomEvent) {
|
||||||
|
const clickedRoute = e.detail;
|
||||||
|
if (!clickedRoute) return;
|
||||||
|
|
||||||
|
const merged = this.routeState.mergedRoutes.find(
|
||||||
|
(mr) => mr.route.name === clickedRoute.name,
|
||||||
|
);
|
||||||
|
if (!merged || !merged.storedRouteId) return;
|
||||||
|
|
||||||
|
this.showEditRouteDialog(merged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleRouteDelete(e: CustomEvent) {
|
||||||
|
const clickedRoute = e.detail;
|
||||||
|
if (!clickedRoute) return;
|
||||||
|
|
||||||
|
const merged = this.routeState.mergedRoutes.find(
|
||||||
|
(mr) => mr.route.name === clickedRoute.name,
|
||||||
|
);
|
||||||
|
if (!merged || !merged.storedRouteId) return;
|
||||||
|
|
||||||
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: `Delete Route: ${merged.route.name}`,
|
||||||
|
content: html`
|
||||||
|
<div style="color: #ccc; padding: 8px 0;">
|
||||||
|
<p>Are you sure you want to delete this route? This action cannot be undone.</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Cancel',
|
||||||
|
iconName: 'lucide:x',
|
||||||
|
action: async (modalArg: any) => await modalArg.destroy(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete',
|
||||||
|
iconName: 'lucide:trash-2',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(
|
||||||
|
appstate.deleteRouteAction,
|
||||||
|
merged.storedRouteId!,
|
||||||
|
);
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async showEditRouteDialog(merged: interfaces.data.IMergedRoute) {
|
||||||
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
|
const profiles = this.profilesTargetsState.profiles;
|
||||||
|
const targets = this.profilesTargetsState.targets;
|
||||||
|
|
||||||
|
const profileOptions = [
|
||||||
|
{ key: '', option: '(none — inline security)' },
|
||||||
|
...profiles.map((p) => ({
|
||||||
|
key: p.id,
|
||||||
|
option: `${p.name}${p.description ? ' — ' + p.description : ''}`,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
const targetOptions = [
|
||||||
|
{ key: '', option: '(none — inline target)' },
|
||||||
|
...targets.map((t) => ({
|
||||||
|
key: t.id,
|
||||||
|
option: `${t.name} (${Array.isArray(t.host) ? t.host.join(',') : t.host}:${t.port})`,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
const route = merged.route;
|
||||||
|
const currentPorts = Array.isArray(route.match.ports)
|
||||||
|
? route.match.ports.map((p: any) => typeof p === 'number' ? String(p) : `${p.from}-${p.to}`).join(', ')
|
||||||
|
: String(route.match.ports);
|
||||||
|
const currentDomains = route.match.domains
|
||||||
|
? (Array.isArray(route.match.domains) ? route.match.domains.join(', ') : route.match.domains)
|
||||||
|
: '';
|
||||||
|
const firstTarget = route.action.targets?.[0];
|
||||||
|
const currentTargetHost = firstTarget
|
||||||
|
? (Array.isArray(firstTarget.host) ? firstTarget.host[0] : firstTarget.host)
|
||||||
|
: '';
|
||||||
|
const currentTargetPort = firstTarget?.port != null ? String(firstTarget.port) : '';
|
||||||
|
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: `Edit Route: ${route.name}`,
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .key=${'name'} .label=${'Route Name'} .value=${route.name || ''} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'ports'} .label=${'Ports (comma-separated)'} .value=${currentPorts} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'domains'} .label=${'Domains (comma-separated, optional)'} .value=${currentDomains}></dees-input-text>
|
||||||
|
<dees-input-dropdown .key=${'securityProfileRef'} .label=${'Security Profile'} .options=${profileOptions} .selectedKey=${merged.metadata?.securityProfileRef || ''}></dees-input-dropdown>
|
||||||
|
<dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions} .selectedKey=${merged.metadata?.networkTargetRef || ''}></dees-input-dropdown>
|
||||||
|
<dees-input-text .key=${'targetHost'} .label=${'Target Host (if no target selected)'} .value=${currentTargetHost}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'targetPort'} .label=${'Target Port (if no target selected)'} .value=${currentTargetPort}></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Cancel',
|
||||||
|
iconName: 'lucide:x',
|
||||||
|
action: async (modalArg: any) => await modalArg.destroy(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Save',
|
||||||
|
iconName: 'lucide:check',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
|
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
||||||
|
if (!form) return;
|
||||||
|
const formData = await form.collectFormData();
|
||||||
|
if (!formData.name || !formData.ports) return;
|
||||||
|
|
||||||
|
const ports = formData.ports.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p));
|
||||||
|
const domains = formData.domains
|
||||||
|
? formData.domains.split(',').map((d: string) => d.trim()).filter(Boolean)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const updatedRoute: any = {
|
||||||
|
name: formData.name,
|
||||||
|
match: {
|
||||||
|
ports,
|
||||||
|
...(domains && domains.length > 0 ? { domains } : {}),
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
host: formData.targetHost || 'localhost',
|
||||||
|
port: parseInt(formData.targetPort, 10) || 443,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const metadata: any = {};
|
||||||
|
if (formData.securityProfileRef) {
|
||||||
|
metadata.securityProfileRef = formData.securityProfileRef;
|
||||||
|
}
|
||||||
|
if (formData.networkTargetRef) {
|
||||||
|
metadata.networkTargetRef = formData.networkTargetRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
await appstate.routeManagementStatePart.dispatchAction(
|
||||||
|
appstate.updateRouteAction,
|
||||||
|
{
|
||||||
|
id: merged.storedRouteId!,
|
||||||
|
route: updatedRoute,
|
||||||
|
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async showCreateRouteDialog() {
|
private async showCreateRouteDialog() {
|
||||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
const profiles = this.profilesTargetsState.profiles;
|
const profiles = this.profilesTargetsState.profiles;
|
||||||
|
|||||||
Reference in New Issue
Block a user