feat(routes): add route edit and delete actions to the ops routes view

This commit is contained in:
2026-04-02 22:37:49 +00:00
parent 6f4a5f19e7
commit 141f185fbf
9 changed files with 214 additions and 22 deletions

View File

@@ -287,27 +287,17 @@ export class OpsViewNetwork extends DeesElement {
{
name: 'Inbound',
data: this.trafficDataIn,
color: '#22c55e', // Green for download
color: '#22c55e',
},
{
name: 'Outbound',
data: this.trafficDataOut,
color: '#8b5cf6', // Purple for upload
color: '#8b5cf6',
}
]}
.stacked=${false}
.realtimeMode=${true}
.rollingWindow=${300000}
.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>
<!-- Top IPs Section -->

View File

@@ -121,11 +121,15 @@ export class OpsViewOverview extends DeesElement {
<dees-chart-area
.label=${'Email Traffic (24h)'}
.series=${this.getEmailTrafficSeries()}
.realtimeMode=${true}
.rollingWindow=${86400000}
.yAxisFormatter=${(val: number) => `${val}`}
></dees-chart-area>
<dees-chart-area
.label=${'DNS Queries (24h)'}
.series=${this.getDnsQuerySeries()}
.realtimeMode=${true}
.rollingWindow=${86400000}
.yAxisFormatter=${(val: number) => `${val}`}
></dees-chart-area>
<dees-chart-log

View File

@@ -204,7 +204,10 @@ export class OpsViewRoutes extends DeesElement {
? html`
<sz-route-list-view
.routes=${szRoutes}
.showActionsFilter=${(route: any) => route.tags?.includes('programmatic') ?? false}
@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>
`
: 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() {
const { DeesModal } = await import('@design.estate/dees-catalog');
const profiles = this.profilesTargetsState.profiles;