BREAKING CHANGE(vpn): replace tag-based VPN access control with source and target profiles
This commit is contained in:
@@ -10,6 +10,7 @@ export * from './ops-view-security.js';
|
||||
export * from './ops-view-certificates.js';
|
||||
export * from './ops-view-remoteingress.js';
|
||||
export * from './ops-view-vpn.js';
|
||||
export * from './ops-view-securityprofiles.js';
|
||||
export * from './ops-view-sourceprofiles.js';
|
||||
export * from './ops-view-networktargets.js';
|
||||
export * from './ops-view-targetprofiles.js';
|
||||
export * from './shared/index.js';
|
||||
@@ -24,8 +24,9 @@ import { OpsViewSecurity } from './ops-view-security.js';
|
||||
import { OpsViewCertificates } from './ops-view-certificates.js';
|
||||
import { OpsViewRemoteIngress } from './ops-view-remoteingress.js';
|
||||
import { OpsViewVpn } from './ops-view-vpn.js';
|
||||
import { OpsViewSecurityProfiles } from './ops-view-securityprofiles.js';
|
||||
import { OpsViewSourceProfiles } from './ops-view-sourceprofiles.js';
|
||||
import { OpsViewNetworkTargets } from './ops-view-networktargets.js';
|
||||
import { OpsViewTargetProfiles } from './ops-view-targetprofiles.js';
|
||||
|
||||
@customElement('ops-dashboard')
|
||||
export class OpsDashboard extends DeesElement {
|
||||
@@ -81,15 +82,20 @@ export class OpsDashboard extends DeesElement {
|
||||
element: OpsViewRoutes,
|
||||
},
|
||||
{
|
||||
name: 'SecurityProfiles',
|
||||
name: 'SourceProfiles',
|
||||
iconName: 'lucide:shieldCheck',
|
||||
element: OpsViewSecurityProfiles,
|
||||
element: OpsViewSourceProfiles,
|
||||
},
|
||||
{
|
||||
name: 'NetworkTargets',
|
||||
iconName: 'lucide:server',
|
||||
element: OpsViewNetworkTargets,
|
||||
},
|
||||
{
|
||||
name: 'TargetProfiles',
|
||||
iconName: 'lucide:target',
|
||||
element: OpsViewTargetProfiles,
|
||||
},
|
||||
{
|
||||
name: 'ApiTokens',
|
||||
iconName: 'lucide:key',
|
||||
|
||||
@@ -337,7 +337,7 @@ 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?.sourceProfileName ? html`<p>Source Profile: <strong style="color: #a78bfa;">${meta.sourceProfileName}</strong></p>` : ''}
|
||||
${meta?.networkTargetName ? html`<p>Network Target: <strong style="color: #a78bfa;">${meta.networkTargetName}</strong></p>` : ''}
|
||||
</div>
|
||||
`,
|
||||
@@ -476,7 +476,7 @@ export class OpsViewRoutes extends DeesElement {
|
||||
<dees-input-text .key=${'ports'} .label=${'Ports (comma-separated)'} .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 (higher = matched first)'} .value=${route.priority != null ? String(route.priority) : ''}></dees-input-text>
|
||||
<dees-input-dropdown .key=${'securityProfileRef'} .label=${'Security Profile'} .options=${profileOptions} .selectedOption=${profileOptions.find((o) => o.key === (merged.metadata?.securityProfileRef || '')) || null}></dees-input-dropdown>
|
||||
<dees-input-dropdown .key=${'sourceProfileRef'} .label=${'Source Profile'} .options=${profileOptions} .selectedOption=${profileOptions.find((o) => o.key === (merged.metadata?.sourceProfileRef || '')) || null}></dees-input-dropdown>
|
||||
<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 (if no target selected)'} .value=${currentTargetHost}></dees-input-text>
|
||||
<dees-input-text .key=${'targetPort'} .label=${'Target Port (if no target selected)'} .value=${currentTargetPort}></dees-input-text>
|
||||
@@ -549,10 +549,10 @@ export class OpsViewRoutes extends DeesElement {
|
||||
}
|
||||
|
||||
const metadata: any = {};
|
||||
const profileRefValue = formData.securityProfileRef as any;
|
||||
const profileRefValue = formData.sourceProfileRef as any;
|
||||
const profileKey = typeof profileRefValue === 'string' ? profileRefValue : profileRefValue?.key;
|
||||
if (profileKey) {
|
||||
metadata.securityProfileRef = profileKey;
|
||||
metadata.sourceProfileRef = profileKey;
|
||||
}
|
||||
const targetRefValue = formData.networkTargetRef as any;
|
||||
const targetKey = typeof targetRefValue === 'string' ? targetRefValue : targetRefValue?.key;
|
||||
@@ -610,7 +610,7 @@ export class OpsViewRoutes extends DeesElement {
|
||||
<dees-input-text .key=${'ports'} .label=${'Ports (comma-separated)'} .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 (higher = matched first)'}></dees-input-text>
|
||||
<dees-input-dropdown .key=${'securityProfileRef'} .label=${'Security Profile'} .options=${profileOptions}></dees-input-dropdown>
|
||||
<dees-input-dropdown .key=${'sourceProfileRef'} .label=${'Source Profile'} .options=${profileOptions}></dees-input-dropdown>
|
||||
<dees-input-dropdown .key=${'networkTargetRef'} .label=${'Network Target'} .options=${targetOptions}></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>
|
||||
@@ -682,10 +682,10 @@ export class OpsViewRoutes extends DeesElement {
|
||||
|
||||
// Build metadata if profile/target selected
|
||||
const metadata: any = {};
|
||||
const profileRefValue = formData.securityProfileRef as any;
|
||||
const profileRefValue = formData.sourceProfileRef as any;
|
||||
const profileKey = typeof profileRefValue === 'string' ? profileRefValue : profileRefValue?.key;
|
||||
if (profileKey) {
|
||||
metadata.securityProfileRef = profileKey;
|
||||
metadata.sourceProfileRef = profileKey;
|
||||
}
|
||||
const targetRefValue = formData.networkTargetRef as any;
|
||||
const targetKey = typeof targetRefValue === 'string' ? targetRefValue : targetRefValue?.key;
|
||||
|
||||
@@ -14,12 +14,12 @@ import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'ops-view-securityprofiles': OpsViewSecurityProfiles;
|
||||
'ops-view-sourceprofiles': OpsViewSourceProfiles;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('ops-view-securityprofiles')
|
||||
export class OpsViewSecurityProfiles extends DeesElement {
|
||||
@customElement('ops-view-sourceprofiles')
|
||||
export class OpsViewSourceProfiles extends DeesElement {
|
||||
@state()
|
||||
accessor profilesState: appstate.IProfilesTargetsState = appstate.profilesTargetsStatePart.getState()!;
|
||||
|
||||
@@ -58,20 +58,20 @@ export class OpsViewSecurityProfiles extends DeesElement {
|
||||
type: 'number',
|
||||
value: profiles.length,
|
||||
icon: 'lucide:shieldCheck',
|
||||
description: 'Reusable security profiles',
|
||||
description: 'Reusable source profiles',
|
||||
color: '#3b82f6',
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<ops-sectionheading>Security Profiles</ops-sectionheading>
|
||||
<ops-sectionheading>Source Profiles</ops-sectionheading>
|
||||
<div class="profilesContainer">
|
||||
<dees-statsgrid .tiles=${statsTiles}></dees-statsgrid>
|
||||
<dees-table
|
||||
.heading1=${'Security Profiles'}
|
||||
.heading2=${'Reusable security configurations for routes'}
|
||||
.heading1=${'Source Profiles'}
|
||||
.heading2=${'Reusable source configurations for routes'}
|
||||
.data=${profiles}
|
||||
.displayFunction=${(profile: interfaces.data.ISecurityProfile) => ({
|
||||
.displayFunction=${(profile: interfaces.data.ISourceProfile) => ({
|
||||
Name: profile.name,
|
||||
Description: profile.description || '-',
|
||||
'IP Allow List': (profile.security?.ipAllowList || []).join(', ') || '-',
|
||||
@@ -107,7 +107,7 @@ export class OpsViewSecurityProfiles extends DeesElement {
|
||||
iconName: 'lucide:pencil',
|
||||
type: ['inRow', 'contextmenu'] as any,
|
||||
actionFunc: async (actionData: any) => {
|
||||
const profile = actionData.item as interfaces.data.ISecurityProfile;
|
||||
const profile = actionData.item as interfaces.data.ISourceProfile;
|
||||
await this.showEditProfileDialog(profile);
|
||||
},
|
||||
},
|
||||
@@ -116,7 +116,7 @@ export class OpsViewSecurityProfiles extends DeesElement {
|
||||
iconName: 'lucide:trash2',
|
||||
type: ['inRow', 'contextmenu'] as any,
|
||||
actionFunc: async (actionData: any) => {
|
||||
const profile = actionData.item as interfaces.data.ISecurityProfile;
|
||||
const profile = actionData.item as interfaces.data.ISourceProfile;
|
||||
await this.deleteProfile(profile);
|
||||
},
|
||||
},
|
||||
@@ -129,7 +129,7 @@ export class OpsViewSecurityProfiles extends DeesElement {
|
||||
private async showCreateProfileDialog() {
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
DeesModal.createAndShow({
|
||||
heading: 'Create Security Profile',
|
||||
heading: 'Create Source Profile',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
|
||||
@@ -167,7 +167,7 @@ export class OpsViewSecurityProfiles extends DeesElement {
|
||||
});
|
||||
}
|
||||
|
||||
private async showEditProfileDialog(profile: interfaces.data.ISecurityProfile) {
|
||||
private async showEditProfileDialog(profile: interfaces.data.ISourceProfile) {
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
DeesModal.createAndShow({
|
||||
heading: `Edit Profile: ${profile.name}`,
|
||||
@@ -209,7 +209,7 @@ export class OpsViewSecurityProfiles extends DeesElement {
|
||||
});
|
||||
}
|
||||
|
||||
private async deleteProfile(profile: interfaces.data.ISecurityProfile) {
|
||||
private async deleteProfile(profile: interfaces.data.ISourceProfile) {
|
||||
await appstate.profilesTargetsStatePart.dispatchAction(appstate.deleteProfileAction, {
|
||||
id: profile.id,
|
||||
force: false,
|
||||
379
ts_web/elements/ops-view-targetprofiles.ts
Normal file
379
ts_web/elements/ops-view-targetprofiles.ts
Normal file
@@ -0,0 +1,379 @@
|
||||
import {
|
||||
DeesElement,
|
||||
html,
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
css,
|
||||
state,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as plugins from '../plugins.js';
|
||||
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';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'ops-view-targetprofiles': OpsViewTargetProfiles;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('ops-view-targetprofiles')
|
||||
export class OpsViewTargetProfiles extends DeesElement {
|
||||
@state()
|
||||
accessor targetProfilesState: appstate.ITargetProfilesState = appstate.targetProfilesStatePart.getState()!;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const sub = appstate.targetProfilesStatePart.select().subscribe((newState) => {
|
||||
this.targetProfilesState = newState;
|
||||
});
|
||||
this.rxSubscriptions.push(sub);
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
await super.connectedCallback();
|
||||
await appstate.targetProfilesStatePart.dispatchAction(appstate.fetchTargetProfilesAction, null);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
viewHostCss,
|
||||
css`
|
||||
.profilesContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.tagBadge {
|
||||
display: inline-flex;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background: ${cssManager.bdTheme('#eff6ff', '#172554')};
|
||||
color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};
|
||||
margin-right: 4px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
const profiles = this.targetProfilesState.profiles;
|
||||
|
||||
const statsTiles: IStatsTile[] = [
|
||||
{
|
||||
id: 'totalProfiles',
|
||||
title: 'Total Profiles',
|
||||
type: 'number',
|
||||
value: profiles.length,
|
||||
icon: 'lucide:target',
|
||||
description: 'Reusable target profiles',
|
||||
color: '#8b5cf6',
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<ops-sectionheading>Target Profiles</ops-sectionheading>
|
||||
<div class="profilesContainer">
|
||||
<dees-statsgrid .tiles=${statsTiles}></dees-statsgrid>
|
||||
<dees-table
|
||||
.heading1=${'Target Profiles'}
|
||||
.heading2=${'Define what resources VPN clients can access'}
|
||||
.data=${profiles}
|
||||
.displayFunction=${(profile: interfaces.data.ITargetProfile) => ({
|
||||
Name: profile.name,
|
||||
Description: profile.description || '-',
|
||||
Domains: profile.domains?.length
|
||||
? html`${profile.domains.map(d => html`<span class="tagBadge">${d}</span>`)}`
|
||||
: '-',
|
||||
Targets: profile.targets?.length
|
||||
? html`${profile.targets.map(t => html`<span class="tagBadge">${t.host}:${t.port}</span>`)}`
|
||||
: '-',
|
||||
'Route Refs': profile.routeRefs?.length
|
||||
? html`${profile.routeRefs.map(r => html`<span class="tagBadge">${r}</span>`)}`
|
||||
: '-',
|
||||
Created: new Date(profile.createdAt).toLocaleDateString(),
|
||||
})}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'Create Profile',
|
||||
iconName: 'lucide:plus',
|
||||
type: ['header' as const],
|
||||
actionFunc: async () => {
|
||||
await this.showCreateProfileDialog();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Refresh',
|
||||
iconName: 'lucide:rotateCw',
|
||||
type: ['header' as const],
|
||||
actionFunc: async () => {
|
||||
await appstate.targetProfilesStatePart.dispatchAction(appstate.fetchTargetProfilesAction, null);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Detail',
|
||||
iconName: 'lucide:info',
|
||||
type: ['doubleClick'] as any,
|
||||
actionFunc: async (actionData: any) => {
|
||||
const profile = actionData.item as interfaces.data.ITargetProfile;
|
||||
await this.showDetailDialog(profile);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Edit',
|
||||
iconName: 'lucide:pencil',
|
||||
type: ['inRow', 'contextmenu'] as any,
|
||||
actionFunc: async (actionData: any) => {
|
||||
const profile = actionData.item as interfaces.data.ITargetProfile;
|
||||
await this.showEditProfileDialog(profile);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
iconName: 'lucide:trash2',
|
||||
type: ['inRow', 'contextmenu'] as any,
|
||||
actionFunc: async (actionData: any) => {
|
||||
const profile = actionData.item as interfaces.data.ITargetProfile;
|
||||
await this.deleteProfile(profile);
|
||||
},
|
||||
},
|
||||
]}
|
||||
></dees-table>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async showCreateProfileDialog() {
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
DeesModal.createAndShow({
|
||||
heading: 'Create Target Profile',
|
||||
content: html`
|
||||
<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=${'domains'} .label=${'Domains (comma-separated, e.g. *.example.com)'} ></dees-input-text>
|
||||
<dees-input-text .key=${'targets'} .label=${'Targets (comma-separated host:port, e.g. 10.0.0.1:443)'}></dees-input-text>
|
||||
<dees-input-text .key=${'routeRefs'} .label=${'Route Refs (comma-separated route names/IDs)'}></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => modalArg.destroy() },
|
||||
{
|
||||
name: 'Create',
|
||||
iconName: 'lucide:plus',
|
||||
action: async (modalArg: any) => {
|
||||
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
||||
if (!form) return;
|
||||
const data = await form.collectFormData();
|
||||
if (!data.name) return;
|
||||
|
||||
const domains = data.domains
|
||||
? String(data.domains).split(',').map((s: string) => s.trim()).filter(Boolean)
|
||||
: undefined;
|
||||
const targets = data.targets
|
||||
? String(data.targets).split(',').map((s: string) => {
|
||||
const trimmed = s.trim();
|
||||
const lastColon = trimmed.lastIndexOf(':');
|
||||
if (lastColon === -1) return null;
|
||||
return {
|
||||
host: trimmed.substring(0, lastColon),
|
||||
port: parseInt(trimmed.substring(lastColon + 1), 10),
|
||||
};
|
||||
}).filter((t): t is { host: string; port: number } => t !== null && !isNaN(t.port))
|
||||
: undefined;
|
||||
const routeRefs = data.routeRefs
|
||||
? String(data.routeRefs).split(',').map((s: string) => s.trim()).filter(Boolean)
|
||||
: undefined;
|
||||
|
||||
await appstate.targetProfilesStatePart.dispatchAction(appstate.createTargetProfileAction, {
|
||||
name: String(data.name),
|
||||
description: data.description ? String(data.description) : undefined,
|
||||
domains,
|
||||
targets,
|
||||
routeRefs,
|
||||
});
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private async showEditProfileDialog(profile: interfaces.data.ITargetProfile) {
|
||||
const currentDomains = profile.domains?.join(', ') ?? '';
|
||||
const currentTargets = profile.targets?.map(t => `${t.host}:${t.port}`).join(', ') ?? '';
|
||||
const currentRouteRefs = profile.routeRefs?.join(', ') ?? '';
|
||||
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
DeesModal.createAndShow({
|
||||
heading: `Edit Profile: ${profile.name}`,
|
||||
content: html`
|
||||
<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=${'domains'} .label=${'Domains (comma-separated, e.g. *.example.com)'} .value=${currentDomains}></dees-input-text>
|
||||
<dees-input-text .key=${'targets'} .label=${'Targets (comma-separated host:port, e.g. 10.0.0.1:443)'} .value=${currentTargets}></dees-input-text>
|
||||
<dees-input-text .key=${'routeRefs'} .label=${'Route Refs (comma-separated route names/IDs)'} .value=${currentRouteRefs}></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => modalArg.destroy() },
|
||||
{
|
||||
name: 'Save',
|
||||
iconName: 'lucide:check',
|
||||
action: async (modalArg: any) => {
|
||||
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
||||
if (!form) return;
|
||||
const data = await form.collectFormData();
|
||||
|
||||
const domains = data.domains
|
||||
? String(data.domains).split(',').map((s: string) => s.trim()).filter(Boolean)
|
||||
: [];
|
||||
const targets = data.targets
|
||||
? String(data.targets).split(',').map((s: string) => {
|
||||
const trimmed = s.trim();
|
||||
if (!trimmed) return null;
|
||||
const lastColon = trimmed.lastIndexOf(':');
|
||||
if (lastColon === -1) return null;
|
||||
return {
|
||||
host: trimmed.substring(0, lastColon),
|
||||
port: parseInt(trimmed.substring(lastColon + 1), 10),
|
||||
};
|
||||
}).filter((t): t is { host: string; port: number } => t !== null && !isNaN(t.port))
|
||||
: [];
|
||||
const routeRefs = data.routeRefs
|
||||
? String(data.routeRefs).split(',').map((s: string) => s.trim()).filter(Boolean)
|
||||
: [];
|
||||
|
||||
await appstate.targetProfilesStatePart.dispatchAction(appstate.updateTargetProfileAction, {
|
||||
id: profile.id,
|
||||
name: String(data.name),
|
||||
description: data.description ? String(data.description) : undefined,
|
||||
domains,
|
||||
targets,
|
||||
routeRefs,
|
||||
});
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private async showDetailDialog(profile: interfaces.data.ITargetProfile) {
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
|
||||
// Fetch usage (which VPN clients reference this profile)
|
||||
let usageHtml = html`<p style="color: #9ca3af;">Loading usage...</p>`;
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetTargetProfileUsage
|
||||
>('/typedrequest', 'getTargetProfileUsage');
|
||||
const response = await request.fire({
|
||||
identity: appstate.loginStatePart.getState()!.identity!,
|
||||
id: profile.id,
|
||||
});
|
||||
if (response.clients.length > 0) {
|
||||
usageHtml = html`
|
||||
<div style="margin-top: 8px;">
|
||||
${response.clients.map(c => html`
|
||||
<div style="padding: 4px 0; font-size: 13px;">
|
||||
<strong>${c.clientId}</strong>${c.description ? html` - ${c.description}` : ''}
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
usageHtml = html`<p style="color: #9ca3af; font-size: 13px;">No VPN clients reference this profile.</p>`;
|
||||
}
|
||||
} catch {
|
||||
usageHtml = html`<p style="color: #9ca3af;">Usage data unavailable.</p>`;
|
||||
}
|
||||
|
||||
DeesModal.createAndShow({
|
||||
heading: `Target Profile: ${profile.name}`,
|
||||
content: html`
|
||||
<div style="display: flex; flex-direction: column; gap: 12px;">
|
||||
<div>
|
||||
<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};">Description</div>
|
||||
<div style="font-size: 14px; margin-top: 4px;">${profile.description || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};">Domains</div>
|
||||
<div style="font-size: 14px; margin-top: 4px;">
|
||||
${profile.domains?.length
|
||||
? profile.domains.map(d => html`<span class="tagBadge">${d}</span>`)
|
||||
: '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};">Targets</div>
|
||||
<div style="font-size: 14px; margin-top: 4px;">
|
||||
${profile.targets?.length
|
||||
? profile.targets.map(t => html`<span class="tagBadge">${t.host}:${t.port}</span>`)
|
||||
: '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};">Route Refs</div>
|
||||
<div style="font-size: 14px; margin-top: 4px;">
|
||||
${profile.routeRefs?.length
|
||||
? profile.routeRefs.map(r => html`<span class="tagBadge">${r}</span>`)
|
||||
: '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};">Created</div>
|
||||
<div style="font-size: 14px; margin-top: 4px;">${new Date(profile.createdAt).toLocaleString()} by ${profile.createdBy}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};">Updated</div>
|
||||
<div style="font-size: 14px; margin-top: 4px;">${new Date(profile.updatedAt).toLocaleString()}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};">VPN Clients Using This Profile</div>
|
||||
${usageHtml}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'Close', iconName: 'lucide:x', action: async (m: any) => await m.destroy() },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private async deleteProfile(profile: interfaces.data.ITargetProfile) {
|
||||
await appstate.targetProfilesStatePart.dispatchAction(appstate.deleteTargetProfileAction, {
|
||||
id: profile.id,
|
||||
force: false,
|
||||
});
|
||||
|
||||
const currentState = appstate.targetProfilesStatePart.getState()!;
|
||||
if (currentState.error?.includes('in use')) {
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
DeesModal.createAndShow({
|
||||
heading: 'Profile In Use',
|
||||
content: html`<p>${currentState.error} Force delete?</p>`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Force Delete',
|
||||
iconName: 'lucide:trash2',
|
||||
action: async (modalArg: any) => {
|
||||
await appstate.targetProfilesStatePart.dispatchAction(appstate.deleteTargetProfileAction, {
|
||||
id: profile.id,
|
||||
force: true,
|
||||
});
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{ name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => modalArg.destroy() },
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -327,8 +327,8 @@ export class OpsViewVpn extends DeesElement {
|
||||
'Status': statusHtml,
|
||||
'Routing': routingHtml,
|
||||
'VPN IP': client.assignedIp || '-',
|
||||
'Tags': client.serverDefinedClientTags?.length
|
||||
? html`${client.serverDefinedClientTags.map(t => html`<span class="tagBadge">${t}</span>`)}`
|
||||
'Target Profiles': client.targetProfileIds?.length
|
||||
? html`${client.targetProfileIds.map(t => html`<span class="tagBadge">${t}</span>`)}`
|
||||
: '-',
|
||||
'Description': client.description || '-',
|
||||
'Created': new Date(client.createdAt).toLocaleDateString(),
|
||||
@@ -347,7 +347,7 @@ export class OpsViewVpn extends DeesElement {
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'clientId'} .label=${'Client ID'} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'description'} .label=${'Description'}></dees-input-text>
|
||||
<dees-input-text .key=${'tags'} .label=${'Server-Defined Tags (comma-separated)'}></dees-input-text>
|
||||
<dees-input-text .key=${'targetProfileIds'} .label=${'Target Profile IDs (comma-separated)'}></dees-input-text>
|
||||
<dees-input-checkbox .key=${'forceDestinationSmartproxy'} .label=${'Force traffic through SmartProxy'} .value=${true}></dees-input-checkbox>
|
||||
<div class="hostIpGroup" style="display: none; flex-direction: column; gap: 16px;">
|
||||
<dees-input-checkbox .key=${'useHostIp'} .label=${'Get Host IP'} .value=${false}></dees-input-checkbox>
|
||||
@@ -383,8 +383,8 @@ export class OpsViewVpn extends DeesElement {
|
||||
if (!form) return;
|
||||
const data = await form.collectFormData();
|
||||
if (!data.clientId) return;
|
||||
const serverDefinedClientTags = data.tags
|
||||
? data.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
|
||||
const targetProfileIds = data.targetProfileIds
|
||||
? data.targetProfileIds.split(',').map((t: string) => t.trim()).filter(Boolean)
|
||||
: undefined;
|
||||
|
||||
// Apply conditional logic based on checkbox states
|
||||
@@ -406,7 +406,7 @@ export class OpsViewVpn extends DeesElement {
|
||||
await appstate.vpnStatePart.dispatchAction(appstate.createVpnClientAction, {
|
||||
clientId: data.clientId,
|
||||
description: data.description || undefined,
|
||||
serverDefinedClientTags,
|
||||
targetProfileIds,
|
||||
forceDestinationSmartproxy: forceSmartproxy,
|
||||
useHostIp: useHostIp || undefined,
|
||||
useDhcp: useDhcp || undefined,
|
||||
@@ -479,7 +479,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">Tags</span><span class="infoValue">${client.serverDefinedClientTags?.join(', ') || '-'}</span></div>
|
||||
<div class="infoItem"><span class="infoLabel">Target Profiles</span><span class="infoValue">${client.targetProfileIds?.join(', ') || '-'}</span></div>
|
||||
<div class="infoItem"><span class="infoLabel">Routing</span><span class="infoValue">${client.forceDestinationSmartproxy !== false ? 'SmartProxy' : client.useHostIp ? 'Host IP' : 'Direct'}</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>
|
||||
@@ -643,7 +643,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 currentTags = client.serverDefinedClientTags?.join(', ') ?? '';
|
||||
const currentTargetProfileIds = client.targetProfileIds?.join(', ') ?? '';
|
||||
const currentForceSmartproxy = client.forceDestinationSmartproxy ?? true;
|
||||
const currentUseHostIp = client.useHostIp ?? false;
|
||||
const currentUseDhcp = client.useDhcp ?? false;
|
||||
@@ -659,7 +659,7 @@ export class OpsViewVpn extends DeesElement {
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'description'} .label=${'Description'} .value=${currentDescription}></dees-input-text>
|
||||
<dees-input-text .key=${'tags'} .label=${'Server-Defined Tags (comma-separated)'} .value=${currentTags}></dees-input-text>
|
||||
<dees-input-text .key=${'targetProfileIds'} .label=${'Target Profile IDs (comma-separated)'} .value=${currentTargetProfileIds}></dees-input-text>
|
||||
<dees-input-checkbox .key=${'forceDestinationSmartproxy'} .label=${'Force traffic through SmartProxy'} .value=${currentForceSmartproxy}></dees-input-checkbox>
|
||||
<div class="hostIpGroup" style="display: ${currentForceSmartproxy ? 'none' : 'flex'}; flex-direction: column; gap: 16px;">
|
||||
<dees-input-checkbox .key=${'useHostIp'} .label=${'Get Host IP'} .value=${currentUseHostIp}></dees-input-checkbox>
|
||||
@@ -690,8 +690,8 @@ export class OpsViewVpn extends DeesElement {
|
||||
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
||||
if (!form) return;
|
||||
const data = await form.collectFormData();
|
||||
const serverDefinedClientTags = data.tags
|
||||
? data.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
|
||||
const targetProfileIds = data.targetProfileIds
|
||||
? data.targetProfileIds.split(',').map((t: string) => t.trim()).filter(Boolean)
|
||||
: [];
|
||||
|
||||
// Apply conditional logic based on checkbox states
|
||||
@@ -713,7 +713,7 @@ export class OpsViewVpn extends DeesElement {
|
||||
await appstate.vpnStatePart.dispatchAction(appstate.updateVpnClientAction, {
|
||||
clientId: client.clientId,
|
||||
description: data.description || undefined,
|
||||
serverDefinedClientTags,
|
||||
targetProfileIds,
|
||||
forceDestinationSmartproxy: forceSmartproxy,
|
||||
useHostIp: useHostIp || undefined,
|
||||
useDhcp: useDhcp || undefined,
|
||||
|
||||
Reference in New Issue
Block a user