@serve.zone/dcrouter 16.2.1 → 16.4.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.
Files changed (41) hide show
  1. package/deno.json +1 -1
  2. package/dist_serve/bundle.js +1190 -1100
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/config/helpers.http-redirects.d.ts +1 -1
  5. package/dist_ts/config/helpers.http-redirects.js +4 -3
  6. package/dist_ts_interfaces/data/route-management.d.ts +2 -0
  7. package/dist_ts_interfaces/data/route-management.js +6 -1
  8. package/dist_ts_web/00_commitinfo_data.js +1 -1
  9. package/dist_ts_web/elements/access/ops-view-apitokens.js +3 -3
  10. package/dist_ts_web/elements/access/ops-view-gatewayclients.js +3 -3
  11. package/dist_ts_web/elements/network/ops-view-network-activity.js +6 -6
  12. package/dist_ts_web/elements/network/ops-view-redirects.d.ts +3 -0
  13. package/dist_ts_web/elements/network/ops-view-redirects.js +38 -21
  14. package/dist_ts_web/elements/network/ops-view-remoteingress.js +3 -3
  15. package/dist_ts_web/elements/network/ops-view-routes.d.ts +8 -7
  16. package/dist_ts_web/elements/network/ops-view-routes.js +331 -161
  17. package/dist_ts_web/elements/network/ops-view-special-forwards.d.ts +4 -1
  18. package/dist_ts_web/elements/network/ops-view-special-forwards.js +118 -168
  19. package/dist_ts_web/elements/network/ops-view-vpn.js +3 -3
  20. package/dist_ts_web/elements/network/route-view-filters.d.ts +6 -0
  21. package/dist_ts_web/elements/network/route-view-filters.js +29 -0
  22. package/dist_ts_web/elements/network/special-forward-dialog.d.ts +11 -0
  23. package/dist_ts_web/elements/network/special-forward-dialog.js +154 -0
  24. package/dist_ts_web/elements/security/ops-view-security-blocked.js +6 -6
  25. package/package.json +2 -2
  26. package/readme.md +1 -1
  27. package/ts/00_commitinfo_data.ts +1 -1
  28. package/ts/config/helpers.http-redirects.ts +6 -3
  29. package/ts_web/00_commitinfo_data.ts +1 -1
  30. package/ts_web/elements/access/ops-view-apitokens.ts +2 -2
  31. package/ts_web/elements/access/ops-view-gatewayclients.ts +2 -2
  32. package/ts_web/elements/network/ops-view-network-activity.ts +5 -5
  33. package/ts_web/elements/network/ops-view-redirects.ts +34 -19
  34. package/ts_web/elements/network/ops-view-remoteingress.ts +2 -2
  35. package/ts_web/elements/network/ops-view-routes.ts +371 -171
  36. package/ts_web/elements/network/ops-view-special-forwards.ts +115 -178
  37. package/ts_web/elements/network/ops-view-vpn.ts +2 -2
  38. package/ts_web/elements/network/route-view-filters.ts +43 -0
  39. package/ts_web/elements/network/special-forward-dialog.ts +174 -0
  40. package/ts_web/elements/security/ops-view-security-blocked.ts +5 -5
  41. package/ts_web/readme.md +4 -4
@@ -1,9 +1,22 @@
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 DeesAppuiTabs, type IStatsTile } from '@design.estate/dees-catalog';
4
+ import {
5
+ type DeesInputMultitoggle,
6
+ type IStatsTile,
7
+ } from '@design.estate/dees-catalog';
5
8
  import './ops-view-redirects.js';
6
9
  import './ops-view-special-forwards.js';
