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` Target Profiles
({ Name: profile.name, Description: profile.description || '-', Domains: profile.domains?.length ? html`${profile.domains.map(d => html`${d}`)}` : '-', Targets: profile.targets?.length ? html`${profile.targets.map(t => html`${t.host}:${t.port}`)}` : '-', 'Route Refs': profile.routeRefs?.length ? html`${profile.routeRefs.map(r => html`${r}`)}` : '-', 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); }, }, ]} >
`; } private async showCreateProfileDialog() { const { DeesModal } = await import('@design.estate/dees-catalog'); DeesModal.createAndShow({ heading: 'Create Target Profile', content: html` `, 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` `, 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`

Loading usage...

`; 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`
${response.clients.map(c => html`
${c.clientId}${c.description ? html` - ${c.description}` : ''}
`)}
`; } else { usageHtml = html`

No VPN clients reference this profile.

`; } } catch { usageHtml = html`

Usage data unavailable.

`; } DeesModal.createAndShow({ heading: `Target Profile: ${profile.name}`, content: html`
Description
${profile.description || '-'}
Domains
${profile.domains?.length ? profile.domains.map(d => html`${d}`) : '-'}
Targets
${profile.targets?.length ? profile.targets.map(t => html`${t.host}:${t.port}`) : '-'}
Route Refs
${profile.routeRefs?.length ? profile.routeRefs.map(r => html`${r}`) : '-'}
Created
${new Date(profile.createdAt).toLocaleString()} by ${profile.createdBy}
Updated
${new Date(profile.updatedAt).toLocaleString()}
VPN Clients Using This Profile
${usageHtml}
`, 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`

${currentState.error} Force delete?

`, 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() }, ], }); } } }