feat(config): add reusable security profiles and network targets with route reference resolution
This commit is contained in:
@@ -10,4 +10,6 @@ export * from './ops-view-security.js';
|
||||
export * from './ops-view-certificates.js';
|
||||
export * from './ops-view-remoteingress.js';
|
||||
export * from './ops-view-vpn.js';
|
||||
export * from './ops-view-securityprofiles.js';
|
||||
export * from './ops-view-networktargets.js';
|
||||
export * from './shared/index.js';
|
||||
@@ -25,6 +25,8 @@ import { OpsViewSecurity } from './ops-view-security.js';
|
||||
import { OpsViewCertificates } from './ops-view-certificates.js';
|
||||
import { OpsViewRemoteIngress } from './ops-view-remoteingress.js';
|
||||
import { OpsViewVpn } from './ops-view-vpn.js';
|
||||
import { OpsViewSecurityProfiles } from './ops-view-securityprofiles.js';
|
||||
import { OpsViewNetworkTargets } from './ops-view-networktargets.js';
|
||||
|
||||
@customElement('ops-dashboard')
|
||||
export class OpsDashboard extends DeesElement {
|
||||
@@ -73,6 +75,16 @@ export class OpsDashboard extends DeesElement {
|
||||
iconName: 'lucide:route',
|
||||
element: OpsViewRoutes,
|
||||
},
|
||||
{
|
||||
name: 'SecurityProfiles',
|
||||
iconName: 'lucide:shieldCheck',
|
||||
element: OpsViewSecurityProfiles,
|
||||
},
|
||||
{
|
||||
name: 'NetworkTargets',
|
||||
iconName: 'lucide:server',
|
||||
element: OpsViewNetworkTargets,
|
||||
},
|
||||
{
|
||||
name: 'ApiTokens',
|
||||
iconName: 'lucide:key',
|
||||
|
||||
214
ts_web/elements/ops-view-networktargets.ts
Normal file
214
ts_web/elements/ops-view-networktargets.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import {
|
||||
DeesElement,
|
||||
html,
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
css,
|
||||
state,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
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-networktargets': OpsViewNetworkTargets;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('ops-view-networktargets')
|
||||
export class OpsViewNetworkTargets extends DeesElement {
|
||||
@state()
|
||||
accessor profilesState: appstate.IProfilesTargetsState = appstate.profilesTargetsStatePart.getState()!;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const sub = appstate.profilesTargetsStatePart.select().subscribe((newState) => {
|
||||
this.profilesState = newState;
|
||||
});
|
||||
this.rxSubscriptions.push(sub);
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
await super.connectedCallback();
|
||||
await appstate.profilesTargetsStatePart.dispatchAction(appstate.fetchProfilesAndTargetsAction, null);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
viewHostCss,
|
||||
css`
|
||||
.targetsContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
const targets = this.profilesState.targets;
|
||||
|
||||
const statsTiles: IStatsTile[] = [
|
||||
{
|
||||
id: 'totalTargets',
|
||||
title: 'Total Targets',
|
||||
type: 'number',
|
||||
value: targets.length,
|
||||
icon: 'lucide:server',
|
||||
description: 'Reusable network targets',
|
||||
color: '#8b5cf6',
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<div class="targetsContainer">
|
||||
<dees-statsgrid .tiles=${statsTiles}></dees-statsgrid>
|
||||
<dees-table
|
||||
.heading=${'Network Targets'}
|
||||
.data=${targets}
|
||||
.displayFunction=${(target: interfaces.data.INetworkTarget) => ({
|
||||
Name: target.name,
|
||||
Host: Array.isArray(target.host) ? target.host.join(', ') : target.host,
|
||||
Port: target.port,
|
||||
Description: target.description || '-',
|
||||
})}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'Create Target',
|
||||
iconName: 'lucide:plus',
|
||||
type: ['header' as const],
|
||||
action: async (_: any, table: any) => {
|
||||
await this.showCreateTargetDialog(table);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Refresh',
|
||||
iconName: 'lucide:rotateCw',
|
||||
type: ['header' as const],
|
||||
action: async () => {
|
||||
await appstate.profilesTargetsStatePart.dispatchAction(appstate.fetchProfilesAndTargetsAction, null);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Edit',
|
||||
iconName: 'lucide:pencil',
|
||||
type: ['contextmenu' as const],
|
||||
action: async (target: interfaces.data.INetworkTarget, table: any) => {
|
||||
await this.showEditTargetDialog(target, table);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
iconName: 'lucide:trash2',
|
||||
type: ['contextmenu' as const],
|
||||
action: async (target: interfaces.data.INetworkTarget) => {
|
||||
await this.deleteTarget(target);
|
||||
},
|
||||
},
|
||||
]}
|
||||
></dees-table>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async showCreateTargetDialog(table: any) {
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
DeesModal.createAndShow({
|
||||
heading: 'Create Network Target',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'host'} .label=${'Host'} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'port'} .label=${'Port'} .required=${true} .value=${'443'}></dees-input-text>
|
||||
<dees-input-text .key=${'description'} .label=${'Description'}></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Create',
|
||||
action: async (modalArg: any) => {
|
||||
const form = modalArg.shadowRoot!.querySelector('dees-form');
|
||||
const data = await form.collectFormData();
|
||||
|
||||
await appstate.profilesTargetsStatePart.dispatchAction(appstate.createTargetAction, {
|
||||
name: String(data.name),
|
||||
description: data.description ? String(data.description) : undefined,
|
||||
host: String(data.host),
|
||||
port: parseInt(String(data.port)) || 443,
|
||||
});
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private async showEditTargetDialog(target: interfaces.data.INetworkTarget, table: any) {
|
||||
const hostStr = Array.isArray(target.host) ? target.host.join(', ') : target.host;
|
||||
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
DeesModal.createAndShow({
|
||||
heading: `Edit Target: ${target.name}`,
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'name'} .label=${'Name'} .value=${target.name}></dees-input-text>
|
||||
<dees-input-text .key=${'host'} .label=${'Host'} .value=${hostStr}></dees-input-text>
|
||||
<dees-input-text .key=${'port'} .label=${'Port'} .value=${String(target.port)}></dees-input-text>
|
||||
<dees-input-text .key=${'description'} .label=${'Description'} .value=${target.description || ''}></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Save',
|
||||
action: async (modalArg: any) => {
|
||||
const form = modalArg.shadowRoot!.querySelector('dees-form');
|
||||
const data = await form.collectFormData();
|
||||
|
||||
await appstate.profilesTargetsStatePart.dispatchAction(appstate.updateTargetAction, {
|
||||
id: target.id,
|
||||
name: String(data.name),
|
||||
description: data.description ? String(data.description) : undefined,
|
||||
host: String(data.host),
|
||||
port: parseInt(String(data.port)) || 443,
|
||||
});
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private async deleteTarget(target: interfaces.data.INetworkTarget) {
|
||||
await appstate.profilesTargetsStatePart.dispatchAction(appstate.deleteTargetAction, {
|
||||
id: target.id,
|
||||
force: false,
|
||||
});
|
||||
|
||||
const currentState = appstate.profilesTargetsStatePart.getState()!;
|
||||
if (currentState.error?.includes('in use')) {
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
DeesModal.createAndShow({
|
||||
heading: 'Target In Use',
|
||||
content: html`<p>${currentState.error} Force delete?</p>`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Force Delete',
|
||||
action: async (modalArg: any) => {
|
||||
await appstate.profilesTargetsStatePart.dispatchAction(appstate.deleteTargetAction, {
|
||||
id: target.id,
|
||||
force: true,
|
||||
});
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
242
ts_web/elements/ops-view-securityprofiles.ts
Normal file
242
ts_web/elements/ops-view-securityprofiles.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
import {
|
||||
DeesElement,
|
||||
html,
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
css,
|
||||
state,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
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-securityprofiles': OpsViewSecurityProfiles;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('ops-view-securityprofiles')
|
||||
export class OpsViewSecurityProfiles extends DeesElement {
|
||||
@state()
|
||||
accessor profilesState: appstate.IProfilesTargetsState = appstate.profilesTargetsStatePart.getState()!;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const sub = appstate.profilesTargetsStatePart.select().subscribe((newState) => {
|
||||
this.profilesState = newState;
|
||||
});
|
||||
this.rxSubscriptions.push(sub);
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
await super.connectedCallback();
|
||||
await appstate.profilesTargetsStatePart.dispatchAction(appstate.fetchProfilesAndTargetsAction, null);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
viewHostCss,
|
||||
css`
|
||||
.profilesContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
const profiles = this.profilesState.profiles;
|
||||
|
||||
const statsTiles: IStatsTile[] = [
|
||||
{
|
||||
id: 'totalProfiles',
|
||||
title: 'Total Profiles',
|
||||
type: 'number',
|
||||
value: profiles.length,
|
||||
icon: 'lucide:shieldCheck',
|
||||
description: 'Reusable security profiles',
|
||||
color: '#3b82f6',
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<div class="profilesContainer">
|
||||
<dees-statsgrid .tiles=${statsTiles}></dees-statsgrid>
|
||||
<dees-table
|
||||
.heading=${'Security Profiles'}
|
||||
.data=${profiles}
|
||||
.displayFunction=${(profile: interfaces.data.ISecurityProfile) => ({
|
||||
Name: profile.name,
|
||||
Description: profile.description || '-',
|
||||
'IP Allow List': (profile.security?.ipAllowList || []).join(', ') || '-',
|
||||
'IP Block List': (profile.security?.ipBlockList || []).join(', ') || '-',
|
||||
'Max Connections': profile.security?.maxConnections ?? '-',
|
||||
'Rate Limit': profile.security?.rateLimit?.enabled ? `${profile.security.rateLimit.maxRequests}/${profile.security.rateLimit.window}s` : '-',
|
||||
Extends: (profile.extendsProfiles || []).length > 0
|
||||
? profile.extendsProfiles!.map(id => {
|
||||
const p = profiles.find(pp => pp.id === id);
|
||||
return p ? p.name : id.slice(0, 8);
|
||||
}).join(', ')
|
||||
: '-',
|
||||
})}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'Create Profile',
|
||||
iconName: 'lucide:plus',
|
||||
type: ['header' as const],
|
||||
action: async (_: any, table: any) => {
|
||||
await this.showCreateProfileDialog(table);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Refresh',
|
||||
iconName: 'lucide:rotateCw',
|
||||
type: ['header' as const],
|
||||
action: async () => {
|
||||
await appstate.profilesTargetsStatePart.dispatchAction(appstate.fetchProfilesAndTargetsAction, null);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Edit',
|
||||
iconName: 'lucide:pencil',
|
||||
type: ['contextmenu' as const],
|
||||
action: async (profile: interfaces.data.ISecurityProfile, table: any) => {
|
||||
await this.showEditProfileDialog(profile, table);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
iconName: 'lucide:trash2',
|
||||
type: ['contextmenu' as const],
|
||||
action: async (profile: interfaces.data.ISecurityProfile) => {
|
||||
await this.deleteProfile(profile);
|
||||
},
|
||||
},
|
||||
]}
|
||||
></dees-table>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async showCreateProfileDialog(table: any) {
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
DeesModal.createAndShow({
|
||||
heading: 'Create Security Profile',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'description'} .label=${'Description'}></dees-input-text>
|
||||
<dees-input-text .key=${'ipAllowList'} .label=${'IP Allow List (comma-separated)'}></dees-input-text>
|
||||
<dees-input-text .key=${'ipBlockList'} .label=${'IP Block List (comma-separated)'}></dees-input-text>
|
||||
<dees-input-text .key=${'maxConnections'} .label=${'Max Connections'}></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Create',
|
||||
action: async (modalArg: any) => {
|
||||
const form = modalArg.shadowRoot!.querySelector('dees-form');
|
||||
const data = await form.collectFormData();
|
||||
const ipAllowList = data.ipAllowList
|
||||
? String(data.ipAllowList).split(',').map((s: string) => s.trim()).filter(Boolean)
|
||||
: undefined;
|
||||
const ipBlockList = data.ipBlockList
|
||||
? String(data.ipBlockList).split(',').map((s: string) => s.trim()).filter(Boolean)
|
||||
: undefined;
|
||||
const maxConnections = data.maxConnections ? parseInt(String(data.maxConnections)) : undefined;
|
||||
|
||||
await appstate.profilesTargetsStatePart.dispatchAction(appstate.createProfileAction, {
|
||||
name: String(data.name),
|
||||
description: data.description ? String(data.description) : undefined,
|
||||
security: {
|
||||
...(ipAllowList ? { ipAllowList } : {}),
|
||||
...(ipBlockList ? { ipBlockList } : {}),
|
||||
...(maxConnections ? { maxConnections } : {}),
|
||||
},
|
||||
});
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private async showEditProfileDialog(profile: interfaces.data.ISecurityProfile, table: any) {
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
DeesModal.createAndShow({
|
||||
heading: `Edit Profile: ${profile.name}`,
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'name'} .label=${'Name'} .value=${profile.name}></dees-input-text>
|
||||
<dees-input-text .key=${'description'} .label=${'Description'} .value=${profile.description || ''}></dees-input-text>
|
||||
<dees-input-text .key=${'ipAllowList'} .label=${'IP Allow List (comma-separated)'} .value=${(profile.security?.ipAllowList || []).join(', ')}></dees-input-text>
|
||||
<dees-input-text .key=${'ipBlockList'} .label=${'IP Block List (comma-separated)'} .value=${(profile.security?.ipBlockList || []).join(', ')}></dees-input-text>
|
||||
<dees-input-text .key=${'maxConnections'} .label=${'Max Connections'} .value=${String(profile.security?.maxConnections || '')}></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Save',
|
||||
action: async (modalArg: any) => {
|
||||
const form = modalArg.shadowRoot!.querySelector('dees-form');
|
||||
const data = await form.collectFormData();
|
||||
const ipAllowList = data.ipAllowList
|
||||
? String(data.ipAllowList).split(',').map((s: string) => s.trim()).filter(Boolean)
|
||||
: [];
|
||||
const ipBlockList = data.ipBlockList
|
||||
? String(data.ipBlockList).split(',').map((s: string) => s.trim()).filter(Boolean)
|
||||
: [];
|
||||
const maxConnections = data.maxConnections ? parseInt(String(data.maxConnections)) : undefined;
|
||||
|
||||
await appstate.profilesTargetsStatePart.dispatchAction(appstate.updateProfileAction, {
|
||||
id: profile.id,
|
||||
name: String(data.name),
|
||||
description: data.description ? String(data.description) : undefined,
|
||||
security: {
|
||||
ipAllowList,
|
||||
ipBlockList,
|
||||
...(maxConnections ? { maxConnections } : {}),
|
||||
},
|
||||
});
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private async deleteProfile(profile: interfaces.data.ISecurityProfile) {
|
||||
await appstate.profilesTargetsStatePart.dispatchAction(appstate.deleteProfileAction, {
|
||||
id: profile.id,
|
||||
force: false,
|
||||
});
|
||||
|
||||
const currentState = appstate.profilesTargetsStatePart.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',
|
||||
action: async (modalArg: any) => {
|
||||
await appstate.profilesTargetsStatePart.dispatchAction(appstate.deleteProfileAction, {
|
||||
id: profile.id,
|
||||
force: true,
|
||||
});
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user