10
+ import {
11
+ getRouteOwner,
12
+ redirectMatchesOwner,
13
+ routeMatchesOwner,
14
+ type TRouteOwnerFilter,
15
+ } from './route-view-filters.js';
16
+ import {
17
+ letsEncryptHttp01ManagedRouteKind,
18
+ showLetsEncryptHttp01ForwardDialog,
19
+ } from './special-forward-dialog.js';
7
20
  import type {
8
21
  IRoutePathClassOption as ISzRoutePathClassOption,
9
22
  IRouteSourcePolicyPreset as ISzRouteSourcePolicyPreset,
@@ -21,9 +34,10 @@ import {
21
34
  type TemplateResult,
22
35
  } from '@design.estate/dees-element';
23
36
 
24
- type TRoutesSection = 'standard' | 'special' | 'redirects';
25
- type TRouteOwnerFilter = 'all' | 'operator' | 'gatewayClient' | 'system';
26
- type IMenuItem = DeesAppuiTabs['tabs'][number];
37
+ type TRoutesSection = 'all' | 'standard' | 'redirects' | 'special';
38
+
39
+ const routeTypeOptions = ['All', 'Standard', 'Redirect', 'Special'];
40
+ const routeOwnerOptions = ['All', 'User', 'Gateway Client', 'System'];
27
41
 
28
42
  // TLS dropdown options shared by create and edit dialogs
29
43
  const tlsModeOptions = [
@@ -327,34 +341,6 @@ export class OpsViewRoutes extends DeesElement {
327
341
  @state() accessor activeSection: TRoutesSection = 'standard';
328
342
  @state() accessor ownerFilter: TRouteOwnerFilter = 'all';
329
343
 
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
344
  private readonly popStateHandler = () => {
359
345
  const urlState = this.readUrlState();
360
346
  this.activeSection = urlState.section;
@@ -406,8 +392,7 @@ export class OpsViewRoutes extends DeesElement {
406
392
  .select((s) => s.isLoggedIn)
407
393
  .subscribe((isLoggedIn) => {
408
394
  if (isLoggedIn) {
409
- appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
410
- appstate.profilesTargetsStatePart.dispatchAction(appstate.fetchProfilesAndTargetsAction, null);
395
+ void this.refreshData();
411
396
  }
412
397
  });
413
398
  this.rxSubscriptions.push(loginSub);
@@ -433,6 +418,31 @@ export class OpsViewRoutes extends DeesElement {
433
418
  gap: 24px;
434
419
  }
435
420
 
421
+ .routeFilters {
422
+ display: flex;
423
+ align-items: flex-end;
424
+ gap: 16px;
425
+ flex-wrap: nowrap;
426
+ }
427
+
428
+ .routeFilter {
429
+ flex: 0 1 auto;
430
+ min-width: 0;
431
+ max-width: 100%;
432
+ overflow-x: auto;
433
+ }
434
+
435
+ @media (max-width: 760px) {
436
+ .routeFilters {
437
+ flex-direction: column;
438
+ align-items: stretch;
439
+ }
440
+
441
+ .routeFilter {
442
+ width: 100%;
443
+ }
444
+ }
445
+
436
446
  .warnings-bar {
437
447
  background: ${cssManager.bdTheme('rgba(255, 170, 0, 0.08)', 'rgba(255, 170, 0, 0.1)')};
438
448
  border: 1px solid ${cssManager.bdTheme('rgba(255, 170, 0, 0.25)', 'rgba(255, 170, 0, 0.3)')};
@@ -467,70 +477,25 @@ export class OpsViewRoutes extends DeesElement {
467
477
 
468
478
  public render(): TemplateResult {
469
479
  const { mergedRoutes, warnings } = this.routeState;
470
-
471
- const disabledCount = mergedRoutes.filter((mr) => !mr.enabled).length;
472
- const configCount = mergedRoutes.filter((mr) => mr.origin !== 'api').length;
473
- const operatorCount = mergedRoutes.filter((mr) => {
474
- return mr.origin === 'api'
475
- && mr.metadata?.ownerType !== 'gatewayClient'
476
- && !mr.metadata?.managedRouteKind;
477
- }).length;
478
-
479
- const statsTiles: IStatsTile[] = [
480
- {
481
- id: 'totalRoutes',
482
- title: 'Total Routes',
483
- type: 'number',
484
- value: mergedRoutes.length,
485
- icon: 'lucide:route',
486
- description: 'All configured routes',
487
- color: '#3b82f6',
488
- },
489
- {
490
- id: 'configRoutes',
491
- title: 'System Routes',
492
- type: 'number',
493
- value: configCount,
494
- icon: 'lucide:settings',
495
- description: 'From config, email, and DNS',
496
- color: '#8b5cf6',
497
- },
498
- {
499
- id: 'operatorRoutes',
500
- title: 'Operator Routes',
501
- type: 'number',
502
- value: operatorCount,
503
- icon: 'lucide:code',
504
- description: 'Created directly by operators',
505
- color: '#0ea5e9',
506
- },
507
- {
508
- id: 'disabled',
509
- title: 'Disabled',
510
- type: 'number',
511
- value: disabledCount,
512
- icon: 'lucide:pauseCircle',
513
- description: 'Currently disabled routes',
514
- color: disabledCount > 0 ? '#ef4444' : '#6b7280',
515
- },
516
- ];
517
-
518
480
  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
- }
481
+ const filteredStandardRoutes = standardRoutes.filter((route) => {
482
+ return routeMatchesOwner(route, this.ownerFilter);
483
+ });
484
+ const filteredSpecialRoutes = mergedRoutes.filter((route) => {
485
+ return route.metadata?.managedRouteKind === letsEncryptHttp01ManagedRouteKind
486
+ && routeMatchesOwner(route, this.ownerFilter);
487
+ });
488
+ const filteredRedirects = (this.routeState.httpRedirects || []).filter((redirect) => {
489
+ return redirectMatchesOwner(redirect, mergedRoutes, this.ownerFilter);
530
490
  });
491
+ const statsTiles = this.getStatsTiles(
492
+ filteredStandardRoutes,
493
+ filteredSpecialRoutes,
494
+ filteredRedirects,
495
+ );
531
496
 
532
497
  // Map filtered routes to sz-route-list-view format
533
- const szRoutes = filteredRoutes.map((mr) => {
498
+ const szRoutes = filteredStandardRoutes.map((mr) => {
534
499
  const tags = [...(mr.route.tags || [])];
535
500
  tags.push(mr.origin);
536
501
  if (!mr.enabled) tags.push('disabled');
@@ -549,73 +514,256 @@ export class OpsViewRoutes extends DeesElement {
549
514
  <dees-heading level="3">Route Management</dees-heading>
550
515
 
551
516
  <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`
517
+ <div class="routeFilters">
518
+ <div class="routeFilter">
519
+ <dees-input-multitoggle
520
+ id="routeTypeFilter"
521
+ .label=${'Type'}
522
+ .options=${routeTypeOptions}
523
+ .value=${this.getSelectedRouteTypeOption()}
524
+ ></dees-input-multitoggle>
525
+ </div>
526
+ <div class="routeFilter">
527
+ <dees-input-multitoggle
528
+ id="routeOwnerFilter"
529
+ .label=${'Owner'}
530
+ .options=${routeOwnerOptions}
531
+ .value=${this.getSelectedOwnerOption()}
532
+ ></dees-input-multitoggle>
533
+ </div>
534
+ </div>
535
+
562
536
  <dees-statsgrid
537
+ id="routeStats"
563
538
  .tiles=${statsTiles}
564
- .gridActions=${[
565
- {
566
- name: 'Add Route',
567
- iconName: 'lucide:plus',
568
- action: () => this.showCreateRouteDialog(),
569
- },
570
- {
571
- name: 'Refresh',
572
- iconName: 'lucide:refreshCw',
573
- action: () => this.refreshData(),
574
- },
575
- ]}
539
+ .gridActions=${this.getGridActions()}
576
540
  ></dees-statsgrid>
577
541
 
578
- <dees-appui-tabs
579
- .tabs=${this.ownerTabs}
580
- .selectedTab=${this.getSelectedOwnerTab()}
581
- ></dees-appui-tabs>
582
-
583
- ${warnings.length > 0
542
+ ${this.activeSection === 'all' || this.activeSection === 'standard'
584
543
  ? html`
585
- <div class="warnings-bar">
586
- ${warnings.map(
587
- (w) => html`
588
- <div class="warning-item">
589
- <span class="warning-icon">&#9888;</span>
590
- <span>${w.message}</span>
544
+ ${this.activeSection === 'all'
545
+ ? html`<dees-heading level="4">Standard Routes</dees-heading>`
546
+ : ''}
547
+
548
+ ${warnings.length > 0
549
+ ? html`
550
+ <div class="warnings-bar">
551
+ ${warnings.map(
552
+ (warning) => html`
553
+ <div class="warning-item">
554
+ <span class="warning-icon">&#9888;</span>
555
+ <span>${warning.message}</span>
556
+ </div>
557
+ `,
558
+ )}
591
559
  </div>
592
- `,
593
- )}
594
- </div>
595
- `
596
- : ''}
560
+ `
561
+ : ''}
597
562
 
598
- ${szRoutes.length > 0
599
- ? html`
600
563
  <sz-route-list-view
601
564
  .routes=${szRoutes}
602
- .showActionsFilter=${(route: { id?: string; name?: string }) => this.canEditRoute(route)}
603
- @route-click=${(e: CustomEvent) => this.handleRouteClick(e)}
604
- @route-edit=${(e: CustomEvent) => this.handleRouteEdit(e)}
605
- @route-delete=${(e: CustomEvent) => this.handleRouteDelete(e)}
565
+ .showActionsFilter=${(route: { id?: string; name?: string }) =>
566
+ this.canEditRoute(route)}
567
+ @route-click=${(event: CustomEvent) => this.handleRouteClick(event)}
568
+ @route-edit=${(event: CustomEvent) => this.handleRouteEdit(event)}
569
+ @route-delete=${(event: CustomEvent) => this.handleRouteDelete(event)}
606
570
  ></sz-route-list-view>
571
+ ${szRoutes.length === 0
572
+ ? html`
573
+ <div class="empty-state">
574
+ <p>No standard routes match these filters</p>
575
+ <p>Choose another ownership filter or add a User Route.</p>
576
+ </div>
577
+ `
578
+ : ''}
579
+ `
580
+ : ''}
581
+
582
+ ${this.activeSection === 'all' || this.activeSection === 'special'
583
+ ? html`
584
+ <ops-view-special-forwards
585
+ .embedded=${true}
586
+ .ownerFilter=${this.ownerFilter}
587
+ ></ops-view-special-forwards>
588
+ `
589
+ : ''}
590
+
591
+ ${this.activeSection === 'all' || this.activeSection === 'redirects'
592
+ ? html`
593
+ <ops-view-redirects
594
+ .embedded=${true}
595
+ .ownerFilter=${this.ownerFilter}
596
+ ></ops-view-redirects>
607
597
  `
608
- : html`
609
- <div class="empty-state">
610
- <p>No ${this.getOwnerFilterLabel().toLowerCase()} routes</p>
611
- <p>Choose another ownership filter or add an operator route.</p>
612
- </div>
613
- `}
614
- `}
598
+ : ''}
615
599
  </div>
616
600
  `;
617
601
  }
618
602
 
603
+ private getStatsTiles(
604
+ standardRoutes: interfaces.data.IMergedRoute[],
605
+ specialRoutes: interfaces.data.IMergedRoute[],
606
+ redirects: interfaces.data.IHttpRedirectInfo[],
607
+ ): IStatsTile[] {
608
+ if (this.activeSection === 'all') {
609
+ const disabledCount = [...standardRoutes, ...specialRoutes]
610
+ .filter((route) => !route.enabled).length;
611
+ return [
612
+ {
613
+ id: 'allStandardRoutes',
614
+ title: 'Standard',
615
+ type: 'number',
616
+ value: standardRoutes.length,
617
+ icon: 'lucide:route',
618
+ description: 'Matching standard routes',
619
+ color: '#3b82f6',
620
+ },
621
+ {
622
+ id: 'allSpecialRoutes',
623
+ title: 'Special',
624
+ type: 'number',
625
+ value: specialRoutes.length,
626
+ icon: 'lucide:wandSparkles',
627
+ description: 'Matching managed special forwards',
628
+ color: '#8b5cf6',
629
+ },
630
+ {
631
+ id: 'allRedirects',
632
+ title: 'Redirects',
633
+ type: 'number',
634
+ value: redirects.length,
635
+ icon: 'lucide:cornerDownRight',
636
+ description: 'Matching derived redirects',
637
+ color: '#0ea5e9',
638
+ },
639
+ {
640
+ id: 'allDisabledRoutes',
641
+ title: 'Disabled',
642
+ type: 'number',
643
+ value: disabledCount,
644
+ icon: 'lucide:circlePause',
645
+ description: 'Disabled Standard and Special routes',
646
+ color: disabledCount > 0 ? '#ef4444' : '#6b7280',
647
+ },
648
+ ];
649
+ }
650
+
651
+ if (this.activeSection === 'redirects') {
652
+ const activeCount = redirects.filter((redirect) => redirect.status === 'active').length;
653
+ const coveredCount = redirects.filter((redirect) => redirect.status === 'covered').length;
654
+ const skippedCount = redirects.filter((redirect) => redirect.status === 'skipped').length;
655
+ return [
656
+ {
657
+ id: 'matchingRedirects',
658
+ title: 'Matching',
659
+ type: 'number',
660
+ value: redirects.length,
661
+ icon: 'lucide:cornerDownRight',
662
+ description: 'Redirects in this owner scope',
663
+ color: '#3b82f6',
664
+ },
665
+ {
666
+ id: 'activeRedirects',
667
+ title: 'Active',
668
+ type: 'number',
669
+ value: activeCount,
670
+ icon: 'lucide:circleCheck',
671
+ description: 'Generated at runtime',
672
+ color: '#22c55e',
673
+ },
674
+ {
675
+ id: 'coveredRedirects',
676
+ title: 'Covered',
677
+ type: 'number',
678
+ value: coveredCount,
679
+ icon: 'lucide:shieldCheck',
680
+ description: 'Handled by explicit HTTP routes',
681
+ color: '#8b5cf6',
682
+ },
683
+ {
684
+ id: 'skippedRedirects',
685
+ title: 'Skipped',
686
+ type: 'number',
687
+ value: skippedCount,
688
+ icon: 'lucide:triangleAlert',
689
+ description: 'Overlaps explicit HTTP routes',
690
+ color: skippedCount > 0 ? '#f59e0b' : '#6b7280',
691
+ },
692
+ ];
693
+ }
694
+
695
+ const routes = this.activeSection === 'special' ? specialRoutes : standardRoutes;
696
+ const enabledCount = routes.filter((route) => route.enabled).length;
697
+ const disabledCount = routes.length - enabledCount;
698
+ const readOnlyCount = routes.filter((route) => getRouteOwner(route) !== 'operator').length;
699
+ const scopeName = this.activeSection === 'special' ? 'Special' : 'Standard';
700
+ return [
701
+ {
702
+ id: `matching${scopeName}Routes`,
703
+ title: 'Matching',
704
+ type: 'number',
705
+ value: routes.length,
706
+ icon: 'lucide:route',
707
+ description: `${scopeName} routes in this owner scope`,
708
+ color: '#3b82f6',
709
+ },
710
+ {
711
+ id: `enabled${scopeName}Routes`,
712
+ title: 'Enabled',
713
+ type: 'number',
714
+ value: enabledCount,
715
+ icon: 'lucide:circleCheck',
716
+ description: 'Currently active',
717
+ color: '#22c55e',
718
+ },
719
+ {
720
+ id: `disabled${scopeName}Routes`,
721
+ title: 'Disabled',
722
+ type: 'number',
723
+ value: disabledCount,
724
+ icon: 'lucide:circlePause',
725
+ description: 'Configured but inactive',
726
+ color: disabledCount > 0 ? '#f59e0b' : '#6b7280',
727
+ },
728
+ {
729
+ id: `readOnly${scopeName}Routes`,
730
+ title: 'Read-only',
731
+ type: 'number',
732
+ value: readOnlyCount,
733
+ icon: 'lucide:lockKeyhole',
734
+ description: 'Managed by gateway clients or system',
735
+ color: '#8b5cf6',
736
+ },
737
+ ];
738
+ }
739
+
740
+ private getGridActions() {
741
+ const canCreateUserRoutes = this.ownerFilter === 'all' || this.ownerFilter === 'operator';
742
+ return [
743
+ ...(canCreateUserRoutes && (this.activeSection === 'standard' || this.activeSection === 'all')
744
+ ? [{
745
+ name: 'Add User Route',
746
+ iconName: 'lucide:plus',
747
+ action: () => this.showCreateRouteDialog(),
748
+ }]
749
+ : []),
750
+ ...(canCreateUserRoutes && (this.activeSection === 'special' || this.activeSection === 'all')
751
+ ? [{
752
+ name: 'Add User Special Forward',
753
+ iconName: 'lucide:wandSparkles',
754
+ action: () => showLetsEncryptHttp01ForwardDialog({
755
+ targets: this.profilesTargetsState.targets,
756
+ }),
757
+ }]
758
+ : []),
759
+ {
760
+ name: 'Refresh',
761
+ iconName: 'lucide:refreshCw',
762
+ action: () => this.refreshData(),
763
+ },
764
+ ];
765
+ }
766
+
619
767
  private async handleRouteClick(e: CustomEvent) {
620
768
  const clickedRoute = e.detail;
621
769
  if (!clickedRoute) return;
@@ -953,7 +1101,7 @@ export class OpsViewRoutes extends DeesElement {
953
1101
  ];
954
1102
 
955
1103
  const createModal = await DeesModal.createAndShow({
956
- heading: 'Add Route',
1104
+ heading: 'Add User Route',
957
1105
  content: html`
958
1106
  <dees-form>
959
1107
  <dees-input-text .key=${'name'} .label=${'Route Name'} .required=${true}></dees-input-text>
@@ -1103,32 +1251,63 @@ export class OpsViewRoutes extends DeesElement {
1103
1251
  }
1104
1252
  }
1105
1253
 
1106
- private refreshData() {
1107
- appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
1254
+ private async refreshData(): Promise<void> {
1255
+ await Promise.all([
1256
+ appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null),
1257
+ appstate.routeManagementStatePart.dispatchAction(appstate.fetchHttpRedirectsAction, null),
1258
+ appstate.profilesTargetsStatePart.dispatchAction(
1259
+ appstate.fetchProfilesAndTargetsAction,
1260
+ null,
1261
+ ),
1262
+ ]);
1108
1263
  }
1109
1264
 
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];
1265
+ private getSelectedRouteTypeOption(): string {
1266
+ switch (this.activeSection) {
1267
+ case 'all':
1268
+ return 'All';
1269
+ case 'redirects':
1270
+ return 'Redirect';
1271
+ case 'special':
1272
+ return 'Special';
1273
+ default:
1274
+ return 'Standard';
1275
+ }
1117
1276
  }
1118
1277
 
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];
1278
+ private getSelectedOwnerOption(): string {
1279
+ switch (this.ownerFilter) {
1280
+ case 'operator':
1281
+ return 'User';
1282
+ case 'gatewayClient':
1283
+ return 'Gateway Client';
1284
+ case 'system':
1285
+ return 'System';
1286
+ default:
1287
+ return 'All';
1288
+ }
1289
+ }
1290
+
1291
+ private handleRouteTypeChange(toggle: DeesInputMultitoggle): void {
1292
+ const section: TRoutesSection = toggle.selectedOption === 'All'
1293
+ ? 'all'
1294
+ : toggle.selectedOption === 'Redirect'
1295
+ ? 'redirects'
1296
+ : toggle.selectedOption === 'Special'
1297
+ ? 'special'
1298
+ : 'standard';
1299
+ this.setRouteView(section);
1128
1300
  }
1129
1301
 
1130
- private getOwnerFilterLabel(): string {
1131
- return this.getSelectedOwnerTab().key;
1302
+ private handleRouteOwnerChange(toggle: DeesInputMultitoggle): void {
1303
+ const owner: TRouteOwnerFilter = toggle.selectedOption === 'User'
1304
+ ? 'operator'
1305
+ : toggle.selectedOption === 'Gateway Client'
1306
+ ? 'gatewayClient'
1307
+ : toggle.selectedOption === 'System'
1308
+ ? 'system'
1309
+ : 'all';
1310
+ this.setRouteOwnerFilter(owner);
1132
1311
  }
1133
1312
 
1134
1313
  private setRouteView(section: TRoutesSection): void {
@@ -1147,7 +1326,11 @@ export class OpsViewRoutes extends DeesElement {
1147
1326
  const params = new URL(window.location.href).searchParams;
1148
1327
  const tabParam = params.get('tab');
1149
1328
  const ownerParam = params.get('owner');
1150
- const section: TRoutesSection = tabParam === 'special' || tabParam === 'redirects'
1329
+ const section: TRoutesSection = (
1330
+ tabParam === 'all'
1331
+ || tabParam === 'special'
1332
+ || tabParam === 'redirects'
1333
+ )
1151
1334
  ? tabParam
1152
1335
  : 'standard';
1153
1336
  const owner: TRouteOwnerFilter = (
@@ -1214,12 +1397,29 @@ export class OpsViewRoutes extends DeesElement {
1214
1397
  }
1215
1398
 
1216
1399
  private isSystemManagedRoute(merged: interfaces.data.IMergedRoute): boolean {
1217
- return merged.origin !== 'api'
1218
- || merged.metadata?.ownerType === 'gatewayClient'
1400
+ return getRouteOwner(merged) !== 'operator'
1219
1401
  || Boolean(merged.metadata?.managedRouteKind);
1220
1402
  }
1221
1403
 
1222
1404
  async firstUpdated() {
1223
- await appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
1405
+ const typeToggle = this.shadowRoot?.querySelector(
1406
+ '#routeTypeFilter',
1407
+ ) as DeesInputMultitoggle | null;
1408
+ const ownerToggle = this.shadowRoot?.querySelector(
1409
+ '#routeOwnerFilter',
1410
+ ) as DeesInputMultitoggle | null;
1411
+
1412
+ if (typeToggle) {
1413
+ this.rxSubscriptions.push(typeToggle.changeSubject.subscribe((toggle) => {
1414
+ this.handleRouteTypeChange(toggle);
1415
+ }));
1416
+ }
1417
+ if (ownerToggle) {
1418
+ this.rxSubscriptions.push(ownerToggle.changeSubject.subscribe((toggle) => {
1419
+ this.handleRouteOwnerChange(toggle);
1420
+ }));
1421
+ }
1422
+
1423
+ await this.refreshData();
1224
1424
  }
1225
1425
  }