@serve.zone/dcrouter 8.0.0 → 8.1.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 +1659 -891
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +9 -0
- package/dist_ts/classes.dcrouter.js +27 -1
- package/dist_ts/config/classes.api-token-manager.d.ts +38 -0
- package/dist_ts/config/classes.api-token-manager.js +134 -0
- package/dist_ts/config/classes.route-config-manager.d.ts +35 -0
- package/dist_ts/config/classes.route-config-manager.js +231 -0
- package/dist_ts/config/index.d.ts +2 -0
- package/dist_ts/config/index.js +3 -1
- package/dist_ts/opsserver/classes.opsserver.d.ts +2 -0
- package/dist_ts/opsserver/classes.opsserver.js +5 -1
- package/dist_ts/opsserver/handlers/{remoteingress.handler.d.ts → api-token.handler.d.ts} +5 -1
- package/dist_ts/opsserver/handlers/api-token.handler.js +66 -0
- package/dist_ts/opsserver/handlers/index.d.ts +2 -0
- package/dist_ts/opsserver/handlers/index.js +3 -1
- package/dist_ts/opsserver/handlers/{radius.handler.d.ts → route-management.handler.d.ts} +6 -1
- package/dist_ts/opsserver/handlers/route-management.handler.js +117 -0
- package/dist_ts_interfaces/data/index.d.ts +1 -0
- package/dist_ts_interfaces/data/index.js +2 -1
- package/dist_ts_interfaces/data/route-management.d.ts +68 -0
- package/dist_ts_interfaces/data/route-management.js +2 -0
- package/dist_ts_interfaces/requests/api-tokens.d.ts +63 -0
- package/dist_ts_interfaces/requests/api-tokens.js +2 -0
- package/dist_ts_interfaces/requests/index.d.ts +2 -0
- package/dist_ts_interfaces/requests/index.js +3 -1
- package/dist_ts_interfaces/requests/route-management.d.ts +114 -0
- package/dist_ts_interfaces/requests/route-management.js +2 -0
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.d.ts +36 -0
- package/dist_ts_web/appstate.js +220 -2
- package/dist_ts_web/elements/index.d.ts +2 -0
- package/dist_ts_web/elements/index.js +3 -1
- package/dist_ts_web/elements/ops-dashboard.js +11 -1
- package/dist_ts_web/elements/ops-view-apitokens.d.ts +12 -0
- package/dist_ts_web/elements/ops-view-apitokens.js +306 -0
- package/dist_ts_web/elements/ops-view-routes.d.ts +12 -0
- package/dist_ts_web/elements/ops-view-routes.js +404 -0
- package/dist_ts_web/router.d.ts +1 -1
- package/dist_ts_web/router.js +2 -2
- package/package.json +2 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +37 -1
- package/ts/config/classes.api-token-manager.ts +155 -0
- package/ts/config/classes.route-config-manager.ts +271 -0
- package/ts/config/index.ts +3 -1
- package/ts/opsserver/classes.opsserver.ts +4 -0
- package/ts/opsserver/handlers/api-token.handler.ts +96 -0
- package/ts/opsserver/handlers/index.ts +3 -1
- package/ts/opsserver/handlers/route-management.handler.ts +163 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +308 -1
- package/ts_web/elements/index.ts +2 -0
- package/ts_web/elements/ops-dashboard.ts +10 -0
- package/ts_web/elements/ops-view-apitokens.ts +281 -0
- package/ts_web/elements/ops-view-routes.ts +389 -0
- package/ts_web/router.ts +1 -1
- package/dist_ts/cache/classes.cache.cleaner.d.ts +0 -47
- package/dist_ts/cache/classes.cache.cleaner.js +0 -130
- package/dist_ts/cache/classes.cached.document.d.ts +0 -76
- package/dist_ts/cache/classes.cached.document.js +0 -100
- package/dist_ts/cache/classes.cachedb.d.ts +0 -60
- package/dist_ts/cache/classes.cachedb.js +0 -126
- package/dist_ts/cache/documents/classes.cached.email.d.ts +0 -125
- package/dist_ts/cache/documents/classes.cached.email.js +0 -337
- package/dist_ts/cache/documents/classes.cached.ip.reputation.d.ts +0 -119
- package/dist_ts/cache/documents/classes.cached.ip.reputation.js +0 -323
- package/dist_ts/cache/documents/index.d.ts +0 -2
- package/dist_ts/cache/documents/index.js +0 -3
- package/dist_ts/cache/index.d.ts +0 -4
- package/dist_ts/cache/index.js +0 -7
- package/dist_ts/monitoring/classes.metricscache.d.ts +0 -32
- package/dist_ts/monitoring/classes.metricscache.js +0 -63
- package/dist_ts/monitoring/classes.metricsmanager.d.ts +0 -169
- package/dist_ts/monitoring/classes.metricsmanager.js +0 -591
- package/dist_ts/monitoring/index.d.ts +0 -1
- package/dist_ts/monitoring/index.js +0 -2
- package/dist_ts/opsserver/handlers/admin.handler.d.ts +0 -31
- package/dist_ts/opsserver/handlers/admin.handler.js +0 -180
- package/dist_ts/opsserver/handlers/certificate.handler.d.ts +0 -34
- package/dist_ts/opsserver/handlers/certificate.handler.js +0 -419
- package/dist_ts/opsserver/handlers/config.handler.d.ts +0 -9
- package/dist_ts/opsserver/handlers/config.handler.js +0 -67
- package/dist_ts/opsserver/handlers/email-ops.handler.d.ts +0 -32
- package/dist_ts/opsserver/handlers/email-ops.handler.js +0 -226
- package/dist_ts/opsserver/handlers/logs.handler.d.ts +0 -17
- package/dist_ts/opsserver/handlers/logs.handler.js +0 -215
- package/dist_ts/opsserver/handlers/radius.handler.js +0 -296
- package/dist_ts/opsserver/handlers/remoteingress.handler.js +0 -154
- package/dist_ts/opsserver/handlers/security.handler.d.ts +0 -11
- package/dist_ts/opsserver/handlers/security.handler.js +0 -232
- package/dist_ts/opsserver/handlers/stats.handler.d.ts +0 -13
- package/dist_ts/opsserver/handlers/stats.handler.js +0 -400
- package/dist_ts/security/classes.securitylogger.d.ts +0 -140
- package/dist_ts/security/classes.securitylogger.js +0 -235
- package/dist_ts/storage/classes.storagemanager.d.ts +0 -82
- package/dist_ts/storage/classes.storagemanager.js +0 -344
- package/dist_ts/storage/index.d.ts +0 -1
- package/dist_ts/storage/index.js +0 -3
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import * as appstate from '../appstate.js';
|
|
2
|
+
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
|
3
|
+
import { viewHostCss } from './shared/css.js';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
DeesElement,
|
|
7
|
+
css,
|
|
8
|
+
cssManager,
|
|
9
|
+
customElement,
|
|
10
|
+
html,
|
|
11
|
+
state,
|
|
12
|
+
type TemplateResult,
|
|
13
|
+
} from '@design.estate/dees-element';
|
|
14
|
+
|
|
15
|
+
type TApiTokenScope = interfaces.data.TApiTokenScope;
|
|
16
|
+
|
|
17
|
+
@customElement('ops-view-apitokens')
|
|
18
|
+
export class OpsViewApiTokens extends DeesElement {
|
|
19
|
+
@state() accessor routeState: appstate.IRouteManagementState = {
|
|
20
|
+
mergedRoutes: [],
|
|
21
|
+
warnings: [],
|
|
22
|
+
apiTokens: [],
|
|
23
|
+
isLoading: false,
|
|
24
|
+
error: null,
|
|
25
|
+
lastUpdated: 0,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
constructor() {
|
|
29
|
+
super();
|
|
30
|
+
const sub = appstate.routeManagementStatePart
|
|
31
|
+
.select((s) => s)
|
|
32
|
+
.subscribe((routeState) => {
|
|
33
|
+
this.routeState = routeState;
|
|
34
|
+
});
|
|
35
|
+
this.rxSubscriptions.push(sub);
|
|
36
|
+
|
|
37
|
+
// Re-fetch tokens when user logs in (fixes race condition where
|
|
38
|
+
// the view is created before authentication completes)
|
|
39
|
+
const loginSub = appstate.loginStatePart
|
|
40
|
+
.select((s) => s.isLoggedIn)
|
|
41
|
+
.subscribe((isLoggedIn) => {
|
|
42
|
+
if (isLoggedIn) {
|
|
43
|
+
appstate.routeManagementStatePart.dispatchAction(appstate.fetchApiTokensAction, null);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
this.rxSubscriptions.push(loginSub);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public static styles = [
|
|
50
|
+
cssManager.defaultStyles,
|
|
51
|
+
viewHostCss,
|
|
52
|
+
css`
|
|
53
|
+
.apiTokensContainer {
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
gap: 24px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.scopePill {
|
|
60
|
+
display: inline-flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
padding: 2px 6px;
|
|
63
|
+
border-radius: 3px;
|
|
64
|
+
font-size: 11px;
|
|
65
|
+
background: ${cssManager.bdTheme('rgba(0, 130, 200, 0.1)', 'rgba(0, 170, 255, 0.1)')};
|
|
66
|
+
color: ${cssManager.bdTheme('#0369a1', '#0af')};
|
|
67
|
+
margin-right: 4px;
|
|
68
|
+
margin-bottom: 2px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.statusBadge {
|
|
72
|
+
display: inline-flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
padding: 3px 10px;
|
|
75
|
+
border-radius: 12px;
|
|
76
|
+
font-size: 12px;
|
|
77
|
+
font-weight: 600;
|
|
78
|
+
letter-spacing: 0.02em;
|
|
79
|
+
text-transform: uppercase;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.statusBadge.active {
|
|
83
|
+
background: ${cssManager.bdTheme('#dcfce7', '#14532d')};
|
|
84
|
+
color: ${cssManager.bdTheme('#166534', '#4ade80')};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.statusBadge.disabled {
|
|
88
|
+
background: ${cssManager.bdTheme('#fef2f2', '#450a0a')};
|
|
89
|
+
color: ${cssManager.bdTheme('#991b1b', '#f87171')};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.statusBadge.expired {
|
|
93
|
+
background: ${cssManager.bdTheme('#f3f4f6', '#374151')};
|
|
94
|
+
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
95
|
+
}
|
|
96
|
+
`,
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
public render(): TemplateResult {
|
|
100
|
+
const { apiTokens } = this.routeState;
|
|
101
|
+
|
|
102
|
+
return html`
|
|
103
|
+
<ops-sectionheading>API Tokens</ops-sectionheading>
|
|
104
|
+
|
|
105
|
+
<div class="apiTokensContainer">
|
|
106
|
+
<dees-table
|
|
107
|
+
.heading1=${'API Tokens'}
|
|
108
|
+
.heading2=${'Manage programmatic access tokens'}
|
|
109
|
+
.data=${apiTokens}
|
|
110
|
+
.dataName=${'token'}
|
|
111
|
+
.searchable=${true}
|
|
112
|
+
.displayFunction=${(token: interfaces.data.IApiTokenInfo) => ({
|
|
113
|
+
name: token.name,
|
|
114
|
+
scopes: this.renderScopePills(token.scopes),
|
|
115
|
+
status: this.renderStatusBadge(token),
|
|
116
|
+
created: new Date(token.createdAt).toLocaleDateString(),
|
|
117
|
+
expires: token.expiresAt ? new Date(token.expiresAt).toLocaleDateString() : 'Never',
|
|
118
|
+
lastUsed: token.lastUsedAt ? new Date(token.lastUsedAt).toLocaleDateString() : 'Never',
|
|
119
|
+
})}
|
|
120
|
+
.dataActions=${[
|
|
121
|
+
{
|
|
122
|
+
name: 'Create Token',
|
|
123
|
+
iconName: 'lucide:plus',
|
|
124
|
+
type: ['header'],
|
|
125
|
+
actionFunc: async () => {
|
|
126
|
+
await this.showCreateTokenDialog();
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'Enable',
|
|
131
|
+
iconName: 'lucide:play',
|
|
132
|
+
type: ['inRow', 'contextmenu'] as any,
|
|
133
|
+
actionRelevancyCheckFunc: (actionData: any) => !actionData.item.enabled,
|
|
134
|
+
actionFunc: async (actionData: any) => {
|
|
135
|
+
const token = actionData.item as interfaces.data.IApiTokenInfo;
|
|
136
|
+
await appstate.routeManagementStatePart.dispatchAction(
|
|
137
|
+
appstate.toggleApiTokenAction,
|
|
138
|
+
{ id: token.id, enabled: true },
|
|
139
|
+
);
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: 'Disable',
|
|
144
|
+
iconName: 'lucide:pause',
|
|
145
|
+
type: ['inRow', 'contextmenu'] as any,
|
|
146
|
+
actionRelevancyCheckFunc: (actionData: any) => actionData.item.enabled,
|
|
147
|
+
actionFunc: async (actionData: any) => {
|
|
148
|
+
const token = actionData.item as interfaces.data.IApiTokenInfo;
|
|
149
|
+
await appstate.routeManagementStatePart.dispatchAction(
|
|
150
|
+
appstate.toggleApiTokenAction,
|
|
151
|
+
{ id: token.id, enabled: false },
|
|
152
|
+
);
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'Revoke',
|
|
157
|
+
iconName: 'lucide:trash2',
|
|
158
|
+
type: ['inRow', 'contextmenu'] as any,
|
|
159
|
+
actionFunc: async (actionData: any) => {
|
|
160
|
+
const token = actionData.item as interfaces.data.IApiTokenInfo;
|
|
161
|
+
await appstate.routeManagementStatePart.dispatchAction(
|
|
162
|
+
appstate.revokeApiTokenAction,
|
|
163
|
+
token.id,
|
|
164
|
+
);
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
]}
|
|
168
|
+
></dees-table>
|
|
169
|
+
</div>
|
|
170
|
+
`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private renderScopePills(scopes: TApiTokenScope[]): TemplateResult {
|
|
174
|
+
return html`<div style="display: flex; flex-wrap: wrap; gap: 2px;">${scopes.map(
|
|
175
|
+
(s) => html`<span class="scopePill">${s}</span>`,
|
|
176
|
+
)}</div>`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private renderStatusBadge(token: interfaces.data.IApiTokenInfo): TemplateResult {
|
|
180
|
+
if (!token.enabled) {
|
|
181
|
+
return html`<span class="statusBadge disabled">Disabled</span>`;
|
|
182
|
+
}
|
|
183
|
+
if (token.expiresAt && token.expiresAt < Date.now()) {
|
|
184
|
+
return html`<span class="statusBadge expired">Expired</span>`;
|
|
185
|
+
}
|
|
186
|
+
return html`<span class="statusBadge active">Active</span>`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private async showCreateTokenDialog() {
|
|
190
|
+
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
191
|
+
|
|
192
|
+
const allScopes: TApiTokenScope[] = [
|
|
193
|
+
'routes:read',
|
|
194
|
+
'routes:write',
|
|
195
|
+
'config:read',
|
|
196
|
+
'tokens:read',
|
|
197
|
+
'tokens:manage',
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
await DeesModal.createAndShow({
|
|
201
|
+
heading: 'Create API Token',
|
|
202
|
+
content: html`
|
|
203
|
+
<div style="color: #888; margin-bottom: 12px; font-size: 13px;">
|
|
204
|
+
The token value will be shown once after creation. Copy it immediately.
|
|
205
|
+
</div>
|
|
206
|
+
<dees-form>
|
|
207
|
+
<dees-input-text .key=${'name'} .label=${'Token Name'} .required=${true}></dees-input-text>
|
|
208
|
+
<dees-input-tags
|
|
209
|
+
.key=${'scopes'}
|
|
210
|
+
.label=${'Token Scopes'}
|
|
211
|
+
.value=${['routes:read', 'routes:write']}
|
|
212
|
+
.suggestions=${allScopes}
|
|
213
|
+
.required=${true}
|
|
214
|
+
></dees-input-tags>
|
|
215
|
+
<dees-input-text .key=${'expiresInDays'} .label=${'Expires in (days, blank = never)'}></dees-input-text>
|
|
216
|
+
</dees-form>
|
|
217
|
+
`,
|
|
218
|
+
menuOptions: [
|
|
219
|
+
{
|
|
220
|
+
name: 'Cancel',
|
|
221
|
+
iconName: 'lucide:x',
|
|
222
|
+
action: async (modalArg: any) => await modalArg.destroy(),
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: 'Create',
|
|
226
|
+
iconName: 'lucide:key',
|
|
227
|
+
action: async (modalArg: any) => {
|
|
228
|
+
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
|
229
|
+
if (!form) return;
|
|
230
|
+
const formData = await form.collectFormData();
|
|
231
|
+
if (!formData.name) return;
|
|
232
|
+
|
|
233
|
+
// dees-input-tags returns string[] directly
|
|
234
|
+
const scopes = (formData.scopes || [])
|
|
235
|
+
.filter((s: string) => allScopes.includes(s as any)) as TApiTokenScope[];
|
|
236
|
+
|
|
237
|
+
const expiresInDays = formData.expiresInDays
|
|
238
|
+
? parseInt(formData.expiresInDays, 10)
|
|
239
|
+
: null;
|
|
240
|
+
|
|
241
|
+
await modalArg.destroy();
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
const response = await appstate.createApiToken(formData.name, scopes, expiresInDays);
|
|
245
|
+
if (response.success && response.tokenValue) {
|
|
246
|
+
// Refresh the list first so it's ready when user dismisses the modal
|
|
247
|
+
await appstate.routeManagementStatePart.dispatchAction(appstate.fetchApiTokensAction, null);
|
|
248
|
+
|
|
249
|
+
// Show the token value in a new modal
|
|
250
|
+
await DeesModal.createAndShow({
|
|
251
|
+
heading: 'Token Created',
|
|
252
|
+
content: html`
|
|
253
|
+
<div style="color: #ccc; padding: 8px 0;">
|
|
254
|
+
<p>Copy this token now. It will not be shown again.</p>
|
|
255
|
+
<div style="background: #111; padding: 12px; border-radius: 6px; margin-top: 8px;">
|
|
256
|
+
<code style="color: #0f8; word-break: break-all; font-size: 13px;">${response.tokenValue}</code>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
`,
|
|
260
|
+
menuOptions: [
|
|
261
|
+
{
|
|
262
|
+
name: 'Done',
|
|
263
|
+
iconName: 'lucide:check',
|
|
264
|
+
action: async (m: any) => await m.destroy(),
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
} catch (error) {
|
|
270
|
+
console.error('Failed to create token:', error);
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async firstUpdated() {
|
|
279
|
+
await appstate.routeManagementStatePart.dispatchAction(appstate.fetchApiTokensAction, null);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import * as appstate from '../appstate.js';
|
|
2
|
+
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
|
3
|
+
import { viewHostCss } from './shared/css.js';
|
|
4
|
+
import { type IStatsTile } from '@design.estate/dees-catalog';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
DeesElement,
|
|
8
|
+
css,
|
|
9
|
+
cssManager,
|
|
10
|
+
customElement,
|
|
11
|
+
html,
|
|
12
|
+
state,
|
|
13
|
+
type TemplateResult,
|
|
14
|
+
} from '@design.estate/dees-element';
|
|
15
|
+
|
|
16
|
+
@customElement('ops-view-routes')
|
|
17
|
+
export class OpsViewRoutes extends DeesElement {
|
|
18
|
+
@state() accessor routeState: appstate.IRouteManagementState = {
|
|
19
|
+
mergedRoutes: [],
|
|
20
|
+
warnings: [],
|
|
21
|
+
apiTokens: [],
|
|
22
|
+
isLoading: false,
|
|
23
|
+
error: null,
|
|
24
|
+
lastUpdated: 0,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
constructor() {
|
|
28
|
+
super();
|
|
29
|
+
const sub = appstate.routeManagementStatePart
|
|
30
|
+
.select((s) => s)
|
|
31
|
+
.subscribe((routeState) => {
|
|
32
|
+
this.routeState = routeState;
|
|
33
|
+
});
|
|
34
|
+
this.rxSubscriptions.push(sub);
|
|
35
|
+
|
|
36
|
+
// Re-fetch routes when user logs in (fixes race condition where
|
|
37
|
+
// the view is created before authentication completes)
|
|
38
|
+
const loginSub = appstate.loginStatePart
|
|
39
|
+
.select((s) => s.isLoggedIn)
|
|
40
|
+
.subscribe((isLoggedIn) => {
|
|
41
|
+
if (isLoggedIn) {
|
|
42
|
+
appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
this.rxSubscriptions.push(loginSub);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public static styles = [
|
|
49
|
+
cssManager.defaultStyles,
|
|
50
|
+
viewHostCss,
|
|
51
|
+
css`
|
|
52
|
+
.routesContainer {
|
|
53
|
+
display: flex;
|
|
54
|
+
flex-direction: column;
|
|
55
|
+
gap: 24px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.warnings-bar {
|
|
59
|
+
background: ${cssManager.bdTheme('rgba(255, 170, 0, 0.08)', 'rgba(255, 170, 0, 0.1)')};
|
|
60
|
+
border: 1px solid ${cssManager.bdTheme('rgba(255, 170, 0, 0.25)', 'rgba(255, 170, 0, 0.3)')};
|
|
61
|
+
border-radius: 8px;
|
|
62
|
+
padding: 12px 16px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.warning-item {
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
gap: 8px;
|
|
69
|
+
padding: 4px 0;
|
|
70
|
+
font-size: 13px;
|
|
71
|
+
color: ${cssManager.bdTheme('#b45309', '#fa0')};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.warning-icon {
|
|
75
|
+
flex-shrink: 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.empty-state {
|
|
79
|
+
text-align: center;
|
|
80
|
+
padding: 48px 24px;
|
|
81
|
+
color: ${cssManager.bdTheme('#6b7280', '#666')};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.empty-state p {
|
|
85
|
+
margin: 8px 0;
|
|
86
|
+
}
|
|
87
|
+
`,
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
public render(): TemplateResult {
|
|
91
|
+
const { mergedRoutes, warnings } = this.routeState;
|
|
92
|
+
|
|
93
|
+
const hardcodedCount = mergedRoutes.filter((mr) => mr.source === 'hardcoded').length;
|
|
94
|
+
const programmaticCount = mergedRoutes.filter((mr) => mr.source === 'programmatic').length;
|
|
95
|
+
const disabledCount = mergedRoutes.filter((mr) => !mr.enabled).length;
|
|
96
|
+
|
|
97
|
+
const statsTiles: IStatsTile[] = [
|
|
98
|
+
{
|
|
99
|
+
id: 'totalRoutes',
|
|
100
|
+
title: 'Total Routes',
|
|
101
|
+
type: 'number',
|
|
102
|
+
value: mergedRoutes.length,
|
|
103
|
+
icon: 'lucide:route',
|
|
104
|
+
description: 'All configured routes',
|
|
105
|
+
color: '#3b82f6',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'hardcoded',
|
|
109
|
+
title: 'Hardcoded',
|
|
110
|
+
type: 'number',
|
|
111
|
+
value: hardcodedCount,
|
|
112
|
+
icon: 'lucide:lock',
|
|
113
|
+
description: 'Routes from constructor config',
|
|
114
|
+
color: '#8b5cf6',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: 'programmatic',
|
|
118
|
+
title: 'Programmatic',
|
|
119
|
+
type: 'number',
|
|
120
|
+
value: programmaticCount,
|
|
121
|
+
icon: 'lucide:code',
|
|
122
|
+
description: 'Routes added via API',
|
|
123
|
+
color: '#0ea5e9',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: 'disabled',
|
|
127
|
+
title: 'Disabled',
|
|
128
|
+
type: 'number',
|
|
129
|
+
value: disabledCount,
|
|
130
|
+
icon: 'lucide:pauseCircle',
|
|
131
|
+
description: 'Currently disabled routes',
|
|
132
|
+
color: disabledCount > 0 ? '#ef4444' : '#6b7280',
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
// Map merged routes to sz-route-list-view format
|
|
137
|
+
const szRoutes = mergedRoutes.map((mr) => {
|
|
138
|
+
const tags = [...(mr.route.tags || [])];
|
|
139
|
+
tags.push(mr.source);
|
|
140
|
+
if (!mr.enabled) tags.push('disabled');
|
|
141
|
+
if (mr.overridden) tags.push('overridden');
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
...mr.route,
|
|
145
|
+
enabled: mr.enabled,
|
|
146
|
+
tags,
|
|
147
|
+
id: mr.storedRouteId || mr.route.name || undefined,
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return html`
|
|
152
|
+
<ops-sectionheading>Route Management</ops-sectionheading>
|
|
153
|
+
|
|
154
|
+
<div class="routesContainer">
|
|
155
|
+
<dees-statsgrid
|
|
156
|
+
.tiles=${statsTiles}
|
|
157
|
+
.gridActions=${[
|
|
158
|
+
{
|
|
159
|
+
name: 'Add Route',
|
|
160
|
+
iconName: 'lucide:plus',
|
|
161
|
+
action: () => this.showCreateRouteDialog(),
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'Refresh',
|
|
165
|
+
iconName: 'lucide:refreshCw',
|
|
166
|
+
action: () => this.refreshData(),
|
|
167
|
+
},
|
|
168
|
+
]}
|
|
169
|
+
></dees-statsgrid>
|
|
170
|
+
|
|
171
|
+
${warnings.length > 0
|
|
172
|
+
? html`
|
|
173
|
+
<div class="warnings-bar">
|
|
174
|
+
${warnings.map(
|
|
175
|
+
(w) => html`
|
|
176
|
+
<div class="warning-item">
|
|
177
|
+
<span class="warning-icon">⚠</span>
|
|
178
|
+
<span>${w.message}</span>
|
|
179
|
+
</div>
|
|
180
|
+
`,
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
`
|
|
184
|
+
: ''}
|
|
185
|
+
|
|
186
|
+
${szRoutes.length > 0
|
|
187
|
+
? html`
|
|
188
|
+
<sz-route-list-view
|
|
189
|
+
.routes=${szRoutes}
|
|
190
|
+
@route-click=${(e: CustomEvent) => this.handleRouteClick(e)}
|
|
191
|
+
></sz-route-list-view>
|
|
192
|
+
`
|
|
193
|
+
: html`
|
|
194
|
+
<div class="empty-state">
|
|
195
|
+
<p>No routes configured</p>
|
|
196
|
+
<p>Add a programmatic route or check your constructor configuration.</p>
|
|
197
|
+
</div>
|
|
198
|
+
`}
|
|
199
|
+
</div>
|
|
200
|
+
`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private async handleRouteClick(e: CustomEvent) {
|
|
204
|
+
const clickedRoute = e.detail;
|
|
205
|
+
if (!clickedRoute) return;
|
|
206
|
+
|
|
207
|
+
// Find the corresponding merged route
|
|
208
|
+
const merged = this.routeState.mergedRoutes.find(
|
|
209
|
+
(mr) => mr.route.name === clickedRoute.name,
|
|
210
|
+
);
|
|
211
|
+
if (!merged) return;
|
|
212
|
+
|
|
213
|
+
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
214
|
+
|
|
215
|
+
if (merged.source === 'hardcoded') {
|
|
216
|
+
const menuOptions = merged.enabled
|
|
217
|
+
? [
|
|
218
|
+
{
|
|
219
|
+
name: 'Disable Route',
|
|
220
|
+
iconName: 'lucide:pause',
|
|
221
|
+
action: async (modalArg: any) => {
|
|
222
|
+
await appstate.routeManagementStatePart.dispatchAction(
|
|
223
|
+
appstate.setRouteOverrideAction,
|
|
224
|
+
{ routeName: merged.route.name!, enabled: false },
|
|
225
|
+
);
|
|
226
|
+
await modalArg.destroy();
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: 'Close',
|
|
231
|
+
iconName: 'lucide:x',
|
|
232
|
+
action: async (modalArg: any) => await modalArg.destroy(),
|
|
233
|
+
},
|
|
234
|
+
]
|
|
235
|
+
: [
|
|
236
|
+
{
|
|
237
|
+
name: 'Enable Route',
|
|
238
|
+
iconName: 'lucide:play',
|
|
239
|
+
action: async (modalArg: any) => {
|
|
240
|
+
await appstate.routeManagementStatePart.dispatchAction(
|
|
241
|
+
appstate.setRouteOverrideAction,
|
|
242
|
+
{ routeName: merged.route.name!, enabled: true },
|
|
243
|
+
);
|
|
244
|
+
await modalArg.destroy();
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: 'Remove Override',
|
|
249
|
+
iconName: 'lucide:undo',
|
|
250
|
+
action: async (modalArg: any) => {
|
|
251
|
+
await appstate.routeManagementStatePart.dispatchAction(
|
|
252
|
+
appstate.removeRouteOverrideAction,
|
|
253
|
+
merged.route.name!,
|
|
254
|
+
);
|
|
255
|
+
await modalArg.destroy();
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: 'Close',
|
|
260
|
+
iconName: 'lucide:x',
|
|
261
|
+
action: async (modalArg: any) => await modalArg.destroy(),
|
|
262
|
+
},
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
await DeesModal.createAndShow({
|
|
266
|
+
heading: `Route: ${merged.route.name}`,
|
|
267
|
+
content: html`
|
|
268
|
+
<div style="color: #ccc; padding: 8px 0;">
|
|
269
|
+
<p>Source: <strong style="color: #88f;">hardcoded</strong></p>
|
|
270
|
+
<p>Status: <strong>${merged.enabled ? 'Enabled' : 'Disabled (overridden)'}</strong></p>
|
|
271
|
+
<p style="color: #888; font-size: 13px;">Hardcoded routes cannot be edited or deleted, but they can be disabled via an override.</p>
|
|
272
|
+
</div>
|
|
273
|
+
`,
|
|
274
|
+
menuOptions,
|
|
275
|
+
});
|
|
276
|
+
} else {
|
|
277
|
+
// Programmatic route
|
|
278
|
+
await DeesModal.createAndShow({
|
|
279
|
+
heading: `Route: ${merged.route.name}`,
|
|
280
|
+
content: html`
|
|
281
|
+
<div style="color: #ccc; padding: 8px 0;">
|
|
282
|
+
<p>Source: <strong style="color: #0af;">programmatic</strong></p>
|
|
283
|
+
<p>Status: <strong>${merged.enabled ? 'Enabled' : 'Disabled'}</strong></p>
|
|
284
|
+
<p>ID: <code style="color: #888;">${merged.storedRouteId}</code></p>
|
|
285
|
+
</div>
|
|
286
|
+
`,
|
|
287
|
+
menuOptions: [
|
|
288
|
+
{
|
|
289
|
+
name: merged.enabled ? 'Disable' : 'Enable',
|
|
290
|
+
iconName: merged.enabled ? 'lucide:pause' : 'lucide:play',
|
|
291
|
+
action: async (modalArg: any) => {
|
|
292
|
+
await appstate.routeManagementStatePart.dispatchAction(
|
|
293
|
+
appstate.toggleRouteAction,
|
|
294
|
+
{ id: merged.storedRouteId!, enabled: !merged.enabled },
|
|
295
|
+
);
|
|
296
|
+
await modalArg.destroy();
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: 'Delete',
|
|
301
|
+
iconName: 'lucide:trash-2',
|
|
302
|
+
action: async (modalArg: any) => {
|
|
303
|
+
await appstate.routeManagementStatePart.dispatchAction(
|
|
304
|
+
appstate.deleteRouteAction,
|
|
305
|
+
merged.storedRouteId!,
|
|
306
|
+
);
|
|
307
|
+
await modalArg.destroy();
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
name: 'Close',
|
|
312
|
+
iconName: 'lucide:x',
|
|
313
|
+
action: async (modalArg: any) => await modalArg.destroy(),
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private async showCreateRouteDialog() {
|
|
321
|
+
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
322
|
+
|
|
323
|
+
await DeesModal.createAndShow({
|
|
324
|
+
heading: 'Add Programmatic Route',
|
|
325
|
+
content: html`
|
|
326
|
+
<dees-form>
|
|
327
|
+
<dees-input-text .key=${'name'} .label=${'Route Name'} .required=${true}></dees-input-text>
|
|
328
|
+
<dees-input-text .key=${'ports'} .label=${'Ports (comma-separated)'} .required=${true}></dees-input-text>
|
|
329
|
+
<dees-input-text .key=${'domains'} .label=${'Domains (comma-separated, optional)'}></dees-input-text>
|
|
330
|
+
<dees-input-text .key=${'targetHost'} .label=${'Target Host'} .value=${'localhost'} .required=${true}></dees-input-text>
|
|
331
|
+
<dees-input-text .key=${'targetPort'} .label=${'Target Port'} .required=${true}></dees-input-text>
|
|
332
|
+
</dees-form>
|
|
333
|
+
`,
|
|
334
|
+
menuOptions: [
|
|
335
|
+
{
|
|
336
|
+
name: 'Cancel',
|
|
337
|
+
iconName: 'lucide:x',
|
|
338
|
+
action: async (modalArg: any) => await modalArg.destroy(),
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: 'Create',
|
|
342
|
+
iconName: 'lucide:plus',
|
|
343
|
+
action: async (modalArg: any) => {
|
|
344
|
+
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
|
345
|
+
if (!form) return;
|
|
346
|
+
const formData = await form.collectFormData();
|
|
347
|
+
if (!formData.name || !formData.ports) return;
|
|
348
|
+
|
|
349
|
+
const ports = formData.ports.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p));
|
|
350
|
+
const domains = formData.domains
|
|
351
|
+
? formData.domains.split(',').map((d: string) => d.trim()).filter(Boolean)
|
|
352
|
+
: undefined;
|
|
353
|
+
|
|
354
|
+
const route: any = {
|
|
355
|
+
name: formData.name,
|
|
356
|
+
match: {
|
|
357
|
+
ports,
|
|
358
|
+
...(domains && domains.length > 0 ? { domains } : {}),
|
|
359
|
+
},
|
|
360
|
+
action: {
|
|
361
|
+
type: 'forward',
|
|
362
|
+
targets: [
|
|
363
|
+
{
|
|
364
|
+
host: formData.targetHost || 'localhost',
|
|
365
|
+
port: parseInt(formData.targetPort, 10),
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
await appstate.routeManagementStatePart.dispatchAction(
|
|
372
|
+
appstate.createRouteAction,
|
|
373
|
+
{ route },
|
|
374
|
+
);
|
|
375
|
+
await modalArg.destroy();
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private refreshData() {
|
|
383
|
+
appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async firstUpdated() {
|
|
387
|
+
await appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
|
|
388
|
+
}
|
|
389
|
+
}
|
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', 'remoteingress'] as const;
|
|
6
|
+
export const validViews = ['overview', 'network', 'emails', 'logs', 'routes', 'apitokens', 'configuration', 'security', 'certificates', 'remoteingress'] as const;
|
|
7
7
|
|
|
8
8
|
export type TValidView = typeof validViews[number];
|
|
9
9
|
|