@serve.zone/dcrouter 6.2.4 → 6.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.
- package/dist_serve/bundle.js +559 -452
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +26 -0
- package/dist_ts/classes.dcrouter.js +49 -5
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +3 -1
- package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
- package/dist_ts/opsserver/classes.opsserver.js +3 -1
- package/dist_ts/opsserver/handlers/index.d.ts +1 -0
- package/dist_ts/opsserver/handlers/index.js +2 -1
- package/dist_ts/opsserver/handlers/remoteingress.handler.d.ts +8 -0
- package/dist_ts/opsserver/handlers/remoteingress.handler.js +107 -0
- package/dist_ts/paths.d.ts +19 -9
- package/dist_ts/paths.js +30 -28
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +3 -2
- package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +56 -0
- package/dist_ts/remoteingress/classes.remoteingress-manager.js +133 -0
- package/dist_ts/remoteingress/classes.tunnel-manager.d.ts +45 -0
- package/dist_ts/remoteingress/classes.tunnel-manager.js +107 -0
- package/dist_ts/remoteingress/index.d.ts +2 -0
- package/dist_ts/remoteingress/index.js +3 -0
- package/dist_ts/security/classes.contentscanner.js +1 -2
- package/dist_ts_interfaces/data/index.d.ts +1 -0
- package/dist_ts_interfaces/data/index.js +2 -1
- package/dist_ts_interfaces/data/remoteingress.d.ts +24 -0
- package/dist_ts_interfaces/data/remoteingress.js +2 -0
- package/dist_ts_interfaces/requests/index.d.ts +1 -0
- package/dist_ts_interfaces/requests/index.js +2 -1
- package/dist_ts_interfaces/requests/remoteingress.d.ts +89 -0
- package/dist_ts_interfaces/requests/remoteingress.js +3 -0
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.d.ts +19 -0
- package/dist_ts_web/appstate.js +124 -2
- package/dist_ts_web/elements/index.d.ts +1 -0
- package/dist_ts_web/elements/index.js +2 -1
- package/dist_ts_web/elements/ops-dashboard.js +6 -1
- package/dist_ts_web/elements/ops-view-remoteingress.d.ts +20 -0
- package/dist_ts_web/elements/ops-view-remoteingress.js +317 -0
- package/dist_ts_web/router.d.ts +1 -1
- package/dist_ts_web/router.js +2 -2
- package/package.json +2 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +79 -6
- package/ts/index.ts +3 -0
- package/ts/opsserver/classes.opsserver.ts +2 -0
- package/ts/opsserver/handlers/index.ts +2 -1
- package/ts/opsserver/handlers/remoteingress.handler.ts +163 -0
- package/ts/paths.ts +32 -31
- package/ts/plugins.ts +3 -1
- package/ts/remoteingress/classes.remoteingress-manager.ts +160 -0
- package/ts/remoteingress/classes.tunnel-manager.ts +126 -0
- package/ts/remoteingress/index.ts +2 -0
- package/ts/security/classes.contentscanner.ts +0 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +180 -1
- package/ts_web/elements/index.ts +1 -0
- package/ts_web/elements/ops-dashboard.ts +5 -0
- package/ts_web/elements/ops-view-remoteingress.ts +290 -0
- package/ts_web/router.ts +1 -1
package/ts_web/appstate.ts
CHANGED
|
@@ -116,7 +116,7 @@ export const configStatePart = await appState.getStatePart<IConfigState>(
|
|
|
116
116
|
// Determine initial view from URL path
|
|
117
117
|
const getInitialView = (): string => {
|
|
118
118
|
const path = typeof window !== 'undefined' ? window.location.pathname : '/';
|
|
119
|
-
const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates'];
|
|
119
|
+
const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates', 'remoteingress'];
|
|
120
120
|
const segments = path.split('/').filter(Boolean);
|
|
121
121
|
const view = segments[0];
|
|
122
122
|
return validViews.includes(view) ? view : 'overview';
|
|
@@ -192,6 +192,34 @@ export const certificateStatePart = await appState.getStatePart<ICertificateStat
|
|
|
192
192
|
'soft'
|
|
193
193
|
);
|
|
194
194
|
|
|
195
|
+
// ============================================================================
|
|
196
|
+
// Remote Ingress State
|
|
197
|
+
// ============================================================================
|
|
198
|
+
|
|
199
|
+
export interface IRemoteIngressState {
|
|
200
|
+
edges: interfaces.data.IRemoteIngress[];
|
|
201
|
+
statuses: interfaces.data.IRemoteIngressStatus[];
|
|
202
|
+
selectedEdgeId: string | null;
|
|
203
|
+
newEdgeSecret: string | null;
|
|
204
|
+
isLoading: boolean;
|
|
205
|
+
error: string | null;
|
|
206
|
+
lastUpdated: number;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export const remoteIngressStatePart = await appState.getStatePart<IRemoteIngressState>(
|
|
210
|
+
'remoteIngress',
|
|
211
|
+
{
|
|
212
|
+
edges: [],
|
|
213
|
+
statuses: [],
|
|
214
|
+
selectedEdgeId: null,
|
|
215
|
+
newEdgeSecret: null,
|
|
216
|
+
isLoading: false,
|
|
217
|
+
error: null,
|
|
218
|
+
lastUpdated: 0,
|
|
219
|
+
},
|
|
220
|
+
'soft'
|
|
221
|
+
);
|
|
222
|
+
|
|
195
223
|
// Actions for state management
|
|
196
224
|
interface IActionContext {
|
|
197
225
|
identity: interfaces.data.IIdentity | null;
|
|
@@ -378,6 +406,13 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
|
|
|
378
406
|
}, 100);
|
|
379
407
|
}
|
|
380
408
|
|
|
409
|
+
// If switching to remoteingress view, ensure we fetch edge data
|
|
410
|
+
if (viewName === 'remoteingress' && currentState.activeView !== 'remoteingress') {
|
|
411
|
+
setTimeout(() => {
|
|
412
|
+
remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
|
413
|
+
}, 100);
|
|
414
|
+
}
|
|
415
|
+
|
|
381
416
|
return {
|
|
382
417
|
...currentState,
|
|
383
418
|
activeView: viewName,
|
|
@@ -745,6 +780,150 @@ export const reprovisionCertificateAction = certificateStatePart.createAction<st
|
|
|
745
780
|
}
|
|
746
781
|
);
|
|
747
782
|
|
|
783
|
+
// ============================================================================
|
|
784
|
+
// Remote Ingress Actions
|
|
785
|
+
// ============================================================================
|
|
786
|
+
|
|
787
|
+
export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(async (statePartArg) => {
|
|
788
|
+
const context = getActionContext();
|
|
789
|
+
const currentState = statePartArg.getState();
|
|
790
|
+
|
|
791
|
+
try {
|
|
792
|
+
const edgesRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
793
|
+
interfaces.requests.IReq_GetRemoteIngresses
|
|
794
|
+
>('/typedrequest', 'getRemoteIngresses');
|
|
795
|
+
|
|
796
|
+
const statusRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
797
|
+
interfaces.requests.IReq_GetRemoteIngressStatus
|
|
798
|
+
>('/typedrequest', 'getRemoteIngressStatus');
|
|
799
|
+
|
|
800
|
+
const [edgesResponse, statusResponse] = await Promise.all([
|
|
801
|
+
edgesRequest.fire({ identity: context.identity }),
|
|
802
|
+
statusRequest.fire({ identity: context.identity }),
|
|
803
|
+
]);
|
|
804
|
+
|
|
805
|
+
return {
|
|
806
|
+
...currentState,
|
|
807
|
+
edges: edgesResponse.edges,
|
|
808
|
+
statuses: statusResponse.statuses,
|
|
809
|
+
isLoading: false,
|
|
810
|
+
error: null,
|
|
811
|
+
lastUpdated: Date.now(),
|
|
812
|
+
};
|
|
813
|
+
} catch (error) {
|
|
814
|
+
return {
|
|
815
|
+
...currentState,
|
|
816
|
+
isLoading: false,
|
|
817
|
+
error: error instanceof Error ? error.message : 'Failed to fetch remote ingress data',
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
|
823
|
+
name: string;
|
|
824
|
+
listenPorts: number[];
|
|
825
|
+
tags?: string[];
|
|
826
|
+
}>(async (statePartArg, dataArg) => {
|
|
827
|
+
const context = getActionContext();
|
|
828
|
+
const currentState = statePartArg.getState();
|
|
829
|
+
|
|
830
|
+
try {
|
|
831
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
832
|
+
interfaces.requests.IReq_CreateRemoteIngress
|
|
833
|
+
>('/typedrequest', 'createRemoteIngress');
|
|
834
|
+
|
|
835
|
+
const response = await request.fire({
|
|
836
|
+
identity: context.identity,
|
|
837
|
+
name: dataArg.name,
|
|
838
|
+
listenPorts: dataArg.listenPorts,
|
|
839
|
+
tags: dataArg.tags,
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
if (response.success) {
|
|
843
|
+
// Refresh the list and store the new secret for display
|
|
844
|
+
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
|
845
|
+
return {
|
|
846
|
+
...statePartArg.getState(),
|
|
847
|
+
newEdgeSecret: response.edge.secret,
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return currentState;
|
|
852
|
+
} catch (error) {
|
|
853
|
+
return {
|
|
854
|
+
...currentState,
|
|
855
|
+
error: error instanceof Error ? error.message : 'Failed to create edge',
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
export const deleteRemoteIngressAction = remoteIngressStatePart.createAction<string>(
|
|
861
|
+
async (statePartArg, edgeId) => {
|
|
862
|
+
const context = getActionContext();
|
|
863
|
+
const currentState = statePartArg.getState();
|
|
864
|
+
|
|
865
|
+
try {
|
|
866
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
867
|
+
interfaces.requests.IReq_DeleteRemoteIngress
|
|
868
|
+
>('/typedrequest', 'deleteRemoteIngress');
|
|
869
|
+
|
|
870
|
+
await request.fire({
|
|
871
|
+
identity: context.identity,
|
|
872
|
+
id: edgeId,
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
|
876
|
+
return statePartArg.getState();
|
|
877
|
+
} catch (error) {
|
|
878
|
+
return {
|
|
879
|
+
...currentState,
|
|
880
|
+
error: error instanceof Error ? error.message : 'Failed to delete edge',
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
);
|
|
885
|
+
|
|
886
|
+
export const regenerateRemoteIngressSecretAction = remoteIngressStatePart.createAction<string>(
|
|
887
|
+
async (statePartArg, edgeId) => {
|
|
888
|
+
const context = getActionContext();
|
|
889
|
+
const currentState = statePartArg.getState();
|
|
890
|
+
|
|
891
|
+
try {
|
|
892
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
893
|
+
interfaces.requests.IReq_RegenerateRemoteIngressSecret
|
|
894
|
+
>('/typedrequest', 'regenerateRemoteIngressSecret');
|
|
895
|
+
|
|
896
|
+
const response = await request.fire({
|
|
897
|
+
identity: context.identity,
|
|
898
|
+
id: edgeId,
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
if (response.success) {
|
|
902
|
+
return {
|
|
903
|
+
...currentState,
|
|
904
|
+
newEdgeSecret: response.secret,
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
return currentState;
|
|
909
|
+
} catch (error) {
|
|
910
|
+
return {
|
|
911
|
+
...currentState,
|
|
912
|
+
error: error instanceof Error ? error.message : 'Failed to regenerate secret',
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
);
|
|
917
|
+
|
|
918
|
+
export const clearNewEdgeSecretAction = remoteIngressStatePart.createAction(
|
|
919
|
+
async (statePartArg) => {
|
|
920
|
+
return {
|
|
921
|
+
...statePartArg.getState(),
|
|
922
|
+
newEdgeSecret: null,
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
);
|
|
926
|
+
|
|
748
927
|
// Combined refresh action for efficient polling
|
|
749
928
|
async function dispatchCombinedRefreshAction() {
|
|
750
929
|
const context = getActionContext();
|
package/ts_web/elements/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { OpsViewLogs } from './ops-view-logs.js';
|
|
|
20
20
|
import { OpsViewConfig } from './ops-view-config.js';
|
|
21
21
|
import { OpsViewSecurity } from './ops-view-security.js';
|
|
22
22
|
import { OpsViewCertificates } from './ops-view-certificates.js';
|
|
23
|
+
import { OpsViewRemoteIngress } from './ops-view-remoteingress.js';
|
|
23
24
|
|
|
24
25
|
@customElement('ops-dashboard')
|
|
25
26
|
export class OpsDashboard extends DeesElement {
|
|
@@ -66,6 +67,10 @@ export class OpsDashboard extends DeesElement {
|
|
|
66
67
|
name: 'Certificates',
|
|
67
68
|
element: OpsViewCertificates,
|
|
68
69
|
},
|
|
70
|
+
{
|
|
71
|
+
name: 'RemoteIngress',
|
|
72
|
+
element: OpsViewRemoteIngress,
|
|
73
|
+
},
|
|
69
74
|
];
|
|
70
75
|
|
|
71
76
|
/**
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DeesElement,
|
|
3
|
+
html,
|
|
4
|
+
customElement,
|
|
5
|
+
type TemplateResult,
|
|
6
|
+
css,
|
|
7
|
+
state,
|
|
8
|
+
cssManager,
|
|
9
|
+
} from '@design.estate/dees-element';
|
|
10
|
+
import * as appstate from '../appstate.js';
|
|
11
|
+
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
|
12
|
+
import { viewHostCss } from './shared/css.js';
|
|
13
|
+
import { type IStatsTile } from '@design.estate/dees-catalog';
|
|
14
|
+
|
|
15
|
+
declare global {
|
|
16
|
+
interface HTMLElementTagNameMap {
|
|
17
|
+
'ops-view-remoteingress': OpsViewRemoteIngress;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@customElement('ops-view-remoteingress')
|
|
22
|
+
export class OpsViewRemoteIngress extends DeesElement {
|
|
23
|
+
@state()
|
|
24
|
+
accessor riState: appstate.IRemoteIngressState = appstate.remoteIngressStatePart.getState();
|
|
25
|
+
|
|
26
|
+
constructor() {
|
|
27
|
+
super();
|
|
28
|
+
const sub = appstate.remoteIngressStatePart.state.subscribe((newState) => {
|
|
29
|
+
this.riState = newState;
|
|
30
|
+
});
|
|
31
|
+
this.rxSubscriptions.push(sub);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async connectedCallback() {
|
|
35
|
+
await super.connectedCallback();
|
|
36
|
+
await appstate.remoteIngressStatePart.dispatchAction(appstate.fetchRemoteIngressAction, null);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public static styles = [
|
|
40
|
+
cssManager.defaultStyles,
|
|
41
|
+
viewHostCss,
|
|
42
|
+
css`
|
|
43
|
+
.remoteIngressContainer {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
gap: 24px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.statusBadge {
|
|
50
|
+
display: inline-flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
padding: 3px 10px;
|
|
53
|
+
border-radius: 12px;
|
|
54
|
+
font-size: 12px;
|
|
55
|
+
font-weight: 600;
|
|
56
|
+
letter-spacing: 0.02em;
|
|
57
|
+
text-transform: uppercase;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.statusBadge.connected {
|
|
61
|
+
background: ${cssManager.bdTheme('#dcfce7', '#14532d')};
|
|
62
|
+
color: ${cssManager.bdTheme('#166534', '#4ade80')};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.statusBadge.disconnected {
|
|
66
|
+
background: ${cssManager.bdTheme('#fef2f2', '#450a0a')};
|
|
67
|
+
color: ${cssManager.bdTheme('#991b1b', '#f87171')};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.statusBadge.disabled {
|
|
71
|
+
background: ${cssManager.bdTheme('#f3f4f6', '#374151')};
|
|
72
|
+
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.secretDialog {
|
|
76
|
+
padding: 16px;
|
|
77
|
+
background: ${cssManager.bdTheme('#fffbeb', '#1c1917')};
|
|
78
|
+
border: 1px solid ${cssManager.bdTheme('#fbbf24', '#92400e')};
|
|
79
|
+
border-radius: 8px;
|
|
80
|
+
margin-bottom: 16px;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.secretDialog code {
|
|
84
|
+
display: block;
|
|
85
|
+
padding: 8px 12px;
|
|
86
|
+
background: ${cssManager.bdTheme('#1f2937', '#111827')};
|
|
87
|
+
color: #10b981;
|
|
88
|
+
border-radius: 4px;
|
|
89
|
+
font-family: monospace;
|
|
90
|
+
font-size: 13px;
|
|
91
|
+
word-break: break-all;
|
|
92
|
+
margin: 8px 0;
|
|
93
|
+
user-select: all;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.secretDialog .warning {
|
|
97
|
+
font-size: 12px;
|
|
98
|
+
color: ${cssManager.bdTheme('#92400e', '#fbbf24')};
|
|
99
|
+
margin-top: 8px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.portsDisplay {
|
|
103
|
+
display: flex;
|
|
104
|
+
gap: 4px;
|
|
105
|
+
flex-wrap: wrap;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.portBadge {
|
|
109
|
+
display: inline-flex;
|
|
110
|
+
padding: 2px 8px;
|
|
111
|
+
border-radius: 4px;
|
|
112
|
+
font-size: 12px;
|
|
113
|
+
font-weight: 500;
|
|
114
|
+
background: ${cssManager.bdTheme('#eff6ff', '#172554')};
|
|
115
|
+
color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};
|
|
116
|
+
}
|
|
117
|
+
`,
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
render(): TemplateResult {
|
|
121
|
+
const totalEdges = this.riState.edges.length;
|
|
122
|
+
const connectedEdges = this.riState.statuses.filter(s => s.connected).length;
|
|
123
|
+
const disconnectedEdges = totalEdges - connectedEdges;
|
|
124
|
+
const activeTunnels = this.riState.statuses.reduce((sum, s) => sum + s.activeTunnels, 0);
|
|
125
|
+
|
|
126
|
+
const statsTiles: IStatsTile[] = [
|
|
127
|
+
{
|
|
128
|
+
id: 'totalEdges',
|
|
129
|
+
title: 'Total Edges',
|
|
130
|
+
type: 'number',
|
|
131
|
+
value: totalEdges,
|
|
132
|
+
icon: 'lucide:server',
|
|
133
|
+
description: 'Registered edge nodes',
|
|
134
|
+
color: '#3b82f6',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: 'connectedEdges',
|
|
138
|
+
title: 'Connected',
|
|
139
|
+
type: 'number',
|
|
140
|
+
value: connectedEdges,
|
|
141
|
+
icon: 'lucide:link',
|
|
142
|
+
description: 'Currently connected edges',
|
|
143
|
+
color: '#10b981',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: 'disconnectedEdges',
|
|
147
|
+
title: 'Disconnected',
|
|
148
|
+
type: 'number',
|
|
149
|
+
value: disconnectedEdges,
|
|
150
|
+
icon: 'lucide:unlink',
|
|
151
|
+
description: 'Offline edge nodes',
|
|
152
|
+
color: disconnectedEdges > 0 ? '#ef4444' : '#6b7280',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: 'activeTunnels',
|
|
156
|
+
title: 'Active Tunnels',
|
|
157
|
+
type: 'number',
|
|
158
|
+
value: activeTunnels,
|
|
159
|
+
icon: 'lucide:cable',
|
|
160
|
+
description: 'Active client connections',
|
|
161
|
+
color: '#8b5cf6',
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
return html`
|
|
166
|
+
<ops-sectionheading>Remote Ingress</ops-sectionheading>
|
|
167
|
+
|
|
168
|
+
${this.riState.newEdgeSecret ? html`
|
|
169
|
+
<div class="secretDialog">
|
|
170
|
+
<strong>Edge Secret (copy now - shown only once):</strong>
|
|
171
|
+
<code>${this.riState.newEdgeSecret}</code>
|
|
172
|
+
<div class="warning">This secret will not be shown again. Save it securely.</div>
|
|
173
|
+
<dees-button
|
|
174
|
+
@click=${() => appstate.remoteIngressStatePart.dispatchAction(appstate.clearNewEdgeSecretAction, null)}
|
|
175
|
+
>Dismiss</dees-button>
|
|
176
|
+
</div>
|
|
177
|
+
` : ''}
|
|
178
|
+
|
|
179
|
+
<div class="remoteIngressContainer">
|
|
180
|
+
<dees-statsgrid .tiles=${statsTiles}></dees-statsgrid>
|
|
181
|
+
|
|
182
|
+
<dees-table
|
|
183
|
+
.heading1=${'Edge Nodes'}
|
|
184
|
+
.heading2=${'Manage remote ingress edge registrations'}
|
|
185
|
+
.data=${this.riState.edges}
|
|
186
|
+
.displayFunction=${(edge: interfaces.data.IRemoteIngress) => ({
|
|
187
|
+
name: edge.name,
|
|
188
|
+
status: this.getEdgeStatusHtml(edge),
|
|
189
|
+
publicIp: this.getEdgePublicIp(edge.id),
|
|
190
|
+
ports: this.getPortsHtml(edge.listenPorts),
|
|
191
|
+
tunnels: this.getEdgeTunnelCount(edge.id),
|
|
192
|
+
lastHeartbeat: this.getLastHeartbeat(edge.id),
|
|
193
|
+
})}
|
|
194
|
+
.dataActions=${[
|
|
195
|
+
{
|
|
196
|
+
name: 'Regenerate Secret',
|
|
197
|
+
iconName: 'lucide:key',
|
|
198
|
+
action: async (edge: interfaces.data.IRemoteIngress) => {
|
|
199
|
+
await appstate.remoteIngressStatePart.dispatchAction(
|
|
200
|
+
appstate.regenerateRemoteIngressSecretAction,
|
|
201
|
+
edge.id,
|
|
202
|
+
);
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'Delete',
|
|
207
|
+
iconName: 'lucide:trash2',
|
|
208
|
+
action: async (edge: interfaces.data.IRemoteIngress) => {
|
|
209
|
+
await appstate.remoteIngressStatePart.dispatchAction(
|
|
210
|
+
appstate.deleteRemoteIngressAction,
|
|
211
|
+
edge.id,
|
|
212
|
+
);
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
]}
|
|
216
|
+
.createNewAction=${async () => {
|
|
217
|
+
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
218
|
+
const result = await DeesModal.createAndShow({
|
|
219
|
+
heading: 'Create Edge Node',
|
|
220
|
+
content: html`
|
|
221
|
+
<dees-form>
|
|
222
|
+
<dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
|
|
223
|
+
<dees-input-text .key=${'listenPorts'} .label=${'Listen Ports (comma-separated)'} .required=${true} .value=${'443,25'}></dees-input-text>
|
|
224
|
+
<dees-input-text .key=${'tags'} .label=${'Tags (comma-separated, optional)'}></dees-input-text>
|
|
225
|
+
</dees-form>
|
|
226
|
+
`,
|
|
227
|
+
menuOptions: [],
|
|
228
|
+
});
|
|
229
|
+
if (result) {
|
|
230
|
+
const formData = result as any;
|
|
231
|
+
const ports = (formData.name ? formData.listenPorts : '443')
|
|
232
|
+
.split(',')
|
|
233
|
+
.map((p: string) => parseInt(p.trim(), 10))
|
|
234
|
+
.filter((p: number) => !isNaN(p));
|
|
235
|
+
const tags = formData.tags
|
|
236
|
+
? formData.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
|
|
237
|
+
: undefined;
|
|
238
|
+
await appstate.remoteIngressStatePart.dispatchAction(
|
|
239
|
+
appstate.createRemoteIngressAction,
|
|
240
|
+
{
|
|
241
|
+
name: formData.name,
|
|
242
|
+
listenPorts: ports,
|
|
243
|
+
tags,
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}}
|
|
248
|
+
></dees-table>
|
|
249
|
+
</div>
|
|
250
|
+
`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private getEdgeStatus(edgeId: string): interfaces.data.IRemoteIngressStatus | undefined {
|
|
254
|
+
return this.riState.statuses.find(s => s.edgeId === edgeId);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private getEdgeStatusHtml(edge: interfaces.data.IRemoteIngress): TemplateResult {
|
|
258
|
+
if (!edge.enabled) {
|
|
259
|
+
return html`<span class="statusBadge disabled">Disabled</span>`;
|
|
260
|
+
}
|
|
261
|
+
const status = this.getEdgeStatus(edge.id);
|
|
262
|
+
if (status?.connected) {
|
|
263
|
+
return html`<span class="statusBadge connected">Connected</span>`;
|
|
264
|
+
}
|
|
265
|
+
return html`<span class="statusBadge disconnected">Disconnected</span>`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private getEdgePublicIp(edgeId: string): string {
|
|
269
|
+
const status = this.getEdgeStatus(edgeId);
|
|
270
|
+
return status?.publicIp || '-';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private getPortsHtml(ports: number[]): TemplateResult {
|
|
274
|
+
return html`<div class="portsDisplay">${ports.map(p => html`<span class="portBadge">${p}</span>`)}</div>`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private getEdgeTunnelCount(edgeId: string): number {
|
|
278
|
+
const status = this.getEdgeStatus(edgeId);
|
|
279
|
+
return status?.activeTunnels || 0;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private getLastHeartbeat(edgeId: string): string {
|
|
283
|
+
const status = this.getEdgeStatus(edgeId);
|
|
284
|
+
if (!status?.lastHeartbeat) return '-';
|
|
285
|
+
const ago = Date.now() - status.lastHeartbeat;
|
|
286
|
+
if (ago < 60000) return `${Math.floor(ago / 1000)}s ago`;
|
|
287
|
+
if (ago < 3600000) return `${Math.floor(ago / 60000)}m ago`;
|
|
288
|
+
return `${Math.floor(ago / 3600000)}h ago`;
|
|
289
|
+
}
|
|
290
|
+
}
|
package/ts_web/router.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as appstate from './appstate.js';
|
|
|
3
3
|
|
|
4
4
|
const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
|
|
5
5
|
|
|
6
|
-
export const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates'] as const;
|
|
6
|
+
export const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates', 'remoteingress'] as const;
|
|
7
7
|
export const validEmailFolders = ['queued', 'sent', 'failed', 'security'] as const;
|
|
8
8
|
|
|
9
9
|
export type TValidView = typeof validViews[number];
|