Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f25ca4095 | |||
| 2891e5d3ee | |||
| 152110c877 | |||
| d780e02928 |
16
changelog.md
16
changelog.md
@@ -1,5 +1,21 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-04-17 - 13.20.0 - feat(routes)
|
||||
add remote ingress controls and preserve-port targeting for route configuration
|
||||
|
||||
- Allow route updates to remove optional top-level properties by treating null values like remoteIngress as explicit clears.
|
||||
- Add route form support for preserving the matched incoming port when forwarding to backend targets.
|
||||
- Add remote ingress enablement and edge filter controls to route create/edit views.
|
||||
- Cover remoteIngress removal behavior with a runtime route manager test.
|
||||
|
||||
## 2026-04-16 - 13.19.1 - fix(routes)
|
||||
preserve inline target ports when clearing network target references
|
||||
|
||||
- Normalize route metadata so empty reference fields are removed instead of persisted.
|
||||
- Allow the routes UI to clear source profile and network target references explicitly during edits.
|
||||
- Disable inline target host and port inputs when a network target is selected and validate target ports when using manual targets.
|
||||
- Add runtime route tests covering removal of a network target reference while keeping the edited inline target port.
|
||||
|
||||
## 2026-04-15 - 13.19.0 - feat(routes,email)
|
||||
persist system DNS routes with runtime hydration and add reusable email ops DNS helpers
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@serve.zone/dcrouter",
|
||||
"private": false,
|
||||
"version": "13.19.0",
|
||||
"version": "13.20.0",
|
||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { DcRouter } from '../ts/classes.dcrouter.js';
|
||||
import { RouteConfigManager } from '../ts/config/index.js';
|
||||
import { ReferenceResolver, RouteConfigManager } from '../ts/config/index.js';
|
||||
import { DcRouterDb, DomainDoc, RouteDoc } from '../ts/db/index.js';
|
||||
import { DnsManager } from '../ts/dns/manager.dns.js';
|
||||
import { logger } from '../ts/logger.js';
|
||||
@@ -204,6 +204,130 @@ tap.test('RouteConfigManager only allows toggling system routes', async () => {
|
||||
expect((await RouteDoc.findById(systemRoute!.id))?.enabled).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('RouteConfigManager clears a network target ref and keeps the edited inline target port', async () => {
|
||||
await testDbPromise;
|
||||
await clearTestState();
|
||||
|
||||
const appliedRoutes: any[][] = [];
|
||||
const smartProxy = {
|
||||
updateRoutes: async (routes: any[]) => {
|
||||
appliedRoutes.push(routes);
|
||||
},
|
||||
};
|
||||
|
||||
const resolver = new ReferenceResolver();
|
||||
(resolver as any).targets.set('target-1', {
|
||||
id: 'target-1',
|
||||
name: 'SSH TARGET',
|
||||
host: '10.0.0.5',
|
||||
port: 443,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
createdBy: 'test',
|
||||
});
|
||||
|
||||
const routeManager = new RouteConfigManager(
|
||||
() => smartProxy as any,
|
||||
undefined,
|
||||
undefined,
|
||||
resolver,
|
||||
);
|
||||
await routeManager.initialize([], [], []);
|
||||
|
||||
const routeId = await routeManager.createRoute(
|
||||
{
|
||||
name: 'ssh-route',
|
||||
match: { ports: [22] },
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [{ host: '127.0.0.1', port: 22 }],
|
||||
},
|
||||
} as any,
|
||||
'test-user',
|
||||
true,
|
||||
{ networkTargetRef: 'target-1' },
|
||||
);
|
||||
|
||||
expect((await RouteDoc.findById(routeId))?.route.action.targets?.[0].port).toEqual(443);
|
||||
expect((await RouteDoc.findById(routeId))?.metadata?.networkTargetRef).toEqual('target-1');
|
||||
|
||||
const updateResult = await routeManager.updateRoute(routeId, {
|
||||
route: {
|
||||
action: {
|
||||
targets: [{ host: '127.0.0.1', port: 29424 }],
|
||||
},
|
||||
} as any,
|
||||
metadata: {
|
||||
networkTargetRef: '',
|
||||
networkTargetName: '',
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(updateResult.success).toEqual(true);
|
||||
|
||||
const storedRoute = await RouteDoc.findById(routeId);
|
||||
expect(storedRoute?.route.action.targets?.[0].host).toEqual('127.0.0.1');
|
||||
expect(storedRoute?.route.action.targets?.[0].port).toEqual(29424);
|
||||
expect(storedRoute?.metadata?.networkTargetRef).toBeUndefined();
|
||||
expect(storedRoute?.metadata?.networkTargetName).toBeUndefined();
|
||||
|
||||
const mergedRoute = routeManager.getMergedRoutes().routes.find((route) => route.id === routeId);
|
||||
expect(mergedRoute?.route.action.targets?.[0].port).toEqual(29424);
|
||||
expect(mergedRoute?.metadata?.networkTargetRef).toBeUndefined();
|
||||
expect(mergedRoute?.metadata?.networkTargetName).toBeUndefined();
|
||||
|
||||
expect(appliedRoutes[appliedRoutes.length - 1][0].action.targets[0].port).toEqual(29424);
|
||||
});
|
||||
|
||||
tap.test('RouteConfigManager clears remote ingress config when route patch sets it to null', async () => {
|
||||
await testDbPromise;
|
||||
await clearTestState();
|
||||
|
||||
const appliedRoutes: any[][] = [];
|
||||
const smartProxy = {
|
||||
updateRoutes: async (routes: any[]) => {
|
||||
appliedRoutes.push(routes);
|
||||
},
|
||||
};
|
||||
|
||||
const routeManager = new RouteConfigManager(
|
||||
() => smartProxy as any,
|
||||
);
|
||||
await routeManager.initialize([], [], []);
|
||||
|
||||
const routeId = await routeManager.createRoute(
|
||||
{
|
||||
name: 'remote-ingress-route',
|
||||
match: { ports: [443], domains: ['app.example.com'] },
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [{ host: '127.0.0.1', port: 8443 }],
|
||||
},
|
||||
remoteIngress: {
|
||||
enabled: true,
|
||||
edgeFilter: ['edge-a', 'blue'],
|
||||
},
|
||||
} as any,
|
||||
'test-user',
|
||||
);
|
||||
|
||||
const updateResult = await routeManager.updateRoute(routeId, {
|
||||
route: {
|
||||
remoteIngress: null,
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(updateResult.success).toEqual(true);
|
||||
|
||||
const storedRoute = await RouteDoc.findById(routeId);
|
||||
expect(storedRoute?.route.remoteIngress).toBeUndefined();
|
||||
|
||||
const mergedRoute = routeManager.getMergedRoutes().routes.find((route) => route.id === routeId);
|
||||
expect(mergedRoute?.route.remoteIngress).toBeUndefined();
|
||||
|
||||
expect(appliedRoutes[appliedRoutes.length - 1][0].remoteIngress).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('DnsManager warning keeps dnsNsDomains in scope', async () => {
|
||||
await testDbPromise;
|
||||
await clearTestState();
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '13.19.0',
|
||||
version: '13.20.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -133,11 +133,11 @@ export class RouteConfigManager {
|
||||
}
|
||||
|
||||
// Resolve references if metadata has refs and resolver is available
|
||||
let resolvedMetadata = metadata;
|
||||
if (metadata && this.referenceResolver) {
|
||||
const resolved = this.referenceResolver.resolveRoute(route, metadata);
|
||||
let resolvedMetadata = this.normalizeRouteMetadata(metadata);
|
||||
if (resolvedMetadata && this.referenceResolver) {
|
||||
const resolved = this.referenceResolver.resolveRoute(route, resolvedMetadata);
|
||||
route = resolved.route;
|
||||
resolvedMetadata = resolved.metadata;
|
||||
resolvedMetadata = this.normalizeRouteMetadata(resolved.metadata);
|
||||
}
|
||||
|
||||
const stored: IRoute = {
|
||||
@@ -192,20 +192,32 @@ export class RouteConfigManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
stored.route = { ...stored.route, ...patch.route, action: mergedAction } as IDcRouterRouteConfig;
|
||||
const mergedRoute = { ...stored.route, ...patch.route, action: mergedAction } as IDcRouterRouteConfig;
|
||||
|
||||
// Handle explicit null to remove optional top-level route properties (e.g., remoteIngress: null)
|
||||
for (const [key, val] of Object.entries(patch.route)) {
|
||||
if (val === null && key !== 'action' && key !== 'match') {
|
||||
delete (mergedRoute as any)[key];
|
||||
}
|
||||
}
|
||||
|
||||
stored.route = mergedRoute;
|
||||
}
|
||||
if (patch.enabled !== undefined) {
|
||||
stored.enabled = patch.enabled;
|
||||
}
|
||||
if (patch.metadata !== undefined) {
|
||||
stored.metadata = { ...stored.metadata, ...patch.metadata };
|
||||
stored.metadata = this.normalizeRouteMetadata({
|
||||
...stored.metadata,
|
||||
...patch.metadata,
|
||||
});
|
||||
}
|
||||
|
||||
// Re-resolve if metadata refs exist and resolver is available
|
||||
if (stored.metadata && this.referenceResolver) {
|
||||
const resolved = this.referenceResolver.resolveRoute(stored.route, stored.metadata);
|
||||
stored.route = resolved.route;
|
||||
stored.metadata = resolved.metadata;
|
||||
stored.metadata = this.normalizeRouteMetadata(resolved.metadata);
|
||||
}
|
||||
|
||||
stored.updatedAt = Date.now();
|
||||
@@ -368,7 +380,7 @@ export class RouteConfigManager {
|
||||
createdBy: doc.createdBy,
|
||||
origin: doc.origin || 'api',
|
||||
systemKey: doc.systemKey,
|
||||
metadata: doc.metadata,
|
||||
metadata: this.normalizeRouteMetadata(doc.metadata),
|
||||
};
|
||||
|
||||
this.routes.set(doc.id, storedRoute);
|
||||
@@ -404,6 +416,46 @@ export class RouteConfigManager {
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeRouteMetadata(metadata?: Partial<IRouteMetadata>): IRouteMetadata | undefined {
|
||||
if (!metadata) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const normalizeString = (value?: string): string | undefined => {
|
||||
if (typeof value !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : undefined;
|
||||
};
|
||||
|
||||
const normalized: IRouteMetadata = {
|
||||
sourceProfileRef: normalizeString(metadata.sourceProfileRef),
|
||||
networkTargetRef: normalizeString(metadata.networkTargetRef),
|
||||
sourceProfileName: normalizeString(metadata.sourceProfileName),
|
||||
networkTargetName: normalizeString(metadata.networkTargetName),
|
||||
lastResolvedAt: typeof metadata.lastResolvedAt === 'number' && Number.isFinite(metadata.lastResolvedAt)
|
||||
? metadata.lastResolvedAt
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (!normalized.sourceProfileRef) {
|
||||
normalized.sourceProfileName = undefined;
|
||||
}
|
||||
if (!normalized.networkTargetRef) {
|
||||
normalized.networkTargetName = undefined;
|
||||
}
|
||||
if (!normalized.sourceProfileRef && !normalized.networkTargetRef) {
|
||||
normalized.lastResolvedAt = undefined;
|
||||
}
|
||||
|
||||
if (Object.values(normalized).every((value) => value === undefined)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Private: warnings
|
||||
// =========================================================================
|
||||
@@ -446,7 +498,7 @@ export class RouteConfigManager {
|
||||
|
||||
const resolved = this.referenceResolver.resolveRoute(stored.route, stored.metadata);
|
||||
stored.route = resolved.route;
|
||||
stored.metadata = resolved.metadata;
|
||||
stored.metadata = this.normalizeRouteMetadata(resolved.metadata);
|
||||
stored.updatedAt = Date.now();
|
||||
await this.persistRoute(stored);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '13.19.0',
|
||||
version: '13.20.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -15,16 +15,90 @@ import {
|
||||
|
||||
// TLS dropdown options shared by create and edit dialogs
|
||||
const tlsModeOptions = [
|
||||
{ key: 'none', option: '(none — no TLS)' },
|
||||
{ key: 'passthrough', option: 'Passthrough' },
|
||||
{ key: 'terminate', option: 'Terminate' },
|
||||
{ key: 'terminate-and-reencrypt', option: 'Terminate & Re-encrypt' },
|
||||
{ key: 'none', option: '(none — plain TCP/HTTP, use for SSH)' },
|
||||
{ key: 'passthrough', option: 'Passthrough (TLS only)' },
|
||||
{ key: 'terminate', option: 'Terminate TLS' },
|
||||
{ key: 'terminate-and-reencrypt', option: 'Terminate & Re-encrypt TLS' },
|
||||
];
|
||||
const tlsCertOptions = [
|
||||
{ key: 'auto', option: 'Auto (ACME/Let\'s Encrypt)' },
|
||||
{ key: 'custom', option: 'Custom certificate' },
|
||||
];
|
||||
|
||||
function getDropdownKey(value: any): string {
|
||||
return typeof value === 'string' ? value : value?.key || '';
|
||||
}
|
||||
|
||||
function parseTargetPort(value: any): number | undefined {
|
||||
const parsed = typeof value === 'number'
|
||||
? value
|
||||
: typeof value === 'string'
|
||||
? parseInt(value.trim(), 10)
|
||||
: Number.NaN;
|
||||
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
||||
return undefined;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function getRouteTargetInputs(formEl: any) {
|
||||
const textInputs = Array.from(formEl.querySelectorAll('dees-input-text')) as any[];
|
||||
const checkboxInputs = Array.from(formEl.querySelectorAll('dees-input-checkbox')) as any[];
|
||||
return {
|
||||
hostInput: textInputs.find((input) => input.key === 'targetHost'),
|
||||
portInput: textInputs.find((input) => input.key === 'targetPort'),
|
||||
preservePortInput: checkboxInputs.find((input) => input.key === 'preserveMatchPort'),
|
||||
};
|
||||
}
|
||||
|
||||
function setupTargetInputState(formEl: any) {
|
||||
const updateState = async () => {
|
||||
const data = await formEl.collectFormData();
|
||||
const contentEl = formEl.closest('.content') || formEl.parentElement;
|
||||
const usesNetworkTarget = !!getDropdownKey(data.networkTargetRef);
|
||||
const preserveMatchPort = !usesNetworkTarget && Boolean(data.preserveMatchPort);
|
||||
const { hostInput, portInput, preservePortInput } = getRouteTargetInputs(formEl);
|
||||
const hostDescription = usesNetworkTarget
|
||||
? 'Controlled by the selected network target'
|
||||
: 'Used when no network target is selected';
|
||||
const portDescription = usesNetworkTarget
|
||||
? 'Controlled by the selected network target'
|
||||
: preserveMatchPort
|
||||
? 'Forwarded to the backend on the same port the client matched'
|
||||
: 'Used when no network target is selected';
|
||||
|
||||
if (hostInput) {
|
||||
hostInput.disabled = usesNetworkTarget;
|
||||
hostInput.required = !usesNetworkTarget;
|
||||
hostInput.description = hostDescription;
|
||||
}
|
||||
if (portInput) {
|
||||
portInput.disabled = usesNetworkTarget || preserveMatchPort;
|
||||
portInput.required = !usesNetworkTarget && !preserveMatchPort;
|
||||
portInput.description = portDescription;
|
||||
}
|
||||
if (preservePortInput) {
|
||||
preservePortInput.disabled = usesNetworkTarget;
|
||||
preservePortInput.description = usesNetworkTarget
|
||||
? 'Unavailable when a network target is selected'
|
||||
: 'Forward to the backend using the same port that matched this route';
|
||||
if (usesNetworkTarget) {
|
||||
preservePortInput.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const remoteIngressGroup = contentEl?.querySelector('.remoteIngressGroup') as HTMLElement | null;
|
||||
if (remoteIngressGroup) {
|
||||
remoteIngressGroup.style.display = Boolean(data.remoteIngressEnabled) ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
await formEl.updateRequiredStatus?.();
|
||||
};
|
||||
|
||||
formEl.changeSubject.subscribe(() => updateState());
|
||||
updateState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle TLS form field visibility based on selected TLS mode and certificate type.
|
||||
*/
|
||||
@@ -411,10 +485,13 @@ export class OpsViewRoutes extends DeesElement {
|
||||
? (Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains])
|
||||
: [];
|
||||
const firstTarget = route.action.targets?.[0];
|
||||
const currentPreserveMatchPort = firstTarget?.port === 'preserve';
|
||||
const currentTargetHost = firstTarget
|
||||
? (Array.isArray(firstTarget.host) ? firstTarget.host[0] : firstTarget.host)
|
||||
: '';
|
||||
const currentTargetPort = firstTarget?.port != null ? String(firstTarget.port) : '';
|
||||
const currentTargetPort = typeof firstTarget?.port === 'number' ? String(firstTarget.port) : '';
|
||||
const currentRemoteIngressEnabled = route.remoteIngress?.enabled === true;
|
||||
const currentEdgeFilter = route.remoteIngress?.edgeFilter || [];
|
||||
|
||||
// Compute current TLS state for pre-population
|
||||
const currentTls = (route.action as any).tls;
|
||||
@@ -439,6 +516,11 @@ export class OpsViewRoutes extends DeesElement {
|
||||
<dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions} .selectedOption=${targetOptions.find((o) => o.key === (merged.metadata?.networkTargetRef || '')) || null}></dees-input-dropdown>
|
||||
<dees-input-text .key=${'targetHost'} .label=${'Target Host'} .description=${'Used when no network target is selected'} .value=${currentTargetHost}></dees-input-text>
|
||||
<dees-input-text .key=${'targetPort'} .label=${'Target Port'} .description=${'Used when no network target is selected'} .value=${currentTargetPort}></dees-input-text>
|
||||
<dees-input-checkbox .key=${'preserveMatchPort'} .label=${'Preserve incoming port'} .value=${currentPreserveMatchPort}></dees-input-checkbox>
|
||||
<dees-input-checkbox .key=${'remoteIngressEnabled'} .label=${'Enable Remote Ingress'} .value=${currentRemoteIngressEnabled}></dees-input-checkbox>
|
||||
<div class="remoteIngressGroup" style="display: ${currentRemoteIngressEnabled ? 'flex' : 'none'}; flex-direction: column; gap: 16px;">
|
||||
<dees-input-list .key=${'remoteIngressEdgeFilter'} .label=${'Edge Filter'} .description=${'Optional edge IDs or tags. Leave empty to allow all edges.'} .placeholder=${'Add edge ID or tag...'} .value=${currentEdgeFilter}></dees-input-list>
|
||||
</div>
|
||||
<dees-input-dropdown .key=${'tlsMode'} .label=${'TLS Mode'} .options=${tlsModeOptions} .selectedOption=${tlsModeOptions.find((o) => o.key === currentTlsMode) || tlsModeOptions[0]}></dees-input-dropdown>
|
||||
<div class="tlsCertificateGroup" style="display: ${needsCert ? 'flex' : 'none'}; flex-direction: column; gap: 16px;">
|
||||
<dees-input-dropdown .key=${'tlsCertificate'} .label=${'Certificate'} .options=${tlsCertOptions} .selectedOption=${tlsCertOptions.find((o) => o.key === currentTlsCert) || tlsCertOptions[0]}></dees-input-dropdown>
|
||||
@@ -470,6 +552,24 @@ export class OpsViewRoutes extends DeesElement {
|
||||
: [];
|
||||
const priority = formData.priority ? parseInt(formData.priority, 10) : undefined;
|
||||
|
||||
const profileKey = getDropdownKey(formData.sourceProfileRef);
|
||||
const targetKey = getDropdownKey(formData.networkTargetRef);
|
||||
const preserveMatchPort = !targetKey && Boolean(formData.preserveMatchPort);
|
||||
const targetPort = preserveMatchPort
|
||||
? 'preserve'
|
||||
: parseTargetPort(formData.targetPort)
|
||||
?? (targetKey ? parseTargetPort(currentTargetPort) ?? ports[0] : undefined);
|
||||
|
||||
if (targetPort === undefined) {
|
||||
alert('Target Port must be a valid port number when no network target is selected.');
|
||||
return;
|
||||
}
|
||||
|
||||
const remoteIngressEnabled = Boolean(formData.remoteIngressEnabled);
|
||||
const remoteIngressEdgeFilter: string[] = Array.isArray(formData.remoteIngressEdgeFilter)
|
||||
? formData.remoteIngressEdgeFilter.filter(Boolean)
|
||||
: [];
|
||||
|
||||
const updatedRoute: any = {
|
||||
name: formData.name,
|
||||
match: {
|
||||
@@ -480,11 +580,17 @@ export class OpsViewRoutes extends DeesElement {
|
||||
type: 'forward',
|
||||
targets: [
|
||||
{
|
||||
host: formData.targetHost || 'localhost',
|
||||
port: parseInt(formData.targetPort, 10) || 443,
|
||||
host: formData.targetHost || currentTargetHost || 'localhost',
|
||||
port: targetPort,
|
||||
},
|
||||
],
|
||||
},
|
||||
remoteIngress: remoteIngressEnabled
|
||||
? {
|
||||
enabled: true,
|
||||
...(remoteIngressEdgeFilter.length > 0 ? { edgeFilter: remoteIngressEdgeFilter } : {}),
|
||||
}
|
||||
: null,
|
||||
...(priority != null && !isNaN(priority) ? { priority } : {}),
|
||||
};
|
||||
|
||||
@@ -508,15 +614,17 @@ export class OpsViewRoutes extends DeesElement {
|
||||
}
|
||||
|
||||
const metadata: any = {};
|
||||
const profileRefValue = formData.sourceProfileRef as any;
|
||||
const profileKey = typeof profileRefValue === 'string' ? profileRefValue : profileRefValue?.key;
|
||||
if (profileKey) {
|
||||
metadata.sourceProfileRef = profileKey;
|
||||
} else if (merged.metadata?.sourceProfileRef) {
|
||||
metadata.sourceProfileRef = '';
|
||||
metadata.sourceProfileName = '';
|
||||
}
|
||||
const targetRefValue = formData.networkTargetRef as any;
|
||||
const targetKey = typeof targetRefValue === 'string' ? targetRefValue : targetRefValue?.key;
|
||||
if (targetKey) {
|
||||
metadata.networkTargetRef = targetKey;
|
||||
} else if (merged.metadata?.networkTargetRef) {
|
||||
metadata.networkTargetRef = '';
|
||||
metadata.networkTargetName = '';
|
||||
}
|
||||
|
||||
await appstate.routeManagementStatePart.dispatchAction(
|
||||
@@ -537,6 +645,7 @@ export class OpsViewRoutes extends DeesElement {
|
||||
if (editForm) {
|
||||
await editForm.updateComplete;
|
||||
setupTlsVisibility(editForm);
|
||||
setupTargetInputState(editForm);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -573,6 +682,11 @@ export class OpsViewRoutes extends DeesElement {
|
||||
<dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions}></dees-input-dropdown>
|
||||
<dees-input-text .key=${'targetHost'} .label=${'Target Host'} .description=${'Used when no network target is selected'} .value=${'localhost'}></dees-input-text>
|
||||
<dees-input-text .key=${'targetPort'} .label=${'Target Port'} .description=${'Used when no network target is selected'}></dees-input-text>
|
||||
<dees-input-checkbox .key=${'preserveMatchPort'} .label=${'Preserve incoming port'} .value=${false}></dees-input-checkbox>
|
||||
<dees-input-checkbox .key=${'remoteIngressEnabled'} .label=${'Enable Remote Ingress'} .value=${false}></dees-input-checkbox>
|
||||
<div class="remoteIngressGroup" style="display: none; flex-direction: column; gap: 16px;">
|
||||
<dees-input-list .key=${'remoteIngressEdgeFilter'} .label=${'Edge Filter'} .description=${'Optional edge IDs or tags. Leave empty to allow all edges.'} .placeholder=${'Add edge ID or tag...'}></dees-input-list>
|
||||
</div>
|
||||
<dees-input-dropdown .key=${'tlsMode'} .label=${'TLS Mode'} .options=${tlsModeOptions} .selectedOption=${tlsModeOptions[0]}></dees-input-dropdown>
|
||||
<div class="tlsCertificateGroup" style="display: none; flex-direction: column; gap: 16px;">
|
||||
<dees-input-dropdown .key=${'tlsCertificate'} .label=${'Certificate'} .options=${tlsCertOptions} .selectedOption=${tlsCertOptions[0]}></dees-input-dropdown>
|
||||
@@ -604,6 +718,24 @@ export class OpsViewRoutes extends DeesElement {
|
||||
: [];
|
||||
const priority = formData.priority ? parseInt(formData.priority, 10) : undefined;
|
||||
|
||||
const profileKey = getDropdownKey(formData.sourceProfileRef);
|
||||
const targetKey = getDropdownKey(formData.networkTargetRef);
|
||||
const preserveMatchPort = !targetKey && Boolean(formData.preserveMatchPort);
|
||||
const targetPort = preserveMatchPort
|
||||
? 'preserve'
|
||||
: parseTargetPort(formData.targetPort)
|
||||
?? (targetKey ? ports[0] : undefined);
|
||||
|
||||
if (targetPort === undefined) {
|
||||
alert('Target Port must be a valid port number when no network target is selected.');
|
||||
return;
|
||||
}
|
||||
|
||||
const remoteIngressEnabled = Boolean(formData.remoteIngressEnabled);
|
||||
const remoteIngressEdgeFilter: string[] = Array.isArray(formData.remoteIngressEdgeFilter)
|
||||
? formData.remoteIngressEdgeFilter.filter(Boolean)
|
||||
: [];
|
||||
|
||||
const route: any = {
|
||||
name: formData.name,
|
||||
match: {
|
||||
@@ -615,10 +747,18 @@ export class OpsViewRoutes extends DeesElement {
|
||||
targets: [
|
||||
{
|
||||
host: formData.targetHost || 'localhost',
|
||||
port: parseInt(formData.targetPort, 10) || 443,
|
||||
port: targetPort,
|
||||
},
|
||||
],
|
||||
},
|
||||
...(remoteIngressEnabled
|
||||
? {
|
||||
remoteIngress: {
|
||||
enabled: true,
|
||||
...(remoteIngressEdgeFilter.length > 0 ? { edgeFilter: remoteIngressEdgeFilter } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(priority != null && !isNaN(priority) ? { priority } : {}),
|
||||
};
|
||||
|
||||
@@ -641,13 +781,9 @@ export class OpsViewRoutes extends DeesElement {
|
||||
|
||||
// Build metadata if profile/target selected
|
||||
const metadata: any = {};
|
||||
const profileRefValue = formData.sourceProfileRef as any;
|
||||
const profileKey = typeof profileRefValue === 'string' ? profileRefValue : profileRefValue?.key;
|
||||
if (profileKey) {
|
||||
metadata.sourceProfileRef = profileKey;
|
||||
}
|
||||
const targetRefValue = formData.networkTargetRef as any;
|
||||
const targetKey = typeof targetRefValue === 'string' ? targetRefValue : targetRefValue?.key;
|
||||
if (targetKey) {
|
||||
metadata.networkTargetRef = targetKey;
|
||||
}
|
||||
@@ -669,6 +805,7 @@ export class OpsViewRoutes extends DeesElement {
|
||||
if (createForm) {
|
||||
await createForm.updateComplete;
|
||||
setupTlsVisibility(createForm);
|
||||
setupTargetInputState(createForm);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user