import * as appstate from '../../appstate.js'; import * as interfaces from '../../../dist_ts_interfaces/index.js'; import { viewHostCss } from '../shared/css.js'; import { DeesElement, css, cssManager, customElement, html, state, type TemplateResult, } from '@design.estate/dees-element'; @customElement('ops-view-gatewayclients') export class OpsViewGatewayClients extends DeesElement { @state() accessor routeState: appstate.IRouteManagementState = { mergedRoutes: [], warnings: [], apiTokens: [], gatewayClients: [], isLoading: false, error: null, lastUpdated: 0, }; constructor() { super(); const sub = appstate.routeManagementStatePart .select((s) => s) .subscribe((routeState) => { this.routeState = routeState; }); this.rxSubscriptions.push(sub); const loginSub = appstate.loginStatePart .select((s) => s.isLoggedIn) .subscribe((isLoggedIn) => { if (isLoggedIn) { appstate.routeManagementStatePart.dispatchAction(appstate.fetchGatewayClientsAction, null); } }); this.rxSubscriptions.push(loginSub); } public static styles = [ cssManager.defaultStyles, viewHostCss, css` .pill { display: inline-flex; padding: 2px 6px; border-radius: 3px; font-size: 11px; background: ${cssManager.bdTheme('rgba(37, 99, 235, 0.1)', 'rgba(96, 165, 250, 0.14)')}; color: ${cssManager.bdTheme('#1d4ed8', '#93c5fd')}; margin-right: 4px; margin-bottom: 2px; } `, ]; public render(): TemplateResult { return html` Gateway Clients ({ name: client.name, id: client.id, type: client.type, hostnames: this.renderPills(client.hostnamePatterns), targets: this.renderTargets(client.allowedRouteTargets), tokens: client.tokenCount || 0, status: client.enabled ? 'Active' : 'Disabled', })} .dataActions=${[ { name: 'Create Client', iconName: 'lucide:plus', type: ['header'], actionFunc: async () => await this.showCreateClientDialog(), }, { name: 'Create Token', iconName: 'lucide:keyRound', type: ['inRow', 'contextmenu'] as any, actionFunc: async (actionData: any) => await this.showCreateTokenDialog(actionData.item), }, { name: 'Enable', iconName: 'lucide:play', type: ['inRow', 'contextmenu'] as any, actionRelevancyCheckFunc: (actionData: any) => !actionData.item.enabled, actionFunc: async (actionData: any) => { await appstate.routeManagementStatePart.dispatchAction(appstate.updateGatewayClientAction, { id: actionData.item.id, enabled: true, }); }, }, { name: 'Disable', iconName: 'lucide:pause', type: ['inRow', 'contextmenu'] as any, actionRelevancyCheckFunc: (actionData: any) => actionData.item.enabled, actionFunc: async (actionData: any) => { await appstate.routeManagementStatePart.dispatchAction(appstate.updateGatewayClientAction, { id: actionData.item.id, enabled: false, }); }, }, { name: 'Delete', iconName: 'lucide:trash2', type: ['inRow', 'contextmenu'] as any, actionFunc: async (actionData: any) => { await appstate.routeManagementStatePart.dispatchAction(appstate.deleteGatewayClientAction, actionData.item.id); }, }, ]} > `; } private renderPills(values: string[]): TemplateResult { if (!values.length) return html`None`; return html`${values.map((value) => html`${value}`)}`; } private renderTargets(targets: interfaces.data.IGatewayClient['allowedRouteTargets']): TemplateResult { if (!targets.length) return html`None`; return html`${targets.map((target) => html`${target.host}:${target.ports.join(',')}`)}`; } private async showCreateClientDialog(): Promise { const { DeesModal } = await import('@design.estate/dees-catalog'); await DeesModal.createAndShow({ heading: 'Create Gateway Client', content: html` `, 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 as any).collectFormData(); const name = String(formData.name || '').trim(); if (!name) return; await modalArg.destroy(); await appstate.createGatewayClient({ id: String(formData.id || '').trim() || undefined, type: this.normalizeClientType(String(formData.type || 'onebox')), name, description: String(formData.description || '').trim() || undefined, hostnamePatterns: this.parseList(String(formData.hostnamePatterns || '')), allowedRouteTargets: this.parseAllowedRouteTargets(String(formData.allowedRouteTarget || '')), }); await appstate.routeManagementStatePart.dispatchAction(appstate.fetchGatewayClientsAction, null); }, }, ], }); } private async showCreateTokenDialog(client: interfaces.data.IGatewayClient): Promise { const { DeesModal } = await import('@design.estate/dees-catalog'); await DeesModal.createAndShow({ heading: `Create Token for ${client.name}`, content: html` The token will be shown once. Configure Onebox with the dcrouter URL and this token. `, menuOptions: [ { name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => await modalArg.destroy() }, { name: 'Create Token', iconName: 'lucide:key', action: async (modalArg: any) => { const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form'); if (!form) return; const formData = await (form as any).collectFormData(); const expiresInDays = formData.expiresInDays ? parseInt(formData.expiresInDays, 10) : null; await modalArg.destroy(); const response = await appstate.createGatewayClientToken( client.id, String(formData.name || '').trim() || undefined, expiresInDays, ); await appstate.routeManagementStatePart.dispatchAction(appstate.fetchGatewayClientsAction, null); if (response.success && response.tokenValue) { await DeesModal.createAndShow({ heading: 'Gateway Client Token Created', content: html` Copy this token now. It will not be shown again. ${response.tokenValue} `, menuOptions: [ { name: 'Done', iconName: 'lucide:check', action: async (m: any) => await m.destroy() }, ], }); } }, }, ], }); } private normalizeClientType(value: string): interfaces.data.IGatewayClient['type'] { const normalized = value.trim().toLowerCase(); if (normalized === 'cloudly' || normalized === 'custom') return normalized; return 'onebox'; } private parseList(value: string): string[] { return value.split(',').map((entry) => entry.trim()).filter(Boolean); } private parseAllowedRouteTargets(value: string): interfaces.data.IGatewayClient['allowedRouteTargets'] { const target = value.trim(); if (!target.includes(':')) return []; const [host, portsValue] = target.split(':'); const ports = portsValue.split(',').map((port) => Number(port.trim())).filter((port) => Number.isInteger(port)); return host.trim() && ports.length ? [{ host: host.trim(), ports }] : []; } }
Copy this token now. It will not be shown again.
${response.tokenValue}