fix(routes): support profile and target metadata in route creation and refresh remote ingress routes after config initialization

This commit is contained in:
2026-04-02 17:27:05 +00:00
parent 0577f45ced
commit 55f5465a9a
9 changed files with 121 additions and 45 deletions

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '12.2.3',
version: '12.2.4',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}

View File

@@ -427,6 +427,8 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
if (viewName === 'routes' && currentState.activeView !== 'routes') {
setTimeout(() => {
routeManagementStatePart.dispatchAction(fetchMergedRoutesAction, null);
// Also fetch profiles/targets for the Create Route dropdowns
profilesTargetsStatePart.dispatchAction(fetchProfilesAndTargetsAction, null);
}, 100);
}
@@ -1413,6 +1415,7 @@ export const fetchMergedRoutesAction = routeManagementStatePart.createAction(asy
export const createRouteAction = routeManagementStatePart.createAction<{
route: any;
enabled?: boolean;
metadata?: any;
}>(async (statePartArg, dataArg, actionContext): Promise<IRouteManagementState> => {
const context = getActionContext();
const currentState = statePartArg.getState()!;
@@ -1426,6 +1429,7 @@ export const createRouteAction = routeManagementStatePart.createAction<{
identity: context.identity!,
route: dataArg.route,
enabled: dataArg.enabled,
metadata: dataArg.metadata,
});
return await actionContext!.dispatch(fetchMergedRoutesAction, null);

View File

@@ -24,6 +24,14 @@ export class OpsViewRoutes extends DeesElement {
lastUpdated: 0,
};
@state() accessor profilesTargetsState: appstate.IProfilesTargetsState = {
profiles: [],
targets: [],
isLoading: false,
error: null,
lastUpdated: 0,
};
constructor() {
super();
const sub = appstate.routeManagementStatePart
@@ -33,6 +41,13 @@ export class OpsViewRoutes extends DeesElement {
});
this.rxSubscriptions.push(sub);
const ptSub = appstate.profilesTargetsStatePart
.select((s) => s)
.subscribe((ptState) => {
this.profilesTargetsState = ptState;
});
this.rxSubscriptions.push(ptSub);
// Re-fetch routes when user logs in (fixes race condition where
// the view is created before authentication completes)
const loginSub = appstate.loginStatePart
@@ -40,6 +55,7 @@ export class OpsViewRoutes extends DeesElement {
.subscribe((isLoggedIn) => {
if (isLoggedIn) {
appstate.routeManagementStatePart.dispatchAction(appstate.fetchMergedRoutesAction, null);
appstate.profilesTargetsStatePart.dispatchAction(appstate.fetchProfilesAndTargetsAction, null);
}
});
this.rxSubscriptions.push(loginSub);
@@ -145,6 +161,7 @@ export class OpsViewRoutes extends DeesElement {
enabled: mr.enabled,
tags,
id: mr.storedRouteId || mr.route.name || undefined,
metadata: mr.metadata,
};
});
@@ -275,6 +292,7 @@ export class OpsViewRoutes extends DeesElement {
});
} else {
// Programmatic route
const meta = merged.metadata;
await DeesModal.createAndShow({
heading: `Route: ${merged.route.name}`,
content: html`
@@ -282,6 +300,8 @@ export class OpsViewRoutes extends DeesElement {
<p>Source: <strong style="color: #0af;">programmatic</strong></p>
<p>Status: <strong>${merged.enabled ? 'Enabled' : 'Disabled'}</strong></p>
<p>ID: <code style="color: #888;">${merged.storedRouteId}</code></p>
${meta?.securityProfileName ? html`<p>Security Profile: <strong style="color: #a78bfa;">${meta.securityProfileName}</strong></p>` : ''}
${meta?.networkTargetName ? html`<p>Network Target: <strong style="color: #a78bfa;">${meta.networkTargetName}</strong></p>` : ''}
</div>
`,
menuOptions: [
@@ -319,6 +339,24 @@ export class OpsViewRoutes extends DeesElement {
private async showCreateRouteDialog() {
const { DeesModal } = await import('@design.estate/dees-catalog');
const profiles = this.profilesTargetsState.profiles;
const targets = this.profilesTargetsState.targets;
// Build dropdown options for profiles and targets
const profileOptions = [
{ key: '', option: '(none — inline security)' },
...profiles.map((p) => ({
key: p.id,
option: `${p.name}${p.description ? ' — ' + p.description : ''}`,
})),
];
const targetOptions = [
{ key: '', option: '(none — inline target)' },
...targets.map((t) => ({
key: t.id,
option: `${t.name} (${Array.isArray(t.host) ? t.host.join(',') : t.host}:${t.port})`,
})),
];
await DeesModal.createAndShow({
heading: 'Add Programmatic Route',
@@ -327,8 +365,10 @@ export class OpsViewRoutes extends DeesElement {
<dees-input-text .key=${'name'} .label=${'Route Name'} .required=${true}></dees-input-text>
<dees-input-text .key=${'ports'} .label=${'Ports (comma-separated)'} .required=${true}></dees-input-text>
<dees-input-text .key=${'domains'} .label=${'Domains (comma-separated, optional)'}></dees-input-text>
<dees-input-text .key=${'targetHost'} .label=${'Target Host'} .value=${'localhost'} .required=${true}></dees-input-text>
<dees-input-text .key=${'targetPort'} .label=${'Target Port'} .required=${true}></dees-input-text>
<dees-input-dropdown .key=${'securityProfileRef'} .label=${'Security Profile'} .options=${profileOptions} .selectedKey=${''}></dees-input-dropdown>
<dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions} .selectedKey=${''}></dees-input-dropdown>
<dees-input-text .key=${'targetHost'} .label=${'Target Host (if no target selected)'} .value=${'localhost'}></dees-input-text>
<dees-input-text .key=${'targetPort'} .label=${'Target Port (if no target selected)'}></dees-input-text>
</dees-form>
`,
menuOptions: [
@@ -362,15 +402,27 @@ export class OpsViewRoutes extends DeesElement {
targets: [
{
host: formData.targetHost || 'localhost',
port: parseInt(formData.targetPort, 10),
port: parseInt(formData.targetPort, 10) || 443,
},
],
},
};
// Build metadata if profile/target selected
const metadata: any = {};
if (formData.securityProfileRef) {
metadata.securityProfileRef = formData.securityProfileRef;
}
if (formData.networkTargetRef) {
metadata.networkTargetRef = formData.networkTargetRef;
}
await appstate.routeManagementStatePart.dispatchAction(
appstate.createRouteAction,
{ route },
{
route,
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
},
);
await modalArg.destroy();
},

View File

@@ -131,8 +131,8 @@ export class OpsViewSecurityProfiles extends DeesElement {
<dees-form>
<dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
<dees-input-text .key=${'description'} .label=${'Description'}></dees-input-text>
<dees-input-text .key=${'ipAllowList'} .label=${'IP Allow List (comma-separated)'}></dees-input-text>
<dees-input-text .key=${'ipBlockList'} .label=${'IP Block List (comma-separated)'}></dees-input-text>
<dees-input-list .key=${'ipAllowList'} .label=${'IP Allow List'} .placeholder=${'Add IP or CIDR...'}></dees-input-list>
<dees-input-list .key=${'ipBlockList'} .label=${'IP Block List'} .placeholder=${'Add IP or CIDR...'}></dees-input-list>
<dees-input-text .key=${'maxConnections'} .label=${'Max Connections'}></dees-input-text>
</dees-form>
`,
@@ -142,20 +142,16 @@ export class OpsViewSecurityProfiles extends DeesElement {
action: async (modalArg: any) => {
const form = modalArg.shadowRoot!.querySelector('dees-form');
const data = await form.collectFormData();
const ipAllowList = data.ipAllowList
? String(data.ipAllowList).split(',').map((s: string) => s.trim()).filter(Boolean)
: undefined;
const ipBlockList = data.ipBlockList
? String(data.ipBlockList).split(',').map((s: string) => s.trim()).filter(Boolean)
: undefined;
const ipAllowList: string[] = Array.isArray(data.ipAllowList) ? data.ipAllowList : [];
const ipBlockList: string[] = Array.isArray(data.ipBlockList) ? data.ipBlockList : [];
const maxConnections = data.maxConnections ? parseInt(String(data.maxConnections)) : undefined;
await appstate.profilesTargetsStatePart.dispatchAction(appstate.createProfileAction, {
name: String(data.name),
description: data.description ? String(data.description) : undefined,
security: {
...(ipAllowList ? { ipAllowList } : {}),
...(ipBlockList ? { ipBlockList } : {}),
...(ipAllowList.length > 0 ? { ipAllowList } : {}),
...(ipBlockList.length > 0 ? { ipBlockList } : {}),
...(maxConnections ? { maxConnections } : {}),
},
});
@@ -175,8 +171,8 @@ export class OpsViewSecurityProfiles extends DeesElement {
<dees-form>
<dees-input-text .key=${'name'} .label=${'Name'} .value=${profile.name}></dees-input-text>
<dees-input-text .key=${'description'} .label=${'Description'} .value=${profile.description || ''}></dees-input-text>
<dees-input-text .key=${'ipAllowList'} .label=${'IP Allow List (comma-separated)'} .value=${(profile.security?.ipAllowList || []).join(', ')}></dees-input-text>
<dees-input-text .key=${'ipBlockList'} .label=${'IP Block List (comma-separated)'} .value=${(profile.security?.ipBlockList || []).join(', ')}></dees-input-text>
<dees-input-list .key=${'ipAllowList'} .label=${'IP Allow List'} .placeholder=${'Add IP or CIDR...'} .value=${profile.security?.ipAllowList || []}></dees-input-list>
<dees-input-list .key=${'ipBlockList'} .label=${'IP Block List'} .placeholder=${'Add IP or CIDR...'} .value=${profile.security?.ipBlockList || []}></dees-input-list>
<dees-input-text .key=${'maxConnections'} .label=${'Max Connections'} .value=${String(profile.security?.maxConnections || '')}></dees-input-text>
</dees-form>
`,
@@ -186,12 +182,8 @@ export class OpsViewSecurityProfiles extends DeesElement {
action: async (modalArg: any) => {
const form = modalArg.shadowRoot!.querySelector('dees-form');
const data = await form.collectFormData();
const ipAllowList = data.ipAllowList
? String(data.ipAllowList).split(',').map((s: string) => s.trim()).filter(Boolean)
: [];
const ipBlockList = data.ipBlockList
? String(data.ipBlockList).split(',').map((s: string) => s.trim()).filter(Boolean)
: [];
const ipAllowList: string[] = Array.isArray(data.ipAllowList) ? data.ipAllowList : [];
const ipBlockList: string[] = Array.isArray(data.ipBlockList) ? data.ipBlockList : [];
const maxConnections = data.maxConnections ? parseInt(String(data.maxConnections)) : undefined;
await appstate.profilesTargetsStatePart.dispatchAction(appstate.updateProfileAction, {