2026-04-08 07:45:26 +00:00
import * as appstate from '../../appstate.js' ;
import * as interfaces from '../../../dist_ts_interfaces/index.js' ;
2026-04-08 08:24:55 +00:00
import { viewHostCss } from '../shared/css.js' ;
2026-02-23 12:40:26 +00:00
import { type IStatsTile } from '@design.estate/dees-catalog' ;
import {
DeesElement ,
css ,
cssManager ,
customElement ,
html ,
state ,
type TemplateResult ,
} from '@design.estate/dees-element' ;
2026-04-04 21:23:16 +00:00
// TLS dropdown options shared by create and edit dialogs
const tlsModeOptions = [
{ key : 'none' , option : '(none — no TLS)' } ,
{ key : 'passthrough' , option : 'Passthrough' } ,
{ key : 'terminate' , option : 'Terminate' } ,
{ key : 'terminate-and-reencrypt' , option : 'Terminate & Re-encrypt' } ,
] ;
const tlsCertOptions = [
{ key : 'auto' , option : 'Auto (ACME/Let\'s Encrypt)' } ,
{ key : 'custom' , option : 'Custom certificate' } ,
] ;
/ * *
* Toggle TLS form field visibility based on selected TLS mode and certificate type .
* /
function setupTlsVisibility ( formEl : any ) {
const updateVisibility = async ( ) = > {
const data = await formEl . collectFormData ( ) ;
const contentEl = formEl . closest ( '.content' ) || formEl . parentElement ;
if ( ! contentEl ) return ;
const tlsModeValue = data . tlsMode ;
const modeKey = typeof tlsModeValue === 'string' ? tlsModeValue : tlsModeValue?.key ;
const needsCert = modeKey === 'terminate' || modeKey === 'terminate-and-reencrypt' ;
const certGroup = contentEl . querySelector ( '.tlsCertificateGroup' ) as HTMLElement ;
if ( certGroup ) certGroup . style . display = needsCert ? 'flex' : 'none' ;
const tlsCertValue = data . tlsCertificate ;
const certKey = typeof tlsCertValue === 'string' ? tlsCertValue : tlsCertValue?.key ;
const customGroup = contentEl . querySelector ( '.tlsCustomCertGroup' ) as HTMLElement ;
if ( customGroup ) customGroup . style . display = ( needsCert && certKey === 'custom' ) ? 'flex' : 'none' ;
} ;
formEl . changeSubject . subscribe ( ( ) = > updateVisibility ( ) ) ;
updateVisibility ( ) ;
}
2026-02-23 12:40:26 +00:00
@customElement ( 'ops-view-routes' )
export class OpsViewRoutes extends DeesElement {
@state ( ) accessor routeState : appstate.IRouteManagementState = {
mergedRoutes : [ ] ,
warnings : [ ] ,
apiTokens : [ ] ,
isLoading : false ,
error : null ,
lastUpdated : 0 ,
} ;
2026-04-02 17:27:05 +00:00
@state ( ) accessor profilesTargetsState : appstate.IProfilesTargetsState = {
profiles : [ ] ,
targets : [ ] ,
isLoading : false ,
error : null ,
lastUpdated : 0 ,
} ;
2026-02-23 12:40:26 +00:00
constructor ( ) {
super ( ) ;
const sub = appstate . routeManagementStatePart
. select ( ( s ) = > s )
. subscribe ( ( routeState ) = > {
this . routeState = routeState ;
} ) ;
this . rxSubscriptions . push ( sub ) ;
2026-04-02 17:27:05 +00:00
const ptSub = appstate . profilesTargetsStatePart
. select ( ( s ) = > s )
. subscribe ( ( ptState ) = > {
this . profilesTargetsState = ptState ;
} ) ;
this . rxSubscriptions . push ( ptSub ) ;
2026-02-23 12:40:26 +00:00
// Re-fetch routes when user logs in (fixes race condition where
// the view is created before authentication completes)
const loginSub = appstate . loginStatePart
. select ( ( s ) = > s . isLoggedIn )
. subscribe ( ( isLoggedIn ) = > {
if ( isLoggedIn ) {
appstate . routeManagementStatePart . dispatchAction ( appstate . fetchMergedRoutesAction , null ) ;
2026-04-02 17:27:05 +00:00
appstate . profilesTargetsStatePart . dispatchAction ( appstate . fetchProfilesAndTargetsAction , null ) ;
2026-02-23 12:40:26 +00:00
}
} ) ;
this . rxSubscriptions . push ( loginSub ) ;
}
public static styles = [
cssManager . defaultStyles ,
2026-04-08 08:24:55 +00:00
viewHostCss ,
2026-02-23 12:40:26 +00:00
css `
. routesContainer {
display : flex ;
flex - direction : column ;
gap : 24px ;
}
. warnings - bar {
background : $ { cssManager . bdTheme ( 'rgba(255, 170, 0, 0.08)' , 'rgba(255, 170, 0, 0.1)' ) } ;
border : 1px solid $ { cssManager . bdTheme ( 'rgba(255, 170, 0, 0.25)' , 'rgba(255, 170, 0, 0.3)' ) } ;
border - radius : 8px ;
padding : 12px 16 px ;
}
. warning - item {
display : flex ;
align - items : center ;
gap : 8px ;
padding : 4px 0 ;
font - size : 13px ;
color : $ { cssManager . bdTheme ( '#b45309' , '#fa0' ) } ;
}
. warning - icon {
flex - shrink : 0 ;
}
. empty - state {
text - align : center ;
padding : 48px 24 px ;
color : $ { cssManager . bdTheme ( '#6b7280' , '#666' ) } ;
}
. empty - state p {
margin : 8px 0 ;
}
` ,
] ;
public render ( ) : TemplateResult {
const { mergedRoutes , warnings } = this . routeState ;
const disabledCount = mergedRoutes . filter ( ( mr ) = > ! mr . enabled ) . length ;
2026-04-13 17:38:23 +00:00
const configCount = mergedRoutes . filter ( ( mr ) = > mr . origin !== 'api' ) . length ;
const apiCount = mergedRoutes . filter ( ( mr ) = > mr . origin === 'api' ) . length ;
2026-02-23 12:40:26 +00:00
const statsTiles : IStatsTile [ ] = [
{
id : 'totalRoutes' ,
title : 'Total Routes' ,
type : 'number' ,
value : mergedRoutes.length ,
icon : 'lucide:route' ,
description : 'All configured routes' ,
color : '#3b82f6' ,
} ,
{
2026-04-13 17:38:23 +00:00
id : 'configRoutes' ,
title : 'From Config' ,
2026-02-23 12:40:26 +00:00
type : 'number' ,
2026-04-13 17:38:23 +00:00
value : configCount ,
icon : 'lucide:settings' ,
description : 'Seeded from config/email/DNS' ,
2026-02-23 12:40:26 +00:00
color : '#8b5cf6' ,
} ,
{
2026-04-13 17:38:23 +00:00
id : 'apiRoutes' ,
title : 'API Created' ,
2026-02-23 12:40:26 +00:00
type : 'number' ,
2026-04-13 17:38:23 +00:00
value : apiCount ,
2026-02-23 12:40:26 +00:00
icon : 'lucide:code' ,
description : 'Routes added via API' ,
color : '#0ea5e9' ,
} ,
{
id : 'disabled' ,
title : 'Disabled' ,
type : 'number' ,
value : disabledCount ,
icon : 'lucide:pauseCircle' ,
description : 'Currently disabled routes' ,
color : disabledCount > 0 ? '#ef4444' : '#6b7280' ,
} ,
] ;
// Map merged routes to sz-route-list-view format
const szRoutes = mergedRoutes . map ( ( mr ) = > {
const tags = [ . . . ( mr . route . tags || [ ] ) ] ;
2026-04-13 17:38:23 +00:00
tags . push ( mr . origin ) ;
2026-02-23 12:40:26 +00:00
if ( ! mr . enabled ) tags . push ( 'disabled' ) ;
return {
. . . mr . route ,
enabled : mr.enabled ,
tags ,
2026-04-13 17:38:23 +00:00
id : mr.id || mr . route . name || undefined ,
2026-04-02 17:27:05 +00:00
metadata : mr.metadata ,
2026-02-23 12:40:26 +00:00
} ;
} ) ;
return html `
2026-04-08 11:08:18 +00:00
< dees - heading level = "3" > Route Management < / d e e s - h e a d i n g >
2026-02-23 12:40:26 +00:00
< div class = "routesContainer" >
< dees - statsgrid
. tiles = $ { statsTiles }
. gridActions = $ { [
{
name : 'Add Route' ,
iconName : 'lucide:plus' ,
action : ( ) = > this . showCreateRouteDialog ( ) ,
} ,
{
name : 'Refresh' ,
iconName : 'lucide:refreshCw' ,
action : ( ) = > this . refreshData ( ) ,
} ,
] }
> < / d e e s - s t a t s g r i d >
$ { warnings . length > 0
? html `
< div class = "warnings-bar" >
$ { warnings . map (
( w ) = > html `
< div class = "warning-item" >
< span class = "warning-icon" > & # 9888 ; < / span >
< span > $ { w . message } < / span >
< / div >
` ,
) }
< / div >
`
: '' }
$ { szRoutes . length > 0
? html `
< sz - route - list - view
. routes = $ { szRoutes }
@route - click = $ { ( e : CustomEvent ) = > this . handleRouteClick ( e ) }
2026-04-02 22:37:49 +00:00
@route - edit = $ { ( e : CustomEvent ) = > this . handleRouteEdit ( e ) }
@route - delete = $ { ( e : CustomEvent ) = > this . handleRouteDelete ( e ) }
2026-02-23 12:40:26 +00:00
> < / s z - r o u t e - l i s t - v i e w >
`
: html `
< div class = "empty-state" >
< p > No routes configured < / p >
2026-04-13 17:38:23 +00:00
< p > Add a route to get started . < / p >
2026-02-23 12:40:26 +00:00
< / div >
` }
< / div >
` ;
}
private async handleRouteClick ( e : CustomEvent ) {
const clickedRoute = e . detail ;
if ( ! clickedRoute ) return ;
// Find the corresponding merged route
const merged = this . routeState . mergedRoutes . find (
( mr ) = > mr . route . name === clickedRoute . name ,
) ;
if ( ! merged ) return ;
const { DeesModal } = await import ( '@design.estate/dees-catalog' ) ;
2026-04-13 17:38:23 +00:00
const meta = merged . metadata ;
await DeesModal . createAndShow ( {
heading : ` Route: ${ merged . route . name } ` ,
content : html `
< div style = "color: #ccc; padding: 8px 0;" >
< p > Origin : < strong style = "color: #0af;" > $ { merged . origin } < / strong > < / p >
< p > Status : < strong > $ { merged . enabled ? 'Enabled' : 'Disabled' } < / strong > < / p >
< p > ID : < code style = "color: #888;" > $ { merged . id } < / code > < / 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 >
` ,
menuOptions : [
{
name : merged.enabled ? 'Disable' : 'Enable' ,
iconName : merged.enabled ? 'lucide:pause' : 'lucide:play' ,
action : async ( modalArg : any ) = > {
await appstate . routeManagementStatePart . dispatchAction (
appstate . toggleRouteAction ,
{ id : merged.id , enabled : ! merged . enabled } ,
) ;
await modalArg . destroy ( ) ;
2026-02-23 12:40:26 +00:00
} ,
2026-04-13 17:38:23 +00:00
} ,
{
name : 'Edit' ,
iconName : 'lucide:pencil' ,
action : async ( modalArg : any ) = > {
await modalArg . destroy ( ) ;
this . showEditRouteDialog ( merged ) ;
2026-02-23 12:40:26 +00:00
} ,
2026-04-13 17:38:23 +00:00
} ,
{
name : 'Delete' ,
iconName : 'lucide:trash-2' ,
action : async ( modalArg : any ) = > {
await appstate . routeManagementStatePart . dispatchAction (
appstate . deleteRouteAction ,
merged . id ,
) ;
await modalArg . destroy ( ) ;
2026-02-23 12:40:26 +00:00
} ,
2026-04-13 17:38:23 +00:00
} ,
{
name : 'Close' ,
iconName : 'lucide:x' ,
action : async ( modalArg : any ) = > await modalArg . destroy ( ) ,
} ,
] ,
} ) ;
2026-02-23 12:40:26 +00:00
}
2026-04-02 22:37:49 +00:00
private async handleRouteEdit ( e : CustomEvent ) {
const clickedRoute = e . detail ;
if ( ! clickedRoute ) return ;
const merged = this . routeState . mergedRoutes . find (
( mr ) = > mr . route . name === clickedRoute . name ,
) ;
2026-04-13 17:38:23 +00:00
if ( ! merged ) return ;
2026-04-02 22:37:49 +00:00
this . showEditRouteDialog ( merged ) ;
}
private async handleRouteDelete ( e : CustomEvent ) {
const clickedRoute = e . detail ;
if ( ! clickedRoute ) return ;
const merged = this . routeState . mergedRoutes . find (
( mr ) = > mr . route . name === clickedRoute . name ,
) ;
2026-04-13 17:38:23 +00:00
if ( ! merged ) return ;
2026-04-02 22:37:49 +00:00
const { DeesModal } = await import ( '@design.estate/dees-catalog' ) ;
await DeesModal . createAndShow ( {
heading : ` Delete Route: ${ merged . route . name } ` ,
content : html `
< div style = "color: #ccc; padding: 8px 0;" >
< p > Are you sure you want to delete this route ? This action cannot be undone . < / p >
< / div >
` ,
menuOptions : [
{
name : 'Cancel' ,
iconName : 'lucide:x' ,
action : async ( modalArg : any ) = > await modalArg . destroy ( ) ,
} ,
{
name : 'Delete' ,
iconName : 'lucide:trash-2' ,
action : async ( modalArg : any ) = > {
await appstate . routeManagementStatePart . dispatchAction (
appstate . deleteRouteAction ,
2026-04-13 17:38:23 +00:00
merged . id ,
2026-04-02 22:37:49 +00:00
) ;
await modalArg . destroy ( ) ;
} ,
} ,
] ,
} ) ;
}
private async showEditRouteDialog ( merged : interfaces.data.IMergedRoute ) {
const { DeesModal } = await import ( '@design.estate/dees-catalog' ) ;
const profiles = this . profilesTargetsState . profiles ;
const targets = this . profilesTargetsState . targets ;
const profileOptions = [
{ key : '' , option : '(none — inline security)' } ,
. . . profiles . map ( ( p ) = > ( {
key : p.id ,
option : ` ${ p . name } ${ p . description ? ' — ' + p . description : '' } ` ,
} ) ) ,
] ;
const targetOptions = [
{ key : '' , option : '(none — inline target)' } ,
. . . targets . map ( ( t ) = > ( {
key : t.id ,
option : ` ${ t . name } ( ${ Array . isArray ( t . host ) ? t . host . join ( ',' ) : t . host } : ${ t . port } ) ` ,
} ) ) ,
] ;
const route = merged . route ;
const currentPorts = Array . isArray ( route . match . ports )
? route . match . ports . map ( ( p : any ) = > typeof p === 'number' ? String ( p ) : ` ${ p . from } - ${ p . to } ` ) . join ( ', ' )
: String ( route . match . ports ) ;
2026-04-02 22:55:57 +00:00
const currentDomains : string [ ] = route . match . domains
? ( Array . isArray ( route . match . domains ) ? route . match . domains : [ route . match . domains ] )
: [ ] ;
2026-04-02 22:37:49 +00:00
const firstTarget = route . action . targets ? . [ 0 ] ;
const currentTargetHost = firstTarget
? ( Array . isArray ( firstTarget . host ) ? firstTarget . host [ 0 ] : firstTarget . host )
: '' ;
const currentTargetPort = firstTarget ? . port != null ? String ( firstTarget . port ) : '' ;
2026-04-04 21:23:16 +00:00
// Compute current TLS state for pre-population
const currentTls = ( route . action as any ) . tls ;
const currentTlsMode = currentTls ? . mode || 'none' ;
const currentTlsCert = currentTls
? ( currentTls . certificate === 'auto' || ! currentTls . certificate ? 'auto' : 'custom' )
: 'auto' ;
const currentCustomKey = ( typeof currentTls ? . certificate === 'object' ) ? currentTls . certificate . key : '' ;
const currentCustomCert = ( typeof currentTls ? . certificate === 'object' ) ? currentTls . certificate . cert : '' ;
const needsCert = currentTlsMode === 'terminate' || currentTlsMode === 'terminate-and-reencrypt' ;
const isCustom = currentTlsCert === 'custom' ;
const editModal = await DeesModal . createAndShow ( {
2026-04-02 22:37:49 +00:00
heading : ` Edit Route: ${ route . name } ` ,
content : html `
< dees - form >
< dees - input - text .key = $ { 'name' } .label = $ { 'Route Name' } .value = $ { route.name | | '' } .required = $ { true } > < / d e e s - i n p u t - t e x t >
2026-04-12 19:42:07 +00:00
< dees - input - text .key = $ { 'ports' } .label = $ { 'Ports' } .description = $ { 'Comma-separated, e.g. 80, 443' } .value = $ { currentPorts } .required = $ { true } > < / d e e s - i n p u t - t e x t >
2026-04-02 22:55:57 +00:00
< dees - input - list .key = $ { 'domains' } .label = $ { 'Domains' } .placeholder = $ { 'Add domain...' } .value = $ { currentDomains } > < / d e e s - i n p u t - l i s t >
2026-04-12 19:42:07 +00:00
< dees - input - text .key = $ { 'priority' } .label = $ { 'Priority' } .description = $ { 'Higher values are matched first' } .value = $ { route.priority ! = null ? String ( route.priority ) : '' } > < / d e e s - i n p u t - t e x t >
2026-04-05 00:37:37 +00:00
< dees - input - dropdown .key = $ { 'sourceProfileRef' } .label = $ { 'Source Profile' } .options = $ { profileOptions } .selectedOption = $ { profileOptions.find ( ( o ) = > o . key === ( merged . metadata ? . sourceProfileRef || '' ) ) || null } > < / d e e s - i n p u t - d r o p d o w n >
2026-04-04 11:00:03 +00:00
< dees - input - dropdown .key = $ { 'networkTargetRef' } .label = $ { 'Network Target' } .options = $ { targetOptions } .selectedOption = $ { targetOptions.find ( ( o ) = > o . key === ( merged . metadata ? . networkTargetRef || '' ) ) || null } > < / d e e s - i n p u t - d r o p d o w n >
2026-04-12 19:42:07 +00:00
< dees - input - text .key = $ { 'targetHost' } .label = $ { 'Target Host' } .description = $ { 'Used when no network target is selected' } .value = $ { currentTargetHost } > < / d e e s - i n p u t - t e x t >
< dees - input - text .key = $ { 'targetPort' } .label = $ { 'Target Port' } .description = $ { 'Used when no network target is selected' } .value = $ { currentTargetPort } > < / d e e s - i n p u t - t e x t >
2026-04-04 21:23:16 +00:00
< dees - input - dropdown .key = $ { 'tlsMode' } .label = $ { 'TLS Mode' } .options = $ { tlsModeOptions } .selectedOption = $ { tlsModeOptions.find ( ( o ) = > o . key === currentTlsMode ) || tlsModeOptions [ 0 ] } > < / d e e s - i n p u t - d r o p d o w n >
< div class = "tlsCertificateGroup" style = "display: ${needsCert ? 'flex' : 'none'}; flex-direction: column; gap: 16px;" >
< dees - input - dropdown .key = $ { 'tlsCertificate' } .label = $ { 'Certificate' } .options = $ { tlsCertOptions } .selectedOption = $ { tlsCertOptions.find ( ( o ) = > o . key === currentTlsCert ) || tlsCertOptions [ 0 ] } > < / d e e s - i n p u t - d r o p d o w n >
< div class = "tlsCustomCertGroup" style = "display: ${needsCert && isCustom ? 'flex' : 'none'}; flex-direction: column; gap: 16px;" >
2026-04-12 19:42:07 +00:00
< dees - input - text .key = $ { 'tlsCertKey' } .label = $ { 'Private Key' } .description = $ { 'PEM-encoded private key' } .value = $ { currentCustomKey } > < / d e e s - i n p u t - t e x t >
< dees - input - text .key = $ { 'tlsCertCert' } .label = $ { 'Certificate' } .description = $ { 'PEM-encoded certificate' } .value = $ { currentCustomCert } > < / d e e s - i n p u t - t e x t >
2026-04-04 21:23:16 +00:00
< / div >
< / div >
2026-04-02 22:37:49 +00:00
< / d e e s - f o r m >
` ,
menuOptions : [
{
name : 'Cancel' ,
iconName : 'lucide:x' ,
action : async ( modalArg : any ) = > await modalArg . destroy ( ) ,
} ,
{
name : 'Save' ,
iconName : 'lucide:check' ,
action : async ( modalArg : any ) = > {
const form = modalArg . shadowRoot ? . querySelector ( '.content' ) ? . querySelector ( 'dees-form' ) ;
if ( ! form ) return ;
const formData = await form . collectFormData ( ) ;
if ( ! formData . name || ! formData . ports ) return ;
const ports = formData . ports . split ( ',' ) . map ( ( p : string ) = > parseInt ( p . trim ( ) , 10 ) ) . filter ( ( p : number ) = > ! isNaN ( p ) ) ;
2026-04-02 22:55:57 +00:00
const domains : string [ ] = Array . isArray ( formData . domains )
? formData . domains . filter ( Boolean )
: [ ] ;
const priority = formData . priority ? parseInt ( formData . priority , 10 ) : undefined ;
2026-04-02 22:37:49 +00:00
const updatedRoute : any = {
name : formData.name ,
match : {
ports ,
2026-04-02 22:55:57 +00:00
. . . ( domains . length > 0 ? { domains } : { } ) ,
2026-04-02 22:37:49 +00:00
} ,
action : {
type : 'forward' ,
targets : [
{
host : formData.targetHost || 'localhost' ,
port : parseInt ( formData . targetPort , 10 ) || 443 ,
} ,
] ,
} ,
2026-04-02 22:55:57 +00:00
. . . ( priority != null && ! isNaN ( priority ) ? { priority } : { } ) ,
2026-04-02 22:37:49 +00:00
} ;
2026-04-04 21:23:16 +00:00
// Build TLS config from form
const tlsModeValue = formData . tlsMode as any ;
const tlsModeKey = typeof tlsModeValue === 'string' ? tlsModeValue : tlsModeValue?.key ;
if ( tlsModeKey && tlsModeKey !== 'none' ) {
const tls : any = { mode : tlsModeKey } ;
if ( tlsModeKey !== 'passthrough' ) {
const tlsCertValue = formData . tlsCertificate as any ;
const tlsCertKey = typeof tlsCertValue === 'string' ? tlsCertValue : tlsCertValue?.key ;
if ( tlsCertKey === 'custom' && formData . tlsCertKey && formData . tlsCertCert ) {
tls . certificate = { key : formData.tlsCertKey , cert : formData.tlsCertCert } ;
} else {
tls . certificate = 'auto' ;
}
}
updatedRoute . action . tls = tls ;
} else {
updatedRoute . action . tls = null ; // explicit removal
}
2026-04-02 22:37:49 +00:00
const metadata : any = { } ;
2026-04-05 00:37:37 +00:00
const profileRefValue = formData . sourceProfileRef as any ;
2026-04-04 11:00:03 +00:00
const profileKey = typeof profileRefValue === 'string' ? profileRefValue : profileRefValue?.key ;
if ( profileKey ) {
2026-04-05 00:37:37 +00:00
metadata . sourceProfileRef = profileKey ;
2026-04-02 22:37:49 +00:00
}
2026-04-04 11:00:03 +00:00
const targetRefValue = formData . networkTargetRef as any ;
const targetKey = typeof targetRefValue === 'string' ? targetRefValue : targetRefValue?.key ;
if ( targetKey ) {
metadata . networkTargetRef = targetKey ;
2026-04-02 22:37:49 +00:00
}
await appstate . routeManagementStatePart . dispatchAction (
appstate . updateRouteAction ,
{
2026-04-13 17:38:23 +00:00
id : merged.id ,
2026-04-02 22:37:49 +00:00
route : updatedRoute ,
metadata : Object.keys ( metadata ) . length > 0 ? metadata : undefined ,
} ,
) ;
await modalArg . destroy ( ) ;
} ,
} ,
] ,
} ) ;
2026-04-04 21:23:16 +00:00
// Setup conditional TLS field visibility after modal renders
const editForm = editModal ? . shadowRoot ? . querySelector ( '.content' ) ? . querySelector ( 'dees-form' ) as any ;
if ( editForm ) {
await editForm . updateComplete ;
setupTlsVisibility ( editForm ) ;
}
2026-04-02 22:37:49 +00:00
}
2026-02-23 12:40:26 +00:00
private async showCreateRouteDialog() {
const { DeesModal } = await import ( '@design.estate/dees-catalog' ) ;
2026-04-02 17:27:05 +00:00
const profiles = this . profilesTargetsState . profiles ;
const targets = this . profilesTargetsState . targets ;
// Build dropdown options for profiles and targets
const profileOptions = [
{ key : '' , option : '(none — inline security)' } ,
. . . profiles . map ( ( p ) = > ( {
key : p.id ,
option : ` ${ p . name } ${ p . description ? ' — ' + p . description : '' } ` ,
} ) ) ,
] ;
const targetOptions = [
{ key : '' , option : '(none — inline target)' } ,
. . . targets . map ( ( t ) = > ( {
key : t.id ,
option : ` ${ t . name } ( ${ Array . isArray ( t . host ) ? t . host . join ( ',' ) : t . host } : ${ t . port } ) ` ,
} ) ) ,
] ;
2026-02-23 12:40:26 +00:00
2026-04-04 21:23:16 +00:00
const createModal = await DeesModal . createAndShow ( {
2026-04-13 17:38:23 +00:00
heading : 'Add Route' ,
2026-02-23 12:40:26 +00:00
content : html `
< dees - form >
< dees - input - text .key = $ { 'name' } .label = $ { 'Route Name' } .required = $ { true } > < / d e e s - i n p u t - t e x t >
2026-04-12 19:42:07 +00:00
< dees - input - text .key = $ { 'ports' } .label = $ { 'Ports' } .description = $ { 'Comma-separated, e.g. 80, 443' } .required = $ { true } > < / d e e s - i n p u t - t e x t >
2026-04-02 22:55:57 +00:00
< dees - input - list .key = $ { 'domains' } .label = $ { 'Domains' } .placeholder = $ { 'Add domain...' } > < / d e e s - i n p u t - l i s t >
2026-04-12 19:42:07 +00:00
< dees - input - text .key = $ { 'priority' } .label = $ { 'Priority' } .description = $ { 'Higher values are matched first' } > < / d e e s - i n p u t - t e x t >
2026-04-05 00:37:37 +00:00
< dees - input - dropdown .key = $ { 'sourceProfileRef' } .label = $ { 'Source Profile' } .options = $ { profileOptions } > < / d e e s - i n p u t - d r o p d o w n >
2026-04-04 11:00:03 +00:00
< dees - input - dropdown .key = $ { 'networkTargetRef' } .label = $ { 'Network Target' } .options = $ { targetOptions } > < / d e e s - i n p u t - d r o p d o w n >
2026-04-12 19:42:07 +00:00
< dees - input - text .key = $ { 'targetHost' } .label = $ { 'Target Host' } .description = $ { 'Used when no network target is selected' } .value = $ { 'localhost' } > < / d e e s - i n p u t - t e x t >
< dees - input - text .key = $ { 'targetPort' } .label = $ { 'Target Port' } .description = $ { 'Used when no network target is selected' } > < / d e e s - i n p u t - t e x t >
2026-04-04 21:23:16 +00:00
< dees - input - dropdown .key = $ { 'tlsMode' } .label = $ { 'TLS Mode' } .options = $ { tlsModeOptions } .selectedOption = $ { tlsModeOptions [ 0 ] } > < / d e e s - i n p u t - d r o p d o w n >
< div class = "tlsCertificateGroup" style = "display: none; flex-direction: column; gap: 16px;" >
< dees - input - dropdown .key = $ { 'tlsCertificate' } .label = $ { 'Certificate' } .options = $ { tlsCertOptions } .selectedOption = $ { tlsCertOptions [ 0 ] } > < / d e e s - i n p u t - d r o p d o w n >
< div class = "tlsCustomCertGroup" style = "display: none; flex-direction: column; gap: 16px;" >
2026-04-12 19:42:07 +00:00
< dees - input - text .key = $ { 'tlsCertKey' } .label = $ { 'Private Key' } .description = $ { 'PEM-encoded private key' } > < / d e e s - i n p u t - t e x t >
< dees - input - text .key = $ { 'tlsCertCert' } .label = $ { 'Certificate' } .description = $ { 'PEM-encoded certificate' } > < / d e e s - i n p u t - t e x t >
2026-04-04 21:23:16 +00:00
< / div >
< / div >
2026-02-23 12:40:26 +00:00
< / d e e s - f o r m >
` ,
menuOptions : [
{
name : 'Cancel' ,
iconName : 'lucide:x' ,
action : async ( modalArg : any ) = > await modalArg . destroy ( ) ,
} ,
{
name : 'Create' ,
iconName : 'lucide:plus' ,
action : async ( modalArg : any ) = > {
const form = modalArg . shadowRoot ? . querySelector ( '.content' ) ? . querySelector ( 'dees-form' ) ;
if ( ! form ) return ;
const formData = await form . collectFormData ( ) ;
if ( ! formData . name || ! formData . ports ) return ;
const ports = formData . ports . split ( ',' ) . map ( ( p : string ) = > parseInt ( p . trim ( ) , 10 ) ) . filter ( ( p : number ) = > ! isNaN ( p ) ) ;
2026-04-02 22:55:57 +00:00
const domains : string [ ] = Array . isArray ( formData . domains )
? formData . domains . filter ( Boolean )
: [ ] ;
const priority = formData . priority ? parseInt ( formData . priority , 10 ) : undefined ;
2026-02-23 12:40:26 +00:00
const route : any = {
name : formData.name ,
match : {
ports ,
2026-04-02 22:55:57 +00:00
. . . ( domains . length > 0 ? { domains } : { } ) ,
2026-02-23 12:40:26 +00:00
} ,
action : {
type : 'forward' ,
targets : [
{
host : formData.targetHost || 'localhost' ,
2026-04-02 17:27:05 +00:00
port : parseInt ( formData . targetPort , 10 ) || 443 ,
2026-02-23 12:40:26 +00:00
} ,
] ,
} ,
2026-04-02 22:55:57 +00:00
. . . ( priority != null && ! isNaN ( priority ) ? { priority } : { } ) ,
2026-02-23 12:40:26 +00:00
} ;
2026-04-04 21:23:16 +00:00
// Build TLS config from form
const tlsModeValue = formData . tlsMode as any ;
const tlsModeKey = typeof tlsModeValue === 'string' ? tlsModeValue : tlsModeValue?.key ;
if ( tlsModeKey && tlsModeKey !== 'none' ) {
const tls : any = { mode : tlsModeKey } ;
if ( tlsModeKey !== 'passthrough' ) {
const tlsCertValue = formData . tlsCertificate as any ;
const tlsCertKey = typeof tlsCertValue === 'string' ? tlsCertValue : tlsCertValue?.key ;
if ( tlsCertKey === 'custom' && formData . tlsCertKey && formData . tlsCertCert ) {
tls . certificate = { key : formData.tlsCertKey , cert : formData.tlsCertCert } ;
} else {
tls . certificate = 'auto' ;
}
}
route . action . tls = tls ;
}
2026-04-02 17:27:05 +00:00
// Build metadata if profile/target selected
const metadata : any = { } ;
2026-04-05 00:37:37 +00:00
const profileRefValue = formData . sourceProfileRef as any ;
2026-04-04 11:00:03 +00:00
const profileKey = typeof profileRefValue === 'string' ? profileRefValue : profileRefValue?.key ;
if ( profileKey ) {
2026-04-05 00:37:37 +00:00
metadata . sourceProfileRef = profileKey ;
2026-04-02 17:27:05 +00:00
}
2026-04-04 11:00:03 +00:00
const targetRefValue = formData . networkTargetRef as any ;
const targetKey = typeof targetRefValue === 'string' ? targetRefValue : targetRefValue?.key ;
if ( targetKey ) {
metadata . networkTargetRef = targetKey ;
2026-04-02 17:27:05 +00:00
}
2026-02-23 12:40:26 +00:00
await appstate . routeManagementStatePart . dispatchAction (
appstate . createRouteAction ,
2026-04-02 17:27:05 +00:00
{
route ,
metadata : Object.keys ( metadata ) . length > 0 ? metadata : undefined ,
} ,
2026-02-23 12:40:26 +00:00
) ;
await modalArg . destroy ( ) ;
} ,
} ,
] ,
} ) ;
2026-04-04 21:23:16 +00:00
// Setup conditional TLS field visibility after modal renders
const createForm = createModal ? . shadowRoot ? . querySelector ( '.content' ) ? . querySelector ( 'dees-form' ) as any ;
if ( createForm ) {
await createForm . updateComplete ;
setupTlsVisibility ( createForm ) ;
}
2026-02-23 12:40:26 +00:00
}
private refreshData() {
appstate . routeManagementStatePart . dispatchAction ( appstate . fetchMergedRoutesAction , null ) ;
}
async firstUpdated() {
await appstate . routeManagementStatePart . dispatchAction ( appstate . fetchMergedRoutesAction , null ) ;
}
}