@serve.zone/dcrouter 16.1.1 → 16.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deno.json +1 -1
- package/dist_serve/bundle.js +11786 -7664
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/config/classes.route-config-manager.d.ts +24 -4
- package/dist_ts/config/classes.route-config-manager.js +195 -34
- package/dist_ts/config/helpers.http-redirects.js +21 -12
- package/dist_ts/config/helpers.special-forwards.d.ts +14 -0
- package/dist_ts/config/helpers.special-forwards.js +85 -0
- package/dist_ts/opsserver/handlers/gatewayclient.handler.d.ts +1 -0
- package/dist_ts/opsserver/handlers/gatewayclient.handler.js +56 -9
- package/dist_ts/opsserver/handlers/route-management.handler.js +36 -1
- package/dist_ts_interfaces/data/route-management.d.ts +19 -1
- package/dist_ts_interfaces/data/route-management.js +5 -1
- package/dist_ts_interfaces/requests/route-management.d.ts +50 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate/routes.d.ts +10 -0
- package/dist_ts_web/appstate/routes.js +67 -1
- package/dist_ts_web/elements/network/ops-view-redirects.d.ts +1 -0
- package/dist_ts_web/elements/network/ops-view-redirects.js +12 -4
- package/dist_ts_web/elements/network/ops-view-routes.d.ts +20 -1
- package/dist_ts_web/elements/network/ops-view-routes.js +182 -52
- package/dist_ts_web/elements/network/ops-view-special-forwards.d.ts +12 -0
- package/dist_ts_web/elements/network/ops-view-special-forwards.js +375 -0
- package/dist_ts_web/elements/ops-dashboard.js +1 -3
- package/dist_ts_web/router.js +10 -2
- package/package.json +5 -5
- package/readme.md +7 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/config/classes.route-config-manager.ts +271 -32
- package/ts/config/helpers.http-redirects.ts +24 -16
- package/ts/config/helpers.special-forwards.ts +108 -0
- package/ts/opsserver/handlers/gatewayclient.handler.ts +55 -8
- package/ts/opsserver/handlers/route-management.handler.ts +59 -0
- package/ts_apiclient/readme.md +2 -2
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate/routes.ts +84 -0
- package/ts_web/elements/network/ops-view-redirects.ts +5 -1
- package/ts_web/elements/network/ops-view-routes.ts +175 -31
- package/ts_web/elements/network/ops-view-special-forwards.ts +352 -0
- package/ts_web/elements/ops-dashboard.ts +0 -2
- package/ts_web/readme.md +9 -0
- package/ts_web/router.ts +10 -1
|
@@ -791,7 +791,14 @@ export class GatewayClientHandler {
|
|
|
791
791
|
const routeKey = metadata.externalKey?.startsWith(prefix)
|
|
792
792
|
? metadata.externalKey.slice(prefix.length)
|
|
793
793
|
: '';
|
|
794
|
-
|
|
794
|
+
const combinedOwnership = this.parseCombinedGatewayClientRouteKey(routeKey);
|
|
795
|
+
if (combinedOwnership) {
|
|
796
|
+
ownership.hostname = combinedOwnership.hostname;
|
|
797
|
+
ownership.routeRef = combinedOwnership.routeRef;
|
|
798
|
+
} else if (routeKey.startsWith('v2:host-route:')) {
|
|
799
|
+
// A malformed versioned key must not be exposed as a hostname.
|
|
800
|
+
ownership.hostname = this.getRouteHostnames(routeArg.route)[0];
|
|
801
|
+
} else if (routeKey.startsWith('route:')) {
|
|
795
802
|
ownership.routeRef = routeKey.slice('route:'.length);
|
|
796
803
|
} else if (routeKey) {
|
|
797
804
|
ownership.hostname = routeKey;
|
|
@@ -934,7 +941,7 @@ export class GatewayClientHandler {
|
|
|
934
941
|
if (!existingRoute) {
|
|
935
942
|
return { success: true, action: 'unchanged' };
|
|
936
943
|
}
|
|
937
|
-
const result = await manager.
|
|
944
|
+
const result = await manager.deleteManagedRoute(existingRoute.id);
|
|
938
945
|
return result.success
|
|
939
946
|
? { success: true, action: 'deleted', routeId: existingRoute.id }
|
|
940
947
|
: { success: false, message: result.message };
|
|
@@ -965,24 +972,35 @@ export class GatewayClientHandler {
|
|
|
965
972
|
const normalizedRoute = this.normalizeGatewayClientRoute(route, resolvedOwnership, externalKey);
|
|
966
973
|
|
|
967
974
|
if (existingRoute) {
|
|
968
|
-
const
|
|
969
|
-
|
|
970
|
-
const result = await manager.updateRoute(existingRoute.id, {
|
|
971
|
-
route: routePatch,
|
|
975
|
+
const result = await manager.updateManagedRoute(existingRoute.id, {
|
|
976
|
+
route: normalizedRoute,
|
|
972
977
|
enabled: enabled ?? true,
|
|
973
978
|
metadata,
|
|
979
|
+
}, {
|
|
980
|
+
replaceMetadata: true,
|
|
981
|
+
replaceRoute: true,
|
|
974
982
|
});
|
|
975
983
|
return result.success
|
|
976
984
|
? { success: true, action: 'updated', routeId: existingRoute.id }
|
|
977
985
|
: { success: false, message: result.message };
|
|
978
986
|
}
|
|
979
987
|
|
|
980
|
-
const routeId = await manager.
|
|
988
|
+
const routeId = await manager.createManagedRoute(normalizedRoute, auth.userId, enabled ?? true, metadata);
|
|
981
989
|
return { success: true, action: 'created', routeId };
|
|
982
990
|
}
|
|
983
991
|
|
|
984
992
|
private buildGatewayClientExternalKey(ownership: TResolvedGatewayClientOwnership): string {
|
|
985
|
-
const
|
|
993
|
+
const hostname = ownership.hostname?.trim().toLowerCase();
|
|
994
|
+
const routeRef = ownership.routeRef?.trim();
|
|
995
|
+
let routeKey: string;
|
|
996
|
+
if (hostname && routeRef) {
|
|
997
|
+
const payload = Buffer.from(JSON.stringify([hostname, routeRef]), 'utf8').toString('base64url');
|
|
998
|
+
routeKey = `v2:host-route:${payload}`;
|
|
999
|
+
} else if (hostname) {
|
|
1000
|
+
routeKey = hostname;
|
|
1001
|
+
} else {
|
|
1002
|
+
routeKey = `route:${routeRef}`;
|
|
1003
|
+
}
|
|
986
1004
|
return [
|
|
987
1005
|
ownership.gatewayClientType,
|
|
988
1006
|
ownership.gatewayClientId,
|
|
@@ -991,6 +1009,35 @@ export class GatewayClientHandler {
|
|
|
991
1009
|
].map((part) => part.trim()).join(':');
|
|
992
1010
|
}
|
|
993
1011
|
|
|
1012
|
+
private parseCombinedGatewayClientRouteKey(
|
|
1013
|
+
routeKeyArg: string,
|
|
1014
|
+
): { hostname: string; routeRef: string } | undefined {
|
|
1015
|
+
const prefix = 'v2:host-route:';
|
|
1016
|
+
if (!routeKeyArg.startsWith(prefix)) {
|
|
1017
|
+
return undefined;
|
|
1018
|
+
}
|
|
1019
|
+
const payload = routeKeyArg.slice(prefix.length);
|
|
1020
|
+
if (!payload || !/^[A-Za-z0-9_-]+$/.test(payload)) {
|
|
1021
|
+
return undefined;
|
|
1022
|
+
}
|
|
1023
|
+
try {
|
|
1024
|
+
const parsed = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
|
1025
|
+
if (
|
|
1026
|
+
!Array.isArray(parsed)
|
|
1027
|
+
|| parsed.length !== 2
|
|
1028
|
+
|| parsed.some((value) => typeof value !== 'string' || !value.trim())
|
|
1029
|
+
) {
|
|
1030
|
+
return undefined;
|
|
1031
|
+
}
|
|
1032
|
+
return {
|
|
1033
|
+
hostname: parsed[0].trim().toLowerCase(),
|
|
1034
|
+
routeRef: parsed[1].trim(),
|
|
1035
|
+
};
|
|
1036
|
+
} catch {
|
|
1037
|
+
return undefined;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
994
1041
|
|
|
995
1042
|
private normalizeGatewayClientRoute(
|
|
996
1043
|
route: interfaces.data.IDcRouterRouteConfig,
|
|
@@ -122,5 +122,64 @@ export class RouteManagementHandler {
|
|
|
122
122
|
},
|
|
123
123
|
),
|
|
124
124
|
);
|
|
125
|
+
|
|
126
|
+
this.typedrouter.addTypedHandler(
|
|
127
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateLetsEncryptHttp01Forward>(
|
|
128
|
+
'createLetsEncryptHttp01Forward',
|
|
129
|
+
async (dataArg) => {
|
|
130
|
+
const userId = await this.requireAuth(dataArg, 'routes:write');
|
|
131
|
+
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
|
132
|
+
if (!manager) {
|
|
133
|
+
return { success: false, message: 'Route management not initialized' };
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const routeId = await manager.createLetsEncryptHttp01Forward(
|
|
137
|
+
dataArg.config,
|
|
138
|
+
userId,
|
|
139
|
+
dataArg.enabled ?? true,
|
|
140
|
+
);
|
|
141
|
+
return { success: true, routeId };
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return { success: false, message: (error as Error).message };
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
this.typedrouter.addTypedHandler(
|
|
150
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateLetsEncryptHttp01Forward>(
|
|
151
|
+
'updateLetsEncryptHttp01Forward',
|
|
152
|
+
async (dataArg) => {
|
|
153
|
+
await this.requireAuth(dataArg, 'routes:write');
|
|
154
|
+
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
|
155
|
+
if (!manager) {
|
|
156
|
+
return { success: false, message: 'Route management not initialized' };
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
return await manager.updateLetsEncryptHttp01Forward(
|
|
160
|
+
dataArg.id,
|
|
161
|
+
dataArg.config,
|
|
162
|
+
dataArg.enabled,
|
|
163
|
+
);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
return { success: false, message: (error as Error).message };
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
this.typedrouter.addTypedHandler(
|
|
172
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteLetsEncryptHttp01Forward>(
|
|
173
|
+
'deleteLetsEncryptHttp01Forward',
|
|
174
|
+
async (dataArg) => {
|
|
175
|
+
await this.requireAuth(dataArg, 'routes:write');
|
|
176
|
+
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
|
177
|
+
if (!manager) {
|
|
178
|
+
return { success: false, message: 'Route management not initialized' };
|
|
179
|
+
}
|
|
180
|
+
return await manager.deleteLetsEncryptHttp01Forward(dataArg.id);
|
|
181
|
+
},
|
|
182
|
+
),
|
|
183
|
+
);
|
|
125
184
|
}
|
|
126
185
|
}
|
package/ts_apiclient/readme.md
CHANGED
|
@@ -63,7 +63,7 @@ const tokenClient = new DcRouterApiClient({
|
|
|
63
63
|
|
|
64
64
|
| Manager | Purpose |
|
|
65
65
|
| --- | --- |
|
|
66
|
-
| `client.routes` | List merged routes, build
|
|
66
|
+
| `client.routes` | List merged routes, build/update/delete ordinary operator routes, and toggle routes. |
|
|
67
67
|
| `client.certificates` | Inspect certificate summaries and trigger certificate operations. |
|
|
68
68
|
| `client.apiTokens` | Create, list, toggle, roll, and revoke API tokens. |
|
|
69
69
|
| `client.remoteIngress` | Manage edge registrations, statuses, ports, tags, and connection tokens. |
|
|
@@ -98,7 +98,7 @@ await route.update({
|
|
|
98
98
|
});
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
System routes from `config`, `email`, and `dns` origins are designed to be toggled, not edited.
|
|
101
|
+
System routes from `config`, `email`, and `dns` origins are designed to be toggled, not edited. Generic create/update/delete behavior is for ordinary operator-owned routes with origin `api`. API-origin routes carrying managed ownership metadata, including gateway-client routes and HTTP-01 Special Forwards, must be changed through their owning specialized TypedRequest workflow; `client.routes` does not bypass that protection.
|
|
102
102
|
|
|
103
103
|
## API Tokens and Remote Ingress
|
|
104
104
|
|
|
@@ -221,6 +221,90 @@ export const toggleRouteAction = routeManagementStatePart.createAction<{
|
|
|
221
221
|
}
|
|
222
222
|
});
|
|
223
223
|
|
|
224
|
+
export const createLetsEncryptHttp01ForwardAction = routeManagementStatePart.createAction<{
|
|
225
|
+
config: interfaces.data.ILetsEncryptHttp01ForwardConfig;
|
|
226
|
+
enabled?: boolean;
|
|
227
|
+
}>(async (statePartArg, dataArg, actionContext): Promise<IRouteManagementState> => {
|
|
228
|
+
const context = getActionContext();
|
|
229
|
+
const currentState = statePartArg.getState()!;
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
233
|
+
interfaces.requests.IReq_CreateLetsEncryptHttp01Forward
|
|
234
|
+
>('/typedrequest', 'createLetsEncryptHttp01Forward');
|
|
235
|
+
const response = await request.fire({
|
|
236
|
+
identity: context.identity!,
|
|
237
|
+
config: dataArg.config,
|
|
238
|
+
enabled: dataArg.enabled,
|
|
239
|
+
});
|
|
240
|
+
if (!response.success) {
|
|
241
|
+
throw new Error(response.message || 'Failed to create Special Forward');
|
|
242
|
+
}
|
|
243
|
+
return await actionContext!.dispatch(fetchMergedRoutesAction, null);
|
|
244
|
+
} catch (error: unknown) {
|
|
245
|
+
return {
|
|
246
|
+
...currentState,
|
|
247
|
+
error: error instanceof Error ? error.message : 'Failed to create Special Forward',
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
export const updateLetsEncryptHttp01ForwardAction = routeManagementStatePart.createAction<{
|
|
253
|
+
id: string;
|
|
254
|
+
config: interfaces.data.ILetsEncryptHttp01ForwardConfig;
|
|
255
|
+
enabled?: boolean;
|
|
256
|
+
}>(async (statePartArg, dataArg, actionContext): Promise<IRouteManagementState> => {
|
|
257
|
+
const context = getActionContext();
|
|
258
|
+
const currentState = statePartArg.getState()!;
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
262
|
+
interfaces.requests.IReq_UpdateLetsEncryptHttp01Forward
|
|
263
|
+
>('/typedrequest', 'updateLetsEncryptHttp01Forward');
|
|
264
|
+
const response = await request.fire({
|
|
265
|
+
identity: context.identity!,
|
|
266
|
+
id: dataArg.id,
|
|
267
|
+
config: dataArg.config,
|
|
268
|
+
enabled: dataArg.enabled,
|
|
269
|
+
});
|
|
270
|
+
if (!response.success) {
|
|
271
|
+
throw new Error(response.message || 'Failed to update Special Forward');
|
|
272
|
+
}
|
|
273
|
+
return await actionContext!.dispatch(fetchMergedRoutesAction, null);
|
|
274
|
+
} catch (error: unknown) {
|
|
275
|
+
return {
|
|
276
|
+
...currentState,
|
|
277
|
+
error: error instanceof Error ? error.message : 'Failed to update Special Forward',
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
export const deleteLetsEncryptHttp01ForwardAction = routeManagementStatePart.createAction<string>(
|
|
283
|
+
async (statePartArg, routeId, actionContext): Promise<IRouteManagementState> => {
|
|
284
|
+
const context = getActionContext();
|
|
285
|
+
const currentState = statePartArg.getState()!;
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
289
|
+
interfaces.requests.IReq_DeleteLetsEncryptHttp01Forward
|
|
290
|
+
>('/typedrequest', 'deleteLetsEncryptHttp01Forward');
|
|
291
|
+
const response = await request.fire({
|
|
292
|
+
identity: context.identity!,
|
|
293
|
+
id: routeId,
|
|
294
|
+
});
|
|
295
|
+
if (!response.success) {
|
|
296
|
+
throw new Error(response.message || 'Failed to delete Special Forward');
|
|
297
|
+
}
|
|
298
|
+
return await actionContext!.dispatch(fetchMergedRoutesAction, null);
|
|
299
|
+
} catch (error: unknown) {
|
|
300
|
+
return {
|
|
301
|
+
...currentState,
|
|
302
|
+
error: error instanceof Error ? error.message : 'Failed to delete Special Forward',
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
);
|
|
307
|
+
|
|
224
308
|
// ============================================================================
|
|
225
309
|
// API Token Actions
|
|
226
310
|
// ============================================================================
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type TemplateResult,
|
|
6
6
|
css,
|
|
7
7
|
state,
|
|
8
|
+
property,
|
|
8
9
|
cssManager,
|
|
9
10
|
} from '@design.estate/dees-element';
|
|
10
11
|
import { type IStatsTile } from '@design.estate/dees-catalog';
|
|
@@ -20,6 +21,9 @@ declare global {
|
|
|
20
21
|
|
|
21
22
|
@customElement('ops-view-redirects')
|
|
22
23
|
export class OpsViewRedirects extends DeesElement {
|
|
24
|
+
@property({ type: Boolean })
|
|
25
|
+
accessor embedded = false;
|
|
26
|
+
|
|
23
27
|
@state()
|
|
24
28
|
accessor routeState: appstate.IRouteManagementState = appstate.routeManagementStatePart.getState()!;
|
|
25
29
|
|
|
@@ -123,7 +127,7 @@ export class OpsViewRedirects extends DeesElement {
|
|
|
123
127
|
];
|
|
124
128
|
|
|
125
129
|
return html`
|
|
126
|
-
|
|
130
|
+
${this.embedded ? '' : html`<dees-heading level="3">Redirects</dees-heading>`}
|
|
127
131
|
<div class="redirectsContainer">
|
|
128
132
|
<dees-statsgrid .tiles=${statsTiles}></dees-statsgrid>
|
|
129
133
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as appstate from '../../appstate.js';
|
|
2
2
|
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
|
3
3
|
import { viewHostCss } from '../shared/css.js';
|
|
4
|
-
import { type IStatsTile } from '@design.estate/dees-catalog';
|
|
4
|
+
import { type DeesAppuiTabs, type IStatsTile } from '@design.estate/dees-catalog';
|
|
5
|
+
import './ops-view-redirects.js';
|
|
6
|
+
import './ops-view-special-forwards.js';
|
|
5
7
|
import type {
|
|
6
8
|
IRoutePathClassOption as ISzRoutePathClassOption,
|
|
7
9
|
IRouteSourcePolicyPreset as ISzRouteSourcePolicyPreset,
|
|
@@ -19,6 +21,10 @@ import {
|
|
|
19
21
|
type TemplateResult,
|
|
20
22
|
} from '@design.estate/dees-element';
|
|
21
23
|
|
|
24
|
+
type TRoutesSection = 'standard' | 'special' | 'redirects';
|
|
25
|
+
type TRouteOwnerFilter = 'all' | 'operator' | 'gatewayClient' | 'system';
|
|
26
|
+
type IMenuItem = DeesAppuiTabs['tabs'][number];
|
|
27
|
+
|
|
22
28
|
// TLS dropdown options shared by create and edit dialogs
|
|
23
29
|
const tlsModeOptions = [
|
|
24
30
|
{ key: 'none', option: '(none — plain TCP/HTTP, use for SSH)' },
|
|
@@ -318,7 +324,42 @@ function setupTlsVisibility(formEl: any) {
|
|
|
318
324
|
|
|
319
325
|
@customElement('ops-view-routes')
|
|
320
326
|
export class OpsViewRoutes extends DeesElement {
|
|
321
|
-
@state() accessor
|
|
327
|
+
@state() accessor activeSection: TRoutesSection = 'standard';
|
|
328
|
+
@state() accessor ownerFilter: TRouteOwnerFilter = 'all';
|
|
329
|
+
|
|
330
|
+
private readonly sectionTabs: IMenuItem[] = [
|
|
331
|
+
{
|
|
332
|
+
key: 'Standard Routes',
|
|
333
|
+
iconName: 'lucide:route',
|
|
334
|
+
action: () => this.setRouteView('standard'),
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
key: 'Special Forwards',
|
|
338
|
+
iconName: 'lucide:split',
|
|
339
|
+
action: () => this.setRouteView('special'),
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
key: 'Redirects',
|
|
343
|
+
iconName: 'lucide:cornerDownRight',
|
|
344
|
+
action: () => this.setRouteView('redirects'),
|
|
345
|
+
},
|
|
346
|
+
];
|
|
347
|
+
|
|
348
|
+
private readonly ownerTabs: IMenuItem[] = [
|
|
349
|
+
{ key: 'All', action: () => this.setRouteOwnerFilter('all') },
|
|
350
|
+
{ key: 'Operator', action: () => this.setRouteOwnerFilter('operator') },
|
|
351
|
+
{
|
|
352
|
+
key: 'Gateway Client',
|
|
353
|
+
action: () => this.setRouteOwnerFilter('gatewayClient'),
|
|
354
|
+
},
|
|
355
|
+
{ key: 'System', action: () => this.setRouteOwnerFilter('system') },
|
|
356
|
+
];
|
|
357
|
+
|
|
358
|
+
private readonly popStateHandler = () => {
|
|
359
|
+
const urlState = this.readUrlState();
|
|
360
|
+
this.activeSection = urlState.section;
|
|
361
|
+
this.ownerFilter = urlState.owner;
|
|
362
|
+
};
|
|
322
363
|
|
|
323
364
|
@state() accessor routeState: appstate.IRouteManagementState = {
|
|
324
365
|
mergedRoutes: [],
|
|
@@ -341,6 +382,10 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
341
382
|
|
|
342
383
|
constructor() {
|
|
343
384
|
super();
|
|
385
|
+
const urlState = this.readUrlState();
|
|
386
|
+
this.activeSection = urlState.section;
|
|
387
|
+
this.ownerFilter = urlState.owner;
|
|
388
|
+
this.writeUrlState(urlState.section, urlState.owner, 'replace');
|
|
344
389
|
const sub = appstate.routeManagementStatePart
|
|
345
390
|
.select((s) => s)
|
|
346
391
|
.subscribe((routeState) => {
|
|
@@ -368,6 +413,16 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
368
413
|
this.rxSubscriptions.push(loginSub);
|
|
369
414
|
}
|
|
370
415
|
|
|
416
|
+
async connectedCallback() {
|
|
417
|
+
await super.connectedCallback();
|
|
418
|
+
window.addEventListener('popstate', this.popStateHandler);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async disconnectedCallback() {
|
|
422
|
+
window.removeEventListener('popstate', this.popStateHandler);
|
|
423
|
+
await super.disconnectedCallback();
|
|
424
|
+
}
|
|
425
|
+
|
|
371
426
|
public static styles = [
|
|
372
427
|
cssManager.defaultStyles,
|
|
373
428
|
viewHostCss,
|
|
@@ -415,7 +470,11 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
415
470
|
|
|
416
471
|
const disabledCount = mergedRoutes.filter((mr) => !mr.enabled).length;
|
|
417
472
|
const configCount = mergedRoutes.filter((mr) => mr.origin !== 'api').length;
|
|
418
|
-
const
|
|
473
|
+
const operatorCount = mergedRoutes.filter((mr) => {
|
|
474
|
+
return mr.origin === 'api'
|
|
475
|
+
&& mr.metadata?.ownerType !== 'gatewayClient'
|
|
476
|
+
&& !mr.metadata?.managedRouteKind;
|
|
477
|
+
}).length;
|
|
419
478
|
|
|
420
479
|
const statsTiles: IStatsTile[] = [
|
|
421
480
|
{
|
|
@@ -437,12 +496,12 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
437
496
|
color: '#8b5cf6',
|
|
438
497
|
},
|
|
439
498
|
{
|
|
440
|
-
id: '
|
|
441
|
-
title: '
|
|
499
|
+
id: 'operatorRoutes',
|
|
500
|
+
title: 'Operator Routes',
|
|
442
501
|
type: 'number',
|
|
443
|
-
value:
|
|
502
|
+
value: operatorCount,
|
|
444
503
|
icon: 'lucide:code',
|
|
445
|
-
description: 'Created
|
|
504
|
+
description: 'Created directly by operators',
|
|
446
505
|
color: '#0ea5e9',
|
|
447
506
|
},
|
|
448
507
|
{
|
|
@@ -456,11 +515,19 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
456
515
|
},
|
|
457
516
|
];
|
|
458
517
|
|
|
459
|
-
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
518
|
+
const standardRoutes = mergedRoutes.filter((route) => !route.metadata?.managedRouteKind);
|
|
519
|
+
const filteredRoutes = standardRoutes.filter((route) => {
|
|
520
|
+
switch (this.ownerFilter) {
|
|
521
|
+
case 'operator':
|
|
522
|
+
return route.origin === 'api' && route.metadata?.ownerType !== 'gatewayClient';
|
|
523
|
+
case 'gatewayClient':
|
|
524
|
+
return route.metadata?.ownerType === 'gatewayClient';
|
|
525
|
+
case 'system':
|
|
526
|
+
return route.origin !== 'api' || route.metadata?.ownerType === 'system';
|
|
527
|
+
default:
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
});
|
|
464
531
|
|
|
465
532
|
// Map filtered routes to sz-route-list-view format
|
|
466
533
|
const szRoutes = filteredRoutes.map((mr) => {
|
|
@@ -482,6 +549,16 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
482
549
|
<dees-heading level="3">Route Management</dees-heading>
|
|
483
550
|
|
|
484
551
|
<div class="routesContainer">
|
|
552
|
+
<dees-appui-tabs
|
|
553
|
+
.tabs=${this.sectionTabs}
|
|
554
|
+
.selectedTab=${this.getSelectedSectionTab()}
|
|
555
|
+
></dees-appui-tabs>
|
|
556
|
+
|
|
557
|
+
${this.activeSection === 'special'
|
|
558
|
+
? html`<ops-view-special-forwards></ops-view-special-forwards>`
|
|
559
|
+
: this.activeSection === 'redirects'
|
|
560
|
+
? html`<ops-view-redirects .embedded=${true}></ops-view-redirects>`
|
|
561
|
+
: html`
|
|
485
562
|
<dees-statsgrid
|
|
486
563
|
.tiles=${statsTiles}
|
|
487
564
|
.gridActions=${[
|
|
@@ -498,12 +575,10 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
498
575
|
]}
|
|
499
576
|
></dees-statsgrid>
|
|
500
577
|
|
|
501
|
-
<dees-
|
|
502
|
-
|
|
503
|
-
.
|
|
504
|
-
|
|
505
|
-
.selectedOption=${this.routeFilter}
|
|
506
|
-
></dees-input-multitoggle>
|
|
578
|
+
<dees-appui-tabs
|
|
579
|
+
.tabs=${this.ownerTabs}
|
|
580
|
+
.selectedTab=${this.getSelectedOwnerTab()}
|
|
581
|
+
></dees-appui-tabs>
|
|
507
582
|
|
|
508
583
|
${warnings.length > 0
|
|
509
584
|
? html`
|
|
@@ -524,7 +599,7 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
524
599
|
? html`
|
|
525
600
|
<sz-route-list-view
|
|
526
601
|
.routes=${szRoutes}
|
|
527
|
-
.showActionsFilter=${
|
|
602
|
+
.showActionsFilter=${(route: { id?: string; name?: string }) => this.canEditRoute(route)}
|
|
528
603
|
@route-click=${(e: CustomEvent) => this.handleRouteClick(e)}
|
|
529
604
|
@route-edit=${(e: CustomEvent) => this.handleRouteEdit(e)}
|
|
530
605
|
@route-delete=${(e: CustomEvent) => this.handleRouteDelete(e)}
|
|
@@ -532,10 +607,11 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
532
607
|
`
|
|
533
608
|
: html`
|
|
534
609
|
<div class="empty-state">
|
|
535
|
-
<p>No ${
|
|
536
|
-
<p
|
|
610
|
+
<p>No ${this.getOwnerFilterLabel().toLowerCase()} routes</p>
|
|
611
|
+
<p>Choose another ownership filter or add an operator route.</p>
|
|
537
612
|
</div>
|
|
538
613
|
`}
|
|
614
|
+
`}
|
|
539
615
|
</div>
|
|
540
616
|
`;
|
|
541
617
|
}
|
|
@@ -560,7 +636,7 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
560
636
|
<p>Status: <strong>${merged.enabled ? 'Enabled' : 'Disabled'}</strong></p>
|
|
561
637
|
${merged.route.vpnOnly ? html`<p>Access: <strong style="color: #22c55e;">VPN only</strong></p>` : ''}
|
|
562
638
|
<p>ID: <code style="color: #888;">${merged.id}</code></p>
|
|
563
|
-
${isSystemManaged ? html`<p>This route is
|
|
639
|
+
${isSystemManaged ? html`<p>This route is managed by the system, a gateway client, or a specialized workflow.</p>` : ''}
|
|
564
640
|
${sourceBindingSummary ? html`<p>Source Bindings: <strong style="color: #a78bfa;">${sourceBindingSummary}</strong></p>` : ''}
|
|
565
641
|
${meta?.networkTargetName ? html`<p>Network Target: <strong style="color: #a78bfa;">${meta.networkTargetName}</strong></p>` : ''}
|
|
566
642
|
</div>
|
|
@@ -1031,6 +1107,80 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
1031
1107
|
appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
|
|
1032
1108
|
}
|
|
1033
1109
|
|
|
1110
|
+
private getSelectedSectionTab(): IMenuItem {
|
|
1111
|
+
const index = this.activeSection === 'special'
|
|
1112
|
+
? 1
|
|
1113
|
+
: this.activeSection === 'redirects'
|
|
1114
|
+
? 2
|
|
1115
|
+
: 0;
|
|
1116
|
+
return this.sectionTabs[index];
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
private getSelectedOwnerTab(): IMenuItem {
|
|
1120
|
+
const index = this.ownerFilter === 'operator'
|
|
1121
|
+
? 1
|
|
1122
|
+
: this.ownerFilter === 'gatewayClient'
|
|
1123
|
+
? 2
|
|
1124
|
+
: this.ownerFilter === 'system'
|
|
1125
|
+
? 3
|
|
1126
|
+
: 0;
|
|
1127
|
+
return this.ownerTabs[index];
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
private getOwnerFilterLabel(): string {
|
|
1131
|
+
return this.getSelectedOwnerTab().key;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
private setRouteView(section: TRoutesSection): void {
|
|
1135
|
+
if (this.activeSection === section) return;
|
|
1136
|
+
this.activeSection = section;
|
|
1137
|
+
this.writeUrlState(section, this.ownerFilter, 'push');
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
private setRouteOwnerFilter(owner: TRouteOwnerFilter): void {
|
|
1141
|
+
if (this.ownerFilter === owner) return;
|
|
1142
|
+
this.ownerFilter = owner;
|
|
1143
|
+
this.writeUrlState(this.activeSection, owner, 'push');
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
private readUrlState(): { section: TRoutesSection; owner: TRouteOwnerFilter } {
|
|
1147
|
+
const params = new URL(window.location.href).searchParams;
|
|
1148
|
+
const tabParam = params.get('tab');
|
|
1149
|
+
const ownerParam = params.get('owner');
|
|
1150
|
+
const section: TRoutesSection = tabParam === 'special' || tabParam === 'redirects'
|
|
1151
|
+
? tabParam
|
|
1152
|
+
: 'standard';
|
|
1153
|
+
const owner: TRouteOwnerFilter = (
|
|
1154
|
+
ownerParam === 'operator'
|
|
1155
|
+
|| ownerParam === 'gatewayClient'
|
|
1156
|
+
|| ownerParam === 'system'
|
|
1157
|
+
)
|
|
1158
|
+
? ownerParam
|
|
1159
|
+
: 'all';
|
|
1160
|
+
return { section, owner };
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
private writeUrlState(
|
|
1164
|
+
section: TRoutesSection,
|
|
1165
|
+
owner: TRouteOwnerFilter,
|
|
1166
|
+
mode: 'push' | 'replace',
|
|
1167
|
+
): void {
|
|
1168
|
+
if (window.location.pathname !== '/network/routes') return;
|
|
1169
|
+
const url = new URL(window.location.href);
|
|
1170
|
+
url.searchParams.set('tab', section);
|
|
1171
|
+
url.searchParams.set('owner', owner);
|
|
1172
|
+
window.history[mode === 'push' ? 'pushState' : 'replaceState'](
|
|
1173
|
+
window.history.state,
|
|
1174
|
+
'',
|
|
1175
|
+
url,
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
private canEditRoute(clickedRoute: { id?: string; name?: string }): boolean {
|
|
1180
|
+
const merged = this.findMergedRoute(clickedRoute);
|
|
1181
|
+
return Boolean(merged && !this.isSystemManagedRoute(merged));
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1034
1184
|
private getSourceBindingRefs(metadata?: interfaces.data.IRouteMetadata): string[] {
|
|
1035
1185
|
const bindingRefs = metadata?.sourceBindings
|
|
1036
1186
|
?.map((binding) => binding.sourceProfileRef)
|
|
@@ -1064,18 +1214,12 @@ export class OpsViewRoutes extends DeesElement {
|
|
|
1064
1214
|
}
|
|
1065
1215
|
|
|
1066
1216
|
private isSystemManagedRoute(merged: interfaces.data.IMergedRoute): boolean {
|
|
1067
|
-
return merged.origin !== 'api'
|
|
1217
|
+
return merged.origin !== 'api'
|
|
1218
|
+
|| merged.metadata?.ownerType === 'gatewayClient'
|
|
1219
|
+
|| Boolean(merged.metadata?.managedRouteKind);
|
|
1068
1220
|
}
|
|
1069
1221
|
|
|
1070
1222
|
async firstUpdated() {
|
|
1071
1223
|
await appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
|
|
1072
|
-
|
|
1073
|
-
const toggle = this.shadowRoot!.querySelector('.routeFilterToggle') as any;
|
|
1074
|
-
if (toggle) {
|
|
1075
|
-
const sub = toggle.changeSubject.subscribe(() => {
|
|
1076
|
-
this.routeFilter = toggle.selectedOption;
|
|
1077
|
-
});
|
|
1078
|
-
this.rxSubscriptions.push(sub);
|
|
1079
|
-
}
|
|
1080
1224
|
}
|
|
1081
1225
|
}
|