feat(route-ui): add VPN details and conditional card actions to route cards

This commit is contained in:
2026-04-02 21:16:01 +00:00
parent 35ffde9253
commit cf247117d4
4 changed files with 162 additions and 1 deletions

View File

@@ -1,5 +1,13 @@
# Changelog
## 2026-04-02 - 2.11.0 - feat(route-ui)
add VPN details and conditional card actions to route cards
- Extend route card data and rendering to display VPN access mode and allowed client tags.
- Add optional Edit and Delete action buttons that emit route-edit and route-delete events.
- Allow the route list view to control action visibility per route via a showActionsFilter callback.
- Include VPN as a visible route feature indicator in the card summary.
## 2026-04-02 - 2.10.0 - feat(docs)
document newly available catalog components and updated build configuration details

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/catalog',
version: '2.10.0',
version: '2.11.0',
description: 'UI component catalog for serve.zone'
}

View File

@@ -63,6 +63,12 @@ export interface IRouteMetadata {
lastResolvedAt?: number;
}
export interface IRouteVpn {
enabled: boolean;
mandatory?: boolean;
allowedServerDefinedClientTags?: string[];
}
export interface IRouteConfig {
id?: string;
match: IRouteMatch;
@@ -70,6 +76,7 @@ export interface IRouteConfig {
security?: IRouteSecurity;
headers?: { request?: Record<string, string>; response?: Record<string, string> };
metadata?: IRouteMetadata;
vpn?: IRouteVpn;
name?: string;
description?: string;
priority?: number;
@@ -136,6 +143,11 @@ export class SzRouteCard extends DeesElement {
rateLimit: { enabled: true, maxRequests: 100, window: 60 },
maxConnections: 1000,
},
vpn: {
enabled: true,
mandatory: true,
allowedServerDefinedClientTags: ['admin', 'devops'],
},
metadata: {
securityProfileName: 'STANDARD',
networkTargetName: 'LOSSLESS_INFRA',
@@ -150,6 +162,9 @@ export class SzRouteCard extends DeesElement {
@property({ type: Object })
public accessor route: IRouteConfig | null = null;
@property({ type: Boolean })
public accessor showActions: boolean = false;
public static styles = [
cssManager.defaultStyles,
css`
@@ -459,6 +474,83 @@ export class SzRouteCard extends DeesElement {
color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
font-size: 13px;
}
.section.vpn {
border-left-color: ${cssManager.bdTheme('#0891b2', '#06b6d4')};
}
.vpn-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 9999px;
font-size: 11px;
font-weight: 600;
white-space: nowrap;
}
.vpn-badge.mandatory {
background: ${cssManager.bdTheme('#fff7ed', 'rgba(249, 115, 22, 0.2)')};
color: ${cssManager.bdTheme('#c2410c', '#fb923c')};
}
.vpn-badge.optional {
background: ${cssManager.bdTheme('#ecfdf5', 'rgba(16, 185, 129, 0.2)')};
color: ${cssManager.bdTheme('#047857', '#34d399')};
}
.vpn-tag {
display: inline-flex;
padding: 1px 6px;
border-radius: 3px;
font-size: 12px;
font-weight: 600;
font-family: monospace;
margin-right: 4px;
margin-bottom: 2px;
background: ${cssManager.bdTheme('#ecfeff', 'rgba(6, 182, 212, 0.15)')};
color: ${cssManager.bdTheme('#0e7490', '#22d3ee')};
}
.card-actions {
display: flex;
gap: 8px;
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid ${cssManager.bdTheme('#f4f4f5', '#1a1a1a')};
justify-content: flex-end;
}
.action-btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 5px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
color: ${cssManager.bdTheme('#52525b', '#a1a1aa')};
transition: all 150ms ease;
}
.action-btn:hover {
border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.action-btn.edit:hover {
border-color: ${cssManager.bdTheme('#93c5fd', 'rgba(59, 130, 246, 0.5)')};
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
}
.action-btn.delete:hover {
border-color: ${cssManager.bdTheme('#fca5a5', 'rgba(239, 68, 68, 0.5)')};
color: ${cssManager.bdTheme('#dc2626', '#f87171')};
}
`,
];
@@ -652,11 +744,36 @@ export class SzRouteCard extends DeesElement {
`
: ''}
<!-- VPN Section -->
${this.renderVpn()}
<!-- Linked References Section -->
${this.renderLinked()}
<!-- Feature Icons Row -->
${this.renderFeatures()}
<!-- Action Buttons -->
${this.showActions ? html`
<div class="card-actions">
<button class="action-btn edit" @click=${(e: Event) => {
e.stopPropagation();
this.dispatchEvent(new CustomEvent('route-edit', {
detail: this.route,
bubbles: true,
composed: true,
}));
}}>Edit</button>
<button class="action-btn delete" @click=${(e: Event) => {
e.stopPropagation();
this.dispatchEvent(new CustomEvent('route-delete', {
detail: this.route,
bubbles: true,
composed: true,
}));
}}>Delete</button>
</div>
` : ''}
</div>
`;
}
@@ -669,6 +786,35 @@ export class SzRouteCard extends DeesElement {
)}`;
}
private renderVpn(): TemplateResult {
const vpn = this.route?.vpn;
if (!vpn?.enabled) return html``;
return html`
<div class="section vpn">
<div class="section-label">VPN Access</div>
<div class="field-row">
<span class="field-key">Mode</span>
<span class="field-value">
<span class="vpn-badge ${vpn.mandatory !== false ? 'mandatory' : 'optional'}">
${vpn.mandatory !== false ? 'VPN Only' : 'VPN + Public'}
</span>
</span>
</div>
${vpn.allowedServerDefinedClientTags?.length ? html`
<div class="field-row">
<span class="field-key">Tags</span>
<span class="field-value">
${vpn.allowedServerDefinedClientTags.map(
(tag) => html`<span class="vpn-tag">${tag}</span>`
)}
</span>
</div>
` : ''}
</div>
`;
}
private renderLinked(): TemplateResult {
const meta = this.route?.metadata;
if (!meta) return html``;
@@ -722,6 +868,9 @@ export class SzRouteCard extends DeesElement {
if (headers) {
features.push(html`<span class="feature"><span class="feature-icon">&#x2699;</span>Headers</span>`);
}
if (this.route?.vpn?.enabled) {
features.push(html`<span class="feature"><span class="feature-icon">&#x1f510;</span>VPN</span>`);
}
if (meta?.securityProfileName || meta?.networkTargetName) {
features.push(html`<span class="feature"><span class="feature-icon">&#x1f517;</span>Linked</span>`);
}

View File

@@ -76,6 +76,9 @@ export class SzRouteListView extends DeesElement {
@property({ type: Array })
public accessor routes: IRouteConfig[] = [];
@property({ attribute: false })
public accessor showActionsFilter: ((route: IRouteConfig) => boolean) | null = null;
@state()
private accessor searchQuery: string = '';
@@ -299,6 +302,7 @@ export class SzRouteListView extends DeesElement {
(route) => html`
<sz-route-card
.route=${route}
.showActions=${this.showActionsFilter?.(route) ?? false}
@click=${() => this.handleRouteClick(route)}
></sz-route-card>
`