251 lines
10 KiB
TypeScript
251 lines
10 KiB
TypeScript
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`
|
|
<dees-heading level="3">Gateway Clients</dees-heading>
|
|
<dees-table
|
|
.heading1=${'Gateway Clients'}
|
|
.heading2=${'Create durable clients and token credentials for Onebox, Cloudly, or custom integrations'}
|
|
.data=${this.routeState.gatewayClients}
|
|
.dataName=${'gateway client'}
|
|
.searchable=${true}
|
|
.showColumnFilters=${true}
|
|
.displayFunction=${(client: interfaces.data.IGatewayClient) => ({
|
|
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);
|
|
},
|
|
},
|
|
]}
|
|
></dees-table>
|
|
`;
|
|
}
|
|
|
|
private renderPills(values: string[]): TemplateResult {
|
|
if (!values.length) return html`<span>None</span>`;
|
|
return html`${values.map((value) => html`<span class="pill">${value}</span>`)}`;
|
|
}
|
|
|
|
private renderTargets(targets: interfaces.data.IGatewayClient['allowedRouteTargets']): TemplateResult {
|
|
if (!targets.length) return html`<span>None</span>`;
|
|
return html`${targets.map((target) => html`<span class="pill">${target.host}:${target.ports.join(',')}</span>`)}`;
|
|
}
|
|
|
|
private async showCreateClientDialog(): Promise<void> {
|
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
await DeesModal.createAndShow({
|
|
heading: 'Create Gateway Client',
|
|
content: html`
|
|
<dees-form>
|
|
<dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
|
|
<dees-input-text .key=${'type'} .label=${'Type'} .value=${'onebox'} .description=${'onebox, cloudly, or custom'}></dees-input-text>
|
|
<dees-input-text .key=${'id'} .label=${'Client ID'} .description=${'Optional stable ID; generated when empty'}></dees-input-text>
|
|
<dees-input-text .key=${'hostnamePatterns'} .label=${'Hostname Patterns'} .description=${'Comma separated, e.g. *.apps.example.com'}></dees-input-text>
|
|
<dees-input-text .key=${'allowedRouteTarget'} .label=${'Allowed Route Target'} .description=${'Optional host:ports, e.g. onebox-smartproxy:80'}></dees-input-text>
|
|
<dees-input-text .key=${'description'} .label=${'Description'}></dees-input-text>
|
|
</dees-form>
|
|
`,
|
|
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<void> {
|
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
await DeesModal.createAndShow({
|
|
heading: `Create Token for ${client.name}`,
|
|
content: html`
|
|
<div style="color: #888; margin-bottom: 12px; font-size: 13px;">
|
|
The token will be shown once. Configure Onebox with the dcrouter URL and this token.
|
|
</div>
|
|
<dees-form>
|
|
<dees-input-text .key=${'name'} .label=${'Token Name'} .value=${`${client.name} Token`}></dees-input-text>
|
|
<dees-input-text .key=${'expiresInDays'} .label=${'Expires in'} .description=${'Number of days; leave blank for no expiration'}></dees-input-text>
|
|
</dees-form>
|
|
`,
|
|
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`
|
|
<p>Copy this token now. It will not be shown again.</p>
|
|
<div style="background: #111; padding: 12px; border-radius: 6px; margin-top: 8px;">
|
|
<code style="color: #0f8; word-break: break-all; font-size: 13px;">${response.tokenValue}</code>
|
|
</div>
|
|
`,
|
|
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 }] : [];
|
|
}
|
|
}
|