2026-04-05 00:37:37 +00:00
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 8 px ;
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 `
2026-04-05 10:13:09 +00:00
< dees - heading level = "2" > Target Profiles < / d e e s - h e a d i n g >
2026-04-05 00:37:37 +00:00
< div class = "profilesContainer" >
< dees - statsgrid .tiles = $ { statsTiles } > < / d e e s - s t a t s g r i d >
< 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 ) ;
} ,
} ,
] }
> < / d e e s - t a b l e >
< / div >
` ;
}
2026-04-05 11:29:47 +00:00
private getRouteCandidates() {
const routeState = appstate . routeManagementStatePart . getState ( ) ;
const routes = routeState ? . mergedRoutes || [ ] ;
return routes
. filter ( ( mr ) = > mr . route . name )
. map ( ( mr ) = > ( { viewKey : mr.route.name ! } ) ) ;
}
2026-04-05 00:37:37 +00:00
private async showCreateProfileDialog() {
const { DeesModal } = await import ( '@design.estate/dees-catalog' ) ;
2026-04-05 11:29:47 +00:00
const routeCandidates = this . getRouteCandidates ( ) ;
2026-04-05 00:37:37 +00:00
DeesModal . createAndShow ( {
heading : 'Create Target Profile' ,
content : html `
< dees - form >
< dees - input - text .key = $ { 'name' } .label = $ { 'Name' } .required = $ { true } > < / d e e s - i n p u t - t e x t >
< dees - input - text .key = $ { 'description' } .label = $ { 'Description' } > < / d e e s - i n p u t - t e x t >
2026-04-05 11:29:47 +00:00
< dees - input - list .key = $ { 'domains' } .label = $ { 'Domains' } .placeholder = $ { 'e.g. *.example.com' } .allowFreeform = $ { true } > < / d e e s - i n p u t - l i s t >
< dees - input - list .key = $ { 'targets' } .label = $ { 'Targets (host:port)' } .placeholder = $ { 'e.g. 10.0.0.1:443' } .allowFreeform = $ { true } > < / d e e s - i n p u t - l i s t >
< dees - input - list .key = $ { 'routeRefs' } .label = $ { 'Route Refs' } .placeholder = $ { 'Type to search routes...' } .candidates = $ { routeCandidates } .allowFreeform = $ { true } > < / d e e s - i n p u t - l i s t >
2026-04-05 00:37:37 +00:00
< / d e e s - f o r m >
` ,
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 ;
2026-04-05 11:29:47 +00:00
const domains : string [ ] = Array . isArray ( data . domains ) ? data . domains : [ ] ;
const targetStrings : string [ ] = Array . isArray ( data . targets ) ? data . targets : [ ] ;
const targets = targetStrings
. map ( ( s : string ) = > {
const lastColon = s . lastIndexOf ( ':' ) ;
if ( lastColon === - 1 ) return null ;
return {
host : s.substring ( 0 , lastColon ) ,
port : parseInt ( s . substring ( lastColon + 1 ) , 10 ) ,
} ;
} )
. filter ( ( t ) : t is { host : string ; port : number } = > t !== null && ! isNaN ( t . port ) ) ;
const routeRefs : string [ ] = Array . isArray ( data . routeRefs ) ? data . routeRefs : [ ] ;
2026-04-05 00:37:37 +00:00
await appstate . targetProfilesStatePart . dispatchAction ( appstate . createTargetProfileAction , {
name : String ( data . name ) ,
description : data.description ? String ( data . description ) : undefined ,
2026-04-05 11:29:47 +00:00
domains : domains.length > 0 ? domains : undefined ,
targets : targets.length > 0 ? targets : undefined ,
routeRefs : routeRefs.length > 0 ? routeRefs : undefined ,
2026-04-05 00:37:37 +00:00
} ) ;
modalArg . destroy ( ) ;
} ,
} ,
] ,
} ) ;
}
private async showEditProfileDialog ( profile : interfaces.data.ITargetProfile ) {
2026-04-05 11:29:47 +00:00
const currentDomains = profile . domains || [ ] ;
const currentTargets = profile . targets ? . map ( t = > ` ${ t . host } : ${ t . port } ` ) || [ ] ;
const currentRouteRefs = profile . routeRefs || [ ] ;
2026-04-05 00:37:37 +00:00
const { DeesModal } = await import ( '@design.estate/dees-catalog' ) ;
2026-04-05 11:29:47 +00:00
const routeCandidates = this . getRouteCandidates ( ) ;
2026-04-05 00:37:37 +00:00
DeesModal . createAndShow ( {
heading : ` Edit Profile: ${ profile . name } ` ,
content : html `
< dees - form >
< dees - input - text .key = $ { 'name' } .label = $ { 'Name' } .value = $ { profile.name } > < / d e e s - i n p u t - t e x t >
< dees - input - text .key = $ { 'description' } .label = $ { 'Description' } .value = $ { profile.description | | '' } > < / d e e s - i n p u t - t e x t >
2026-04-05 11:29:47 +00:00
< dees - input - list .key = $ { 'domains' } .label = $ { 'Domains' } .placeholder = $ { 'e.g. *.example.com' } .allowFreeform = $ { true } .value = $ { currentDomains } > < / d e e s - i n p u t - l i s t >
< dees - input - list .key = $ { 'targets' } .label = $ { 'Targets (host:port)' } .placeholder = $ { 'e.g. 10.0.0.1:443' } .allowFreeform = $ { true } .value = $ { currentTargets } > < / d e e s - i n p u t - l i s t >
< dees - input - list .key = $ { 'routeRefs' } .label = $ { 'Route Refs' } .placeholder = $ { 'Type to search routes...' } .candidates = $ { routeCandidates } .allowFreeform = $ { true } .value = $ { currentRouteRefs } > < / d e e s - i n p u t - l i s t >
2026-04-05 00:37:37 +00:00
< / d e e s - f o r m >
` ,
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 ( ) ;
2026-04-05 11:29:47 +00:00
const domains : string [ ] = Array . isArray ( data . domains ) ? data . domains : [ ] ;
const targetStrings : string [ ] = Array . isArray ( data . targets ) ? data . targets : [ ] ;
const targets = targetStrings
. map ( ( s : string ) = > {
const lastColon = s . lastIndexOf ( ':' ) ;
if ( lastColon === - 1 ) return null ;
return {
host : s.substring ( 0 , lastColon ) ,
port : parseInt ( s . substring ( lastColon + 1 ) , 10 ) ,
} ;
} )
. filter ( ( t ) : t is { host : string ; port : number } = > t !== null && ! isNaN ( t . port ) ) ;
const routeRefs : string [ ] = Array . isArray ( data . routeRefs ) ? data . routeRefs : [ ] ;
2026-04-05 00:37:37 +00:00
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 ( ) } ,
] ,
} ) ;
}
}
}