diff --git a/changelog.md b/changelog.md index 2d8e868..437a682 100644 --- a/changelog.md +++ b/changelog.md @@ -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 diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index a8440bb..8d2f361 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -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' } diff --git a/ts_web/elements/sz-route-card.ts b/ts_web/elements/sz-route-card.ts index ba1a8e8..b1ef6d7 100644 --- a/ts_web/elements/sz-route-card.ts +++ b/ts_web/elements/sz-route-card.ts @@ -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; response?: Record }; 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 { ` : ''} + + ${this.renderVpn()} + ${this.renderLinked()} ${this.renderFeatures()} + + + ${this.showActions ? html` +
+ + +
+ ` : ''} `; } @@ -669,6 +786,35 @@ export class SzRouteCard extends DeesElement { )}`; } + private renderVpn(): TemplateResult { + const vpn = this.route?.vpn; + if (!vpn?.enabled) return html``; + + return html` +
+ +
+ Mode + + + ${vpn.mandatory !== false ? 'VPN Only' : 'VPN + Public'} + + +
+ ${vpn.allowedServerDefinedClientTags?.length ? html` +
+ Tags + + ${vpn.allowedServerDefinedClientTags.map( + (tag) => html`${tag}` + )} + +
+ ` : ''} +
+ `; + } + 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`Headers`); } + if (this.route?.vpn?.enabled) { + features.push(html`🔐VPN`); + } if (meta?.securityProfileName || meta?.networkTargetName) { features.push(html`🔗Linked`); } diff --git a/ts_web/elements/sz-route-list-view.ts b/ts_web/elements/sz-route-list-view.ts index ae80814..1451f5a 100644 --- a/ts_web/elements/sz-route-list-view.ts +++ b/ts_web/elements/sz-route-list-view.ts @@ -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` this.handleRouteClick(route)} > `