feat(network-routes): add route source policy editor

This commit is contained in:
2026-06-04 13:36:02 +00:00
parent 17bb63f129
commit 6c8073b91a
5 changed files with 152 additions and 166 deletions
+7
View File
@@ -15,6 +15,13 @@
- Hydrate DB-backed generated email routes to the same runtime handlers when their email system keys match.
- Add bidirectional socket proxy cleanup and tests for route hydration and SMTP banner relay.
### Features
- add route source policy editor (network-routes)
- Replace fixed source binding dropdown rows with the catalog route source policy input in route create and edit dialogs.
- Add source profile normalization, path class options, Gitea source policy presets, and validation for route source policies.
- Bump catalog UI dependencies and update pnpm built dependency configuration.
## 2026-06-04 - 13.44.1
### Fixes
+2 -2
View File
@@ -41,7 +41,7 @@
"@api.global/typedserver": "^8.4.7",
"@api.global/typedsocket": "^4.1.4",
"@apiclient.xyz/cloudflare": "^7.1.0",
"@design.estate/dees-catalog": "^3.83.0",
"@design.estate/dees-catalog": "^3.84.0",
"@design.estate/dees-element": "^2.2.4",
"@idp.global/sdk": "^1.4.0",
"@push.rocks/lik": "^6.4.1",
@@ -69,7 +69,7 @@
"@push.rocks/smartunique": "^3.0.9",
"@push.rocks/smartvpn": "1.20.0",
"@push.rocks/taskbuffer": "^8.0.2",
"@serve.zone/catalog": "^2.12.8",
"@serve.zone/catalog": "^2.13.0",
"@serve.zone/interfaces": "^6.2.1",
"@serve.zone/remoteingress": "^4.23.0",
"@tsclass/tsclass": "^9.5.1",
+12 -12
View File
@@ -24,8 +24,8 @@ importers:
specifier: ^7.1.0
version: 7.1.0
'@design.estate/dees-catalog':
specifier: ^3.83.0
version: 3.83.0(@tiptap/pm@2.27.2)
specifier: ^3.84.0
version: 3.84.0(@tiptap/pm@2.27.2)
'@design.estate/dees-element':
specifier: ^2.2.4
version: 2.2.4
@@ -108,8 +108,8 @@ importers:
specifier: ^8.0.2
version: 8.0.2
'@serve.zone/catalog':
specifier: ^2.12.8
version: 2.12.8(@tiptap/pm@2.27.2)
specifier: ^2.13.0
version: 2.13.0(@tiptap/pm@2.27.2)
'@serve.zone/interfaces':
specifier: ^6.2.1
version: 6.2.1
@@ -365,8 +365,8 @@ packages:
'@configvault.io/interfaces@1.0.17':
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
'@design.estate/dees-catalog@3.83.0':
resolution: {integrity: sha512-Ia4fwZ5ndziJkSE000nCro83rD8Rujki7ASHBQhL6ZDflZRJRlfuc13azVnQC2sazKlo/bWSgiiLcpc3V2IYrw==}
'@design.estate/dees-catalog@3.84.0':
resolution: {integrity: sha512-CYNsKwOcu3FvkA+G3fli4P9fVfDcMK3my5AbhN6jLNM0JPMlKyKV8s3q6bAqQPc9QGAtm+XhY2zLI4Cgurs2HA==}
'@design.estate/dees-comms@1.0.30':
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
@@ -1689,8 +1689,8 @@ packages:
'@selderee/plugin-htmlparser2@0.11.0':
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
'@serve.zone/catalog@2.12.8':
resolution: {integrity: sha512-TBclzYbDH3OJlbLkWpLrBij2MU4eFpBs5MIJU7njBMZZaQ37IVYftG+vn6N4W2E2WfuTxaPVshN7MV3A/oR81g==}
'@serve.zone/catalog@2.13.0':
resolution: {integrity: sha512-w2zfbcbJLR1jbwJQkeLNCOW/WB71FyMBfVb+uiIO5XTVK+7zTD0cFozySjBDOrueCFDcL7GcoO8Ohgs9jCfuhQ==}
'@serve.zone/interfaces@6.2.1':
resolution: {integrity: sha512-t2wrpBmd8zDdnyeeY/LG2hfjCXdm/uTHB6oovJ/xHgOws1E2VimYJPFiN7zqs1aEJAmFukfgOq79+eZeq3hfWw==}
@@ -4276,7 +4276,7 @@ snapshots:
'@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 4.1.4(@push.rocks/smartserve@2.0.4)
'@cloudflare/workers-types': 4.20260602.1
'@design.estate/dees-catalog': 3.83.0(@tiptap/pm@2.27.2)
'@design.estate/dees-catalog': 3.84.0(@tiptap/pm@2.27.2)
'@design.estate/dees-comms': 1.0.30
'@push.rocks/lik': 6.4.1
'@push.rocks/smartdelay': 3.1.0
@@ -4809,7 +4809,7 @@ snapshots:
dependencies:
'@api.global/typedrequest-interfaces': 3.0.19
'@design.estate/dees-catalog@3.83.0(@tiptap/pm@2.27.2)':
'@design.estate/dees-catalog@3.84.0(@tiptap/pm@2.27.2)':
dependencies:
'@design.estate/dees-domtools': 2.5.6
'@design.estate/dees-element': 2.2.4
@@ -6915,9 +6915,9 @@ snapshots:
domhandler: 5.0.3
selderee: 0.11.0
'@serve.zone/catalog@2.12.8(@tiptap/pm@2.27.2)':
'@serve.zone/catalog@2.13.0(@tiptap/pm@2.27.2)':
dependencies:
'@design.estate/dees-catalog': 3.83.0(@tiptap/pm@2.27.2)
'@design.estate/dees-catalog': 3.84.0(@tiptap/pm@2.27.2)
'@design.estate/dees-domtools': 2.5.6
'@design.estate/dees-element': 2.2.4
'@design.estate/dees-wcctools': 3.9.0
+5 -4
View File
@@ -1,4 +1,5 @@
allowBuilds:
esbuild: true
mongodb-memory-server: true
puppeteer: true
onlyBuiltDependencies:
- '@design.estate/dees-catalog'
- esbuild
- mongodb-memory-server
- puppeteer
+126 -148
View File
@@ -2,6 +2,12 @@ import * as appstate from '../../appstate.js';
import * as interfaces from '../../../dist_ts_interfaces/index.js';
import { viewHostCss } from '../shared/css.js';
import { type IStatsTile } from '@design.estate/dees-catalog';
import type {
IRoutePathClassOption as ISzRoutePathClassOption,
IRouteSourcePolicyPreset as ISzRouteSourcePolicyPreset,
ISourceProfileOption as ISzSourceProfileOption,
SzInputRouteSourcePolicy,
} from '@serve.zone/catalog';
import {
DeesElement,
@@ -24,8 +30,8 @@ const tlsCertOptions = [
{ key: 'auto', option: 'Auto (ACME/Let\'s Encrypt)' },
{ key: 'custom', option: 'Custom certificate' },
];
const maxSourceBindingRows = 16;
const giteaSourcePolicyProfileNames = ['TRUSTED NETWORKS', 'AI CRAWLERS', 'PUBLIC'] as const;
type TSzRouteSecurity = NonNullable<ISzSourceProfileOption['security']>;
function rateLimit(maxRequests: number): interfaces.data.IRouteSecurity['rateLimit'] {
return { enabled: true, maxRequests, window: 60, keyBy: 'ip' };
@@ -35,36 +41,6 @@ function getDropdownKey(value: any): string {
return typeof value === 'string' ? value : value?.key || '';
}
function getSourceBindingRefsFromFormData(formData: Record<string, any>): string[] {
const refs: string[] = [];
for (let index = 0; index < maxSourceBindingRows; index++) {
const ref = getDropdownKey(formData[`sourceBindingProfileRef${index}`]);
if (ref && !refs.includes(ref)) {
refs.push(ref);
}
}
return refs;
}
function buildSourceBindingsMetadata(
profileRefs: string[],
existingSourceBindings?: interfaces.data.IRouteSourceBinding[],
): interfaces.data.IRouteSourceBinding[] {
return profileRefs.map((sourceProfileRef) => {
const existingBinding = existingSourceBindings?.find((binding) => binding.sourceProfileRef === sourceProfileRef);
return existingBinding
? {
...existingBinding,
sourceProfileRef,
onExceeded: existingBinding.onExceeded || { type: '429' as const },
}
: {
sourceProfileRef,
onExceeded: { type: '429' as const },
};
});
}
function getGiteaPresetProfileRefs(profiles: interfaces.data.ISourceProfile[]): {
refs: string[];
missingNames: string[];
@@ -116,70 +92,111 @@ function buildGiteaSourceBindingsMetadata(profileRefs: string[]): interfaces.dat
];
}
function getGiteaPresetSourceBindings(profiles: interfaces.data.ISourceProfile[]): interfaces.data.IRouteSourceBinding[] | null {
function getGiteaSourcePolicyPresets(profiles: interfaces.data.ISourceProfile[]): ISzRouteSourcePolicyPreset[] {
const { refs, missingNames } = getGiteaPresetProfileRefs(profiles);
if (missingNames.length > 0) {
alert(`Gitea source-policy preset needs these seeded profiles: ${missingNames.join(', ')}`);
return null;
return [];
}
if (!validateSourceBindingSelection(refs, profiles)) {
return null;
}
return buildGiteaSourceBindingsMetadata(refs);
return [
{
key: 'gitea-bot-protection',
label: 'Gitea bot protection',
description: 'TRUSTED NETWORKS -> AI CRAWLERS -> PUBLIC with path-class rate limits.',
bindings: buildGiteaSourceBindingsMetadata(refs),
},
];
}
function metadataUsesPathPolicies(metadata?: interfaces.data.IRouteMetadata): boolean {
return Boolean(metadata?.sourceBindings?.some((binding) => binding.pathPolicies?.length));
function normalizeSecurityListEntries(entries: unknown): string[] {
if (!Array.isArray(entries)) {
return [];
}
return entries
.map((entry) => {
if (typeof entry === 'string') return entry.trim();
if (entry && typeof entry === 'object' && 'ip' in entry) {
const ip = (entry as Record<string, unknown>).ip;
return typeof ip === 'string' ? ip.trim() : '';
}
return '';
})
.filter(Boolean);
}
function sourceProfileMatchesAll(profile: interfaces.data.ISourceProfile): boolean {
return (profile.security?.ipAllowList || []).some((entry) => {
const source = typeof entry === 'string' ? entry : entry.ip;
return normalizeSecurityListEntries(profile.security?.ipAllowList).some((source) => {
return ['*', '0.0.0.0/0', '::/0'].includes(source.trim());
});
}
function sourceProfileHasSourceMatches(profile: interfaces.data.ISourceProfile): boolean {
return (profile.security?.ipAllowList || []).some((entry) => {
const source = typeof entry === 'string' ? entry : entry.ip;
return source.trim().length > 0;
return normalizeSecurityListEntries(profile.security?.ipAllowList).length > 0;
}
function normalizeCatalogRateLimit(
rateLimitValue: interfaces.data.IRouteSecurity['rateLimit'] | undefined,
): TSzRouteSecurity['rateLimit'] | undefined {
if (!rateLimitValue) return undefined;
return {
enabled: Boolean(rateLimitValue.enabled),
maxRequests: Number(rateLimitValue.maxRequests) || 0,
window: Number(rateLimitValue.window) || 0,
...(rateLimitValue.keyBy ? { keyBy: String(rateLimitValue.keyBy) } : {}),
};
}
function getSourceProfileOptions(profiles: interfaces.data.ISourceProfile[]): ISzSourceProfileOption[] {
return profiles.map((profile) => {
const ipAllowList = normalizeSecurityListEntries(profile.security?.ipAllowList);
const ipBlockList = normalizeSecurityListEntries(profile.security?.ipBlockList);
const rateLimitValue = normalizeCatalogRateLimit(profile.security?.rateLimit);
const security: TSzRouteSecurity = {
...(ipAllowList.length ? { ipAllowList } : {}),
...(ipBlockList.length ? { ipBlockList } : {}),
...(typeof profile.security?.maxConnections === 'number' ? { maxConnections: profile.security.maxConnections } : {}),
...(rateLimitValue ? { rateLimit: rateLimitValue } : {}),
};
return {
id: profile.id,
name: profile.name,
description: profile.description,
security,
hasSourceMatches: sourceProfileHasSourceMatches(profile),
matchesAllSources: sourceProfileMatchesAll(profile),
};
});
}
function validateSourceBindingSelection(
profileRefs: string[],
profiles: interfaces.data.ISourceProfile[],
): boolean {
if (profileRefs.length === 0) {
function getRoutePathClassOptions(): ISzRoutePathClassOption[] {
return interfaces.data.routePathClasses.map((pathClass) => ({
key: pathClass,
label: interfaces.data.giteaRoutePathClassLabels[pathClass],
defaultPatterns: interfaces.data.giteaRoutePathClassPatterns[pathClass],
}));
}
function getSourcePolicyInfoText(profiles: interfaces.data.ISourceProfile[]): string {
const { missingNames } = getGiteaPresetProfileRefs(profiles);
const presetText = missingNames.length > 0
? `Gitea preset hidden until these source profiles exist: ${missingNames.join(', ')}.`
: 'Use the Gitea preset as a starting point, then edit the generated bindings before saving.';
return `First matching source profile wins. Leave empty for no route-level source access control. ${presetText}`;
}
function validateSourcePolicyInput(form: Element): boolean {
const sourcePolicyInput = form.querySelector('sz-input-route-source-policy') as SzInputRouteSourcePolicy | null;
if (!sourcePolicyInput || sourcePolicyInput.isValid()) {
return true;
}
alert(sourcePolicyInput.getValidationMessages().join('\n'));
return false;
}
const selectedProfiles = profileRefs
.map((profileRef) => profiles.find((profile) => profile.id === profileRef))
.filter(Boolean) as interfaces.data.ISourceProfile[];
if (selectedProfiles.length !== profileRefs.length) {
alert('One or more selected source profiles could not be found. Refresh profiles and try again.');
return false;
}
const profilesWithoutMatches = selectedProfiles.filter((profile) => !sourceProfileHasSourceMatches(profile));
if (profilesWithoutMatches.length > 0) {
alert(`Source profiles need IP/CIDR match entries before use: ${profilesWithoutMatches.map((profile) => profile.name).join(', ')}`);
return false;
}
if (selectedProfiles.slice(0, -1).some((profile) => sourceProfileMatchesAll(profile))) {
alert('Wildcard source profiles must be last. Earlier wildcard profiles would shadow all following profiles.');
return false;
}
const fallbackProfile = selectedProfiles[selectedProfiles.length - 1];
if (sourceProfileMatchesAll(fallbackProfile) && fallbackProfile.security?.rateLimit?.enabled !== true) {
return confirm(`The wildcard profile "${fallbackProfile.name}" has no enabled rate limit. Save anyway?`);
}
return true;
function getSourceBindingsFromFormData(formData: Record<string, unknown>): interfaces.data.IRouteSourceBinding[] {
const sourceBindings = formData.sourceBindings;
return Array.isArray(sourceBindings)
? sourceBindings as interfaces.data.IRouteSourceBinding[]
: [];
}
function parseTargetPort(value: any): number | undefined {
@@ -620,13 +637,6 @@ export class OpsViewRoutes extends DeesElement {
const profiles = this.profilesTargetsState.profiles;
const targets = this.profilesTargetsState.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) => ({
@@ -651,7 +661,10 @@ export class OpsViewRoutes extends DeesElement {
const currentVpnOnly = route.vpnOnly === true;
const currentRemoteIngressEnabled = route.remoteIngress?.enabled === true;
const currentEdgeFilter = route.remoteIngress?.edgeFilter || [];
const currentSourceBindingRefs = this.getSourceBindingRefs(merged.metadata);
const sourceProfileOptions = getSourceProfileOptions(profiles);
const pathClassOptions = getRoutePathClassOptions();
const sourcePolicyPresets = getGiteaSourcePolicyPresets(profiles);
const sourcePolicyInfoText = getSourcePolicyInfoText(profiles);
// Compute current TLS state for pre-population
const currentTls = (route.action as any).tls;
@@ -672,24 +685,15 @@ export class OpsViewRoutes extends DeesElement {
<dees-input-text .key=${'ports'} .label=${'Ports'} .description=${'Comma-separated, e.g. 80, 443'} .value=${currentPorts} .required=${true}></dees-input-text>
<dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'Add domain...'} .value=${currentDomains}></dees-input-list>
<dees-input-text .key=${'priority'} .label=${'Priority'} .description=${'Higher values are matched first'} .value=${route.priority != null ? String(route.priority) : ''}></dees-input-text>
<div class="sourcePolicyGroup" style="display: flex; flex-direction: column; gap: 12px; padding: 12px; border: 1px solid rgba(255,255,255,0.12); border-radius: 8px;">
<strong>Source Bindings</strong>
<small>First matching source profile wins. Leave all rows empty to remove route-level source access control.</small>
<dees-input-checkbox
.key=${'useGiteaTemplate'}
.label=${'Apply Gitea bot protection template on save'}
.description=${'Replaces these rows with TRUSTED NETWORKS -> AI CRAWLERS -> PUBLIC and path-class limits.'}
.value=${false}
></dees-input-checkbox>
${Array.from({ length: maxSourceBindingRows }, (_item, index) => html`
<dees-input-dropdown
.key=${`sourceBindingProfileRef${index}`}
.label=${`Binding ${index + 1}`}
.options=${profileOptions}
.selectedOption=${profileOptions.find((o) => o.key === (currentSourceBindingRefs[index] || '')) || profileOptions[0]}
></dees-input-dropdown>
`)}
</div>
<sz-input-route-source-policy
.key=${'sourceBindings'}
.label=${'Source Policy'}
.infoText=${sourcePolicyInfoText}
.sourceProfiles=${sourceProfileOptions}
.pathClassOptions=${pathClassOptions}
.presets=${sourcePolicyPresets}
.value=${merged.metadata?.sourceBindings || []}
></sz-input-route-source-policy>
<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>
@@ -723,6 +727,7 @@ export class OpsViewRoutes extends DeesElement {
if (!form) return;
const formData = await form.collectFormData();
if (!formData.name || !formData.ports) return;
if (!validateSourcePolicyInput(form)) return;
const ports = formData.ports.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p));
const domains: string[] = Array.isArray(formData.domains)
@@ -730,11 +735,7 @@ export class OpsViewRoutes extends DeesElement {
: [];
const priority = formData.priority ? parseInt(formData.priority, 10) : undefined;
const useGiteaTemplate = Boolean(formData.useGiteaTemplate);
const sourceBindingRefs = useGiteaTemplate
? []
: getSourceBindingRefsFromFormData(formData);
if (!useGiteaTemplate && !validateSourceBindingSelection(sourceBindingRefs, profiles)) return;
const sourceBindings = getSourceBindingsFromFormData(formData);
const targetKey = getDropdownKey(formData.networkTargetRef);
const preserveMatchPort = !targetKey && Boolean(formData.preserveMatchPort);
const targetPort = preserveMatchPort
@@ -798,12 +799,8 @@ export class OpsViewRoutes extends DeesElement {
}
const metadata: any = {};
if (useGiteaTemplate) {
const sourceBindings = getGiteaPresetSourceBindings(profiles);
if (!sourceBindings) return;
if (sourceBindings.length > 0) {
metadata.sourceBindings = sourceBindings;
} else if (sourceBindingRefs.length > 0) {
metadata.sourceBindings = buildSourceBindingsMetadata(sourceBindingRefs, merged.metadata?.sourceBindings);
} else if (merged.metadata?.sourceBindings) {
metadata.sourceBindings = [];
}
@@ -841,14 +838,11 @@ export class OpsViewRoutes extends DeesElement {
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 : ''}`,
})),
];
// Build dropdown options for targets and source policy metadata
const sourceProfileOptions = getSourceProfileOptions(profiles);
const pathClassOptions = getRoutePathClassOptions();
const sourcePolicyPresets = getGiteaSourcePolicyPresets(profiles);
const sourcePolicyInfoText = getSourcePolicyInfoText(profiles);
const targetOptions = [
{ key: '', option: '(none — inline target)' },
...targets.map((t) => ({
@@ -865,24 +859,15 @@ export class OpsViewRoutes extends DeesElement {
<dees-input-text .key=${'ports'} .label=${'Ports'} .description=${'Comma-separated, e.g. 80, 443'} .required=${true}></dees-input-text>
<dees-input-list .key=${'domains'} .label=${'Domains'} .placeholder=${'Add domain...'}></dees-input-list>
<dees-input-text .key=${'priority'} .label=${'Priority'} .description=${'Higher values are matched first'}></dees-input-text>
<div class="sourcePolicyGroup" style="display: flex; flex-direction: column; gap: 12px; padding: 12px; border: 1px solid rgba(255,255,255,0.12); border-radius: 8px;">
<strong>Source Bindings</strong>
<small>First matching source profile wins. Leave all rows empty for no route-level source access control.</small>
<dees-input-checkbox
.key=${'useGiteaTemplate'}
.label=${'Apply Gitea bot protection template on save'}
.description=${'Writes TRUSTED NETWORKS -> AI CRAWLERS -> PUBLIC and path-class limits.'}
.value=${false}
></dees-input-checkbox>
${Array.from({ length: maxSourceBindingRows }, (_item, index) => html`
<dees-input-dropdown
.key=${`sourceBindingProfileRef${index}`}
.label=${`Binding ${index + 1}`}
.options=${profileOptions}
.selectedOption=${profileOptions[0]}
></dees-input-dropdown>
`)}
</div>
<sz-input-route-source-policy
.key=${'sourceBindings'}
.label=${'Source Policy'}
.infoText=${sourcePolicyInfoText}
.sourceProfiles=${sourceProfileOptions}
.pathClassOptions=${pathClassOptions}
.presets=${sourcePolicyPresets}
.value=${[]}
></sz-input-route-source-policy>
<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>
@@ -916,6 +901,7 @@ export class OpsViewRoutes extends DeesElement {
if (!form) return;
const formData = await form.collectFormData();
if (!formData.name || !formData.ports) return;
if (!validateSourcePolicyInput(form)) return;
const ports = formData.ports.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p));
const domains: string[] = Array.isArray(formData.domains)
@@ -923,11 +909,7 @@ export class OpsViewRoutes extends DeesElement {
: [];
const priority = formData.priority ? parseInt(formData.priority, 10) : undefined;
const useGiteaTemplate = Boolean(formData.useGiteaTemplate);
const sourceBindingRefs = useGiteaTemplate
? []
: getSourceBindingRefsFromFormData(formData);
if (!useGiteaTemplate && !validateSourceBindingSelection(sourceBindingRefs, profiles)) return;
const sourceBindings = getSourceBindingsFromFormData(formData);
const targetKey = getDropdownKey(formData.networkTargetRef);
const preserveMatchPort = !targetKey && Boolean(formData.preserveMatchPort);
const targetPort = preserveMatchPort
@@ -992,12 +974,8 @@ export class OpsViewRoutes extends DeesElement {
// Build metadata if profile/target selected
const metadata: any = {};
if (useGiteaTemplate) {
const sourceBindings = getGiteaPresetSourceBindings(profiles);
if (!sourceBindings) return;
if (sourceBindings.length > 0) {
metadata.sourceBindings = sourceBindings;
} else if (sourceBindingRefs.length > 0) {
metadata.sourceBindings = buildSourceBindingsMetadata(sourceBindingRefs);
}
if (targetKey) {
metadata.networkTargetRef = targetKey;