fix(vpn,target-profiles): normalize target profile route references and stabilize VPN host-IP client routing behavior

This commit is contained in:
2026-04-13 23:02:42 +00:00
parent 466654ee4c
commit aec8b72ca3
11 changed files with 446 additions and 131 deletions

View File

@@ -28,7 +28,7 @@ function setupFormVisibility(formEl: any) {
const staticIpGroup = contentEl.querySelector('.staticIpGroup') as HTMLElement;
const vlanIdGroup = contentEl.querySelector('.vlanIdGroup') as HTMLElement;
const aclGroup = contentEl.querySelector('.aclGroup') as HTMLElement;
if (hostIpGroup) hostIpGroup.style.display = show; // always show (forceTarget is always on)
if (hostIpGroup) hostIpGroup.style.display = show;
if (hostIpDetails) hostIpDetails.style.display = data.useHostIp ? show : 'none';
if (staticIpGroup) staticIpGroup.style.display = data.useDhcp ? 'none' : show;
if (vlanIdGroup) vlanIdGroup.style.display = data.forceVlan ? show : 'none';
@@ -390,7 +390,7 @@ export class OpsViewVpn extends DeesElement {
if (!form) return;
const data = await form.collectFormData();
if (!data.clientId) return;
const targetProfileIds = this.resolveProfileNamesToIds(
const targetProfileIds = this.resolveProfileLabelsToIds(
Array.isArray(data.targetProfileNames) ? data.targetProfileNames : [],
);
@@ -414,10 +414,10 @@ export class OpsViewVpn extends DeesElement {
description: data.description || undefined,
targetProfileIds,
useHostIp: useHostIp || undefined,
useDhcp: useDhcp || undefined,
useHostIp,
useDhcp,
staticIp,
forceVlan: forceVlan || undefined,
forceVlan,
vlanId,
destinationAllowList,
destinationBlockList,
@@ -485,7 +485,7 @@ export class OpsViewVpn extends DeesElement {
<div class="infoItem"><span class="infoLabel">Transport</span><span class="infoValue">${conn.transport}</span></div>
` : ''}
<div class="infoItem"><span class="infoLabel">Description</span><span class="infoValue">${client.description || '-'}</span></div>
<div class="infoItem"><span class="infoLabel">Target Profiles</span><span class="infoValue">${this.resolveProfileIdsToNames(client.targetProfileIds)?.join(', ') || '-'}</span></div>
<div class="infoItem"><span class="infoLabel">Target Profiles</span><span class="infoValue">${this.resolveProfileIdsToLabels(client.targetProfileIds)?.join(', ') || '-'}</span></div>
<div class="infoItem"><span class="infoLabel">Routing</span><span class="infoValue">${client.useHostIp ? 'Host IP' : 'SmartProxy'}</span></div>
${client.useHostIp ? html`
<div class="infoItem"><span class="infoLabel">Host IP</span><span class="infoValue">${client.useDhcp ? 'DHCP' : client.staticIp ? `Static: ${client.staticIp}` : 'Not configured'}</span></div>
@@ -649,7 +649,7 @@ export class OpsViewVpn extends DeesElement {
const client = actionData.item as interfaces.data.IVpnClient;
const { DeesModal } = await import('@design.estate/dees-catalog');
const currentDescription = client.description ?? '';
const currentTargetProfileNames = this.resolveProfileIdsToNames(client.targetProfileIds) || [];
const currentTargetProfileNames = this.resolveProfileIdsToLabels(client.targetProfileIds) || [];
const profileCandidates = this.getTargetProfileCandidates();
const currentUseHostIp = client.useHostIp ?? false;
const currentUseDhcp = client.useDhcp ?? false;
@@ -695,7 +695,7 @@ export class OpsViewVpn extends DeesElement {
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
if (!form) return;
const data = await form.collectFormData();
const targetProfileIds = this.resolveProfileNamesToIds(
const targetProfileIds = this.resolveProfileLabelsToIds(
Array.isArray(data.targetProfileNames) ? data.targetProfileNames : [],
);
@@ -719,10 +719,10 @@ export class OpsViewVpn extends DeesElement {
description: data.description || undefined,
targetProfileIds,
useHostIp: useHostIp || undefined,
useDhcp: useDhcp || undefined,
useHostIp,
useDhcp,
staticIp,
forceVlan: forceVlan || undefined,
forceVlan,
vlanId,
destinationAllowList,
destinationBlockList,
@@ -811,41 +811,52 @@ export class OpsViewVpn extends DeesElement {
}
/**
* Build autocomplete candidates from loaded target profiles.
* viewKey = profile name (displayed), payload = { id } (carried for resolution).
* Build stable profile labels for list inputs.
*/
private getTargetProfileCandidates() {
private getTargetProfileChoices() {
const profileState = appstate.targetProfilesStatePart.getState();
const profiles = profileState?.profiles || [];
return profiles.map((p) => ({ viewKey: p.name, payload: { id: p.id } }));
const nameCounts = new Map<string, number>();
for (const profile of profiles) {
nameCounts.set(profile.name, (nameCounts.get(profile.name) || 0) + 1);
}
return profiles.map((profile) => ({
id: profile.id,
label: (nameCounts.get(profile.name) || 0) > 1
? `${profile.name} (${profile.id})`
: profile.name,
}));
}
private getTargetProfileCandidates() {
return this.getTargetProfileChoices().map((profile) => ({ viewKey: profile.label }));
}
/**
* Convert profile IDs to profile names (for populating edit form values).
* Convert profile IDs to form labels (for populating edit form values).
*/
private resolveProfileIdsToNames(ids?: string[]): string[] | undefined {
private resolveProfileIdsToLabels(ids?: string[]): string[] | undefined {
if (!ids?.length) return undefined;
const profileState = appstate.targetProfilesStatePart.getState();
const profiles = profileState?.profiles || [];
const choices = this.getTargetProfileChoices();
const labelsById = new Map(choices.map((profile) => [profile.id, profile.label]));
return ids.map((id) => {
const profile = profiles.find((p) => p.id === id);
return profile?.name || id;
return labelsById.get(id) || id;
});
}
/**
* Convert profile names back to IDs (for saving form data).
* Uses the dees-input-list candidates' payload when available.
* Convert profile form labels back to IDs.
*/
private resolveProfileNamesToIds(names: string[]): string[] | undefined {
if (!names.length) return undefined;
const profileState = appstate.targetProfilesStatePart.getState();
const profiles = profileState?.profiles || [];
return names
.map((name) => {
const profile = profiles.find((p) => p.name === name);
return profile?.id;
})
private resolveProfileLabelsToIds(labels: string[]): string[] {
if (!labels.length) return [];
const labelsToIds = new Map(
this.getTargetProfileChoices().map((profile) => [profile.label, profile.id]),
);
return labels
.map((label) => labelsToIds.get(label))
.filter((id): id is string => !!id);
}
}