fix(vpn): handle VPN forwarding mode downgrades and support runtime VPN config updates
This commit is contained in:
@@ -49,19 +49,28 @@ export class OpsViewVpn extends DeesElement {
|
||||
@state()
|
||||
accessor vpnState: appstate.IVpnState = appstate.vpnStatePart.getState()!;
|
||||
|
||||
@state()
|
||||
accessor targetProfilesState: appstate.ITargetProfilesState = appstate.targetProfilesStatePart.getState()!;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const sub = appstate.vpnStatePart.select().subscribe((newState) => {
|
||||
this.vpnState = newState;
|
||||
});
|
||||
this.rxSubscriptions.push(sub);
|
||||
|
||||
const targetProfilesSub = appstate.targetProfilesStatePart.select().subscribe((newState) => {
|
||||
this.targetProfilesState = newState;
|
||||
});
|
||||
this.rxSubscriptions.push(targetProfilesSub);
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
await super.connectedCallback();
|
||||
await appstate.vpnStatePart.dispatchAction(appstate.fetchVpnAction, null);
|
||||
// Ensure target profiles are loaded for autocomplete candidates
|
||||
await appstate.targetProfilesStatePart.dispatchAction(appstate.fetchTargetProfilesAction, null);
|
||||
await Promise.all([
|
||||
appstate.vpnStatePart.dispatchAction(appstate.fetchVpnAction, null),
|
||||
appstate.targetProfilesStatePart.dispatchAction(appstate.fetchTargetProfilesAction, null),
|
||||
]);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
@@ -330,13 +339,7 @@ export class OpsViewVpn extends DeesElement {
|
||||
'Status': statusHtml,
|
||||
'Routing': routingHtml,
|
||||
'VPN IP': client.assignedIp || '-',
|
||||
'Target Profiles': client.targetProfileIds?.length
|
||||
? html`${client.targetProfileIds.map(id => {
|
||||
const profileState = appstate.targetProfilesStatePart.getState();
|
||||
const profile = profileState?.profiles.find(p => p.id === id);
|
||||
return html`<span class="tagBadge">${profile?.name || id}</span>`;
|
||||
})}`
|
||||
: '-',
|
||||
'Target Profiles': this.renderTargetProfileBadges(client.targetProfileIds),
|
||||
'Description': client.description || '-',
|
||||
'Created': new Date(client.createdAt).toLocaleDateString(),
|
||||
};
|
||||
@@ -347,6 +350,7 @@ export class OpsViewVpn extends DeesElement {
|
||||
iconName: 'lucide:plus',
|
||||
type: ['header'],
|
||||
actionFunc: async () => {
|
||||
await this.ensureTargetProfilesLoaded();
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
const profileCandidates = this.getTargetProfileCandidates();
|
||||
const createModal = await DeesModal.createAndShow({
|
||||
@@ -647,6 +651,7 @@ export class OpsViewVpn extends DeesElement {
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionData: any) => {
|
||||
const client = actionData.item as interfaces.data.IVpnClient;
|
||||
await this.ensureTargetProfilesLoaded();
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
const currentDescription = client.description ?? '';
|
||||
const currentTargetProfileNames = this.resolveProfileIdsToLabels(client.targetProfileIds) || [];
|
||||
@@ -810,12 +815,28 @@ export class OpsViewVpn extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async ensureTargetProfilesLoaded(): Promise<void> {
|
||||
await appstate.targetProfilesStatePart.dispatchAction(appstate.fetchTargetProfilesAction, null);
|
||||
}
|
||||
|
||||
private renderTargetProfileBadges(ids?: string[]): TemplateResult | string {
|
||||
const labels = this.resolveProfileIdsToLabels(ids, {
|
||||
pendingLabel: 'Loading profile...',
|
||||
missingLabel: (id) => `Unknown profile (${id})`,
|
||||
});
|
||||
|
||||
if (!labels?.length) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return html`${labels.map((label) => html`<span class="tagBadge">${label}</span>`)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build stable profile labels for list inputs.
|
||||
*/
|
||||
private getTargetProfileChoices() {
|
||||
const profileState = appstate.targetProfilesStatePart.getState();
|
||||
const profiles = profileState?.profiles || [];
|
||||
const profiles = this.targetProfilesState.profiles || [];
|
||||
const nameCounts = new Map<string, number>();
|
||||
|
||||
for (const profile of profiles) {
|
||||
@@ -837,12 +858,27 @@ export class OpsViewVpn extends DeesElement {
|
||||
/**
|
||||
* Convert profile IDs to form labels (for populating edit form values).
|
||||
*/
|
||||
private resolveProfileIdsToLabels(ids?: string[]): string[] | undefined {
|
||||
private resolveProfileIdsToLabels(
|
||||
ids?: string[],
|
||||
options: {
|
||||
pendingLabel?: string;
|
||||
missingLabel?: (id: string) => string;
|
||||
} = {},
|
||||
): string[] | undefined {
|
||||
if (!ids?.length) return undefined;
|
||||
const choices = this.getTargetProfileChoices();
|
||||
const labelsById = new Map(choices.map((profile) => [profile.id, profile.label]));
|
||||
return ids.map((id) => {
|
||||
return labelsById.get(id) || id;
|
||||
const label = labelsById.get(id);
|
||||
if (label) {
|
||||
return label;
|
||||
}
|
||||
|
||||
if (this.targetProfilesState.lastUpdated === 0 && !this.targetProfilesState.error) {
|
||||
return options.pendingLabel || 'Loading profile...';
|
||||
}
|
||||
|
||||
return options.missingLabel?.(id) || id;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user