feat(vpn): add VPN client editing and connected client visibility in ops server
This commit is contained in:
@@ -141,10 +141,16 @@ export class OpsViewVpn extends DeesElement {
|
||||
`,
|
||||
];
|
||||
|
||||
/** Look up connected client info by clientId */
|
||||
private getConnectedInfo(clientId: string): interfaces.data.IVpnConnectedClient | undefined {
|
||||
return this.vpnState.connectedClients?.find(c => c.clientId === clientId);
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
const status = this.vpnState.status;
|
||||
const clients = this.vpnState.clients;
|
||||
const connectedCount = status?.connectedClients ?? 0;
|
||||
const connectedClients = this.vpnState.connectedClients || [];
|
||||
const connectedCount = connectedClients.length;
|
||||
const totalClients = clients.length;
|
||||
const enabledClients = clients.filter(c => c.enabled).length;
|
||||
|
||||
@@ -270,18 +276,28 @@ export class OpsViewVpn extends DeesElement {
|
||||
.heading1=${'VPN Clients'}
|
||||
.heading2=${'Manage WireGuard and SmartVPN client registrations'}
|
||||
.data=${clients}
|
||||
.displayFunction=${(client: interfaces.data.IVpnClient) => ({
|
||||
'Client ID': client.clientId,
|
||||
'Status': client.enabled
|
||||
? html`<span class="statusBadge enabled">enabled</span>`
|
||||
: html`<span class="statusBadge disabled">disabled</span>`,
|
||||
'VPN IP': client.assignedIp || '-',
|
||||
'Tags': client.serverDefinedClientTags?.length
|
||||
? html`${client.serverDefinedClientTags.map(t => html`<span class="tagBadge">${t}</span>`)}`
|
||||
: '-',
|
||||
'Description': client.description || '-',
|
||||
'Created': new Date(client.createdAt).toLocaleDateString(),
|
||||
})}
|
||||
.displayFunction=${(client: interfaces.data.IVpnClient) => {
|
||||
const conn = this.getConnectedInfo(client.clientId);
|
||||
let statusHtml;
|
||||
if (!client.enabled) {
|
||||
statusHtml = html`<span class="statusBadge disabled">disabled</span>`;
|
||||
} else if (conn) {
|
||||
const since = new Date(conn.connectedSince).toLocaleString();
|
||||
statusHtml = html`<span class="statusBadge enabled" title="Since ${since}">connected</span>`;
|
||||
} else {
|
||||
statusHtml = html`<span class="statusBadge enabled" style="background: ${cssManager.bdTheme('#eff6ff', '#172554')}; color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};">offline</span>`;
|
||||
}
|
||||
return {
|
||||
'Client ID': client.clientId,
|
||||
'Status': statusHtml,
|
||||
'VPN IP': client.assignedIp || '-',
|
||||
'Tags': client.serverDefinedClientTags?.length
|
||||
? html`${client.serverDefinedClientTags.map(t => html`<span class="tagBadge">${t}</span>`)}`
|
||||
: '-',
|
||||
'Description': client.description || '-',
|
||||
'Created': new Date(client.createdAt).toLocaleDateString(),
|
||||
};
|
||||
}}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'Create Client',
|
||||
@@ -328,14 +344,91 @@ export class OpsViewVpn extends DeesElement {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Toggle',
|
||||
name: 'Detail',
|
||||
iconName: 'lucide:info',
|
||||
type: ['doubleClick'],
|
||||
actionFunc: async (actionData: any) => {
|
||||
const client = actionData.item as interfaces.data.IVpnClient;
|
||||
const conn = this.getConnectedInfo(client.clientId);
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
|
||||
// Fetch telemetry on-demand
|
||||
let telemetryHtml = html`<p style="color: #9ca3af;">Loading telemetry...</p>`;
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetVpnClientTelemetry
|
||||
>('/typedrequest', 'getVpnClientTelemetry');
|
||||
const response = await request.fire({
|
||||
identity: appstate.loginStatePart.getState()!.identity!,
|
||||
clientId: client.clientId,
|
||||
});
|
||||
const t = response.telemetry;
|
||||
if (t) {
|
||||
const formatBytes = (b: number) => b > 1048576 ? `${(b / 1048576).toFixed(1)} MB` : b > 1024 ? `${(b / 1024).toFixed(1)} KB` : `${b} B`;
|
||||
telemetryHtml = html`
|
||||
<div class="serverInfo" style="margin-top: 12px;">
|
||||
<div class="infoItem"><span class="infoLabel">Bytes Sent</span><span class="infoValue">${formatBytes(t.bytesSent)}</span></div>
|
||||
<div class="infoItem"><span class="infoLabel">Bytes Received</span><span class="infoValue">${formatBytes(t.bytesReceived)}</span></div>
|
||||
<div class="infoItem"><span class="infoLabel">Keepalives</span><span class="infoValue">${t.keepalivesReceived}</span></div>
|
||||
<div class="infoItem"><span class="infoLabel">Last Keepalive</span><span class="infoValue">${t.lastKeepaliveAt ? new Date(t.lastKeepaliveAt).toLocaleString() : '-'}</span></div>
|
||||
<div class="infoItem"><span class="infoLabel">Packets Dropped</span><span class="infoValue">${t.packetsDropped}</span></div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
telemetryHtml = html`<p style="color: #9ca3af;">No telemetry available (client not connected)</p>`;
|
||||
}
|
||||
} catch {
|
||||
telemetryHtml = html`<p style="color: #9ca3af;">Telemetry unavailable</p>`;
|
||||
}
|
||||
|
||||
DeesModal.createAndShow({
|
||||
heading: `Client: ${client.clientId}`,
|
||||
content: html`
|
||||
<div class="serverInfo">
|
||||
<div class="infoItem"><span class="infoLabel">Client ID</span><span class="infoValue">${client.clientId}</span></div>
|
||||
<div class="infoItem"><span class="infoLabel">VPN IP</span><span class="infoValue">${client.assignedIp || '-'}</span></div>
|
||||
<div class="infoItem"><span class="infoLabel">Status</span><span class="infoValue">${!client.enabled ? 'Disabled' : conn ? 'Connected' : 'Offline'}</span></div>
|
||||
${conn ? html`
|
||||
<div class="infoItem"><span class="infoLabel">Connected Since</span><span class="infoValue">${new Date(conn.connectedSince).toLocaleString()}</span></div>
|
||||
<div class="infoItem"><span class="infoLabel">Transport</span><span class="infoValue">${conn.transport}</span></div>
|
||||
` : ''}
|
||||
<div class="infoItem"><span class="infoLabel">Description</span><span class="infoValue">${client.description || '-'}</span></div>
|
||||
<div class="infoItem"><span class="infoLabel">Tags</span><span class="infoValue">${client.serverDefinedClientTags?.join(', ') || '-'}</span></div>
|
||||
<div class="infoItem"><span class="infoLabel">Created</span><span class="infoValue">${new Date(client.createdAt).toLocaleString()}</span></div>
|
||||
<div class="infoItem"><span class="infoLabel">Updated</span><span class="infoValue">${new Date(client.updatedAt).toLocaleString()}</span></div>
|
||||
</div>
|
||||
<h3 style="margin: 16px 0 4px; font-size: 14px;">Telemetry</h3>
|
||||
${telemetryHtml}
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'Close', iconName: 'lucide:x', action: async (m: any) => await m.destroy() },
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Enable',
|
||||
iconName: 'lucide:power',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionRelevancyCheckFunc: (actionData: any) => !actionData.item.enabled,
|
||||
actionFunc: async (actionData: any) => {
|
||||
const client = actionData.item as interfaces.data.IVpnClient;
|
||||
await appstate.vpnStatePart.dispatchAction(appstate.toggleVpnClientAction, {
|
||||
clientId: client.clientId,
|
||||
enabled: !client.enabled,
|
||||
enabled: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Disable',
|
||||
iconName: 'lucide:power',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionRelevancyCheckFunc: (actionData: any) => actionData.item.enabled,
|
||||
actionFunc: async (actionData: any) => {
|
||||
const client = actionData.item as interfaces.data.IVpnClient;
|
||||
await appstate.vpnStatePart.dispatchAction(appstate.toggleVpnClientAction, {
|
||||
clientId: client.clientId,
|
||||
enabled: false,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -449,6 +542,47 @@ export class OpsViewVpn extends DeesElement {
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Edit',
|
||||
iconName: 'lucide:pencil',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionData: any) => {
|
||||
const client = actionData.item as interfaces.data.IVpnClient;
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
const currentDescription = client.description ?? '';
|
||||
const currentTags = client.serverDefinedClientTags?.join(', ') ?? '';
|
||||
DeesModal.createAndShow({
|
||||
heading: `Edit: ${client.clientId}`,
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'description'} .label=${'Description'} .value=${currentDescription}></dees-input-text>
|
||||
<dees-input-text .key=${'tags'} .label=${'Server-Defined Tags (comma-separated)'} .value=${currentTags}></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
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 data = await form.collectFormData();
|
||||
const serverDefinedClientTags = data.tags
|
||||
? data.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
|
||||
: [];
|
||||
await appstate.vpnStatePart.dispatchAction(appstate.updateVpnClientAction, {
|
||||
clientId: client.clientId,
|
||||
description: data.description || undefined,
|
||||
serverDefinedClientTags,
|
||||
});
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Rotate Keys',
|
||||
iconName: 'lucide:rotate-cw',
|
||||
|
||||
Reference in New Issue
Block a user