6 Commits

5 changed files with 158 additions and 8 deletions

View File

@@ -1,5 +1,24 @@
# Changelog
## 2026-04-05 - 2.11.2 - fix(route-card)
align route card with source profile metadata and vpnOnly route configuration
- rename linked route metadata fields from security profile to source profile in rendering and feature detection
- simplify VPN display logic to use the boolean vpnOnly flag instead of the previous nested VPN configuration object
## 2026-04-04 - 2.11.1 - fix(route-card)
clarify VPN mode badge labels in route cards
- Renames VPN badge text from "VPN Only"/"VPN + Public" to "VPN Mandatory"/"VPN Voluntary" for clearer route mode descriptions.
## 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

@@ -1,6 +1,6 @@
{
"name": "@serve.zone/catalog",
"version": "2.10.0",
"version": "2.11.2",
"private": false,
"description": "UI component catalog for serve.zone",
"main": "dist_ts_web/index.js",

View File

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

View File

@@ -56,9 +56,9 @@ export interface IRouteSecurity {
}
export interface IRouteMetadata {
securityProfileRef?: string;
sourceProfileRef?: string;
networkTargetRef?: string;
securityProfileName?: string;
sourceProfileName?: string;
networkTargetName?: string;
lastResolvedAt?: number;
}
@@ -70,6 +70,8 @@ export interface IRouteConfig {
security?: IRouteSecurity;
headers?: { request?: Record<string, string>; response?: Record<string, string> };
metadata?: IRouteMetadata;
/** When true, only VPN clients whose TargetProfile matches this route get access */
vpnOnly?: boolean;
name?: string;
description?: string;
priority?: number;
@@ -136,8 +138,9 @@ export class SzRouteCard extends DeesElement {
rateLimit: { enabled: true, maxRequests: 100, window: 60 },
maxConnections: 1000,
},
vpnOnly: true,
metadata: {
securityProfileName: 'STANDARD',
sourceProfileName: 'STANDARD',
networkTargetName: 'LOSSLESS_INFRA',
},
} satisfies IRouteConfig}
@@ -150,6 +153,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 +465,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 +735,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,10 +777,26 @@ export class SzRouteCard extends DeesElement {
)}`;
}
private renderVpn(): TemplateResult {
if (!this.route?.vpnOnly) 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 mandatory">VPN Only</span>
</span>
</div>
</div>
`;
}
private renderLinked(): TemplateResult {
const meta = this.route?.metadata;
if (!meta) return html``;
const hasProfile = !!meta.securityProfileName;
const hasProfile = !!meta.sourceProfileName;
const hasTarget = !!meta.networkTargetName;
if (!hasProfile && !hasTarget) return html``;
@@ -683,7 +807,7 @@ export class SzRouteCard extends DeesElement {
? html`
<div class="field-row">
<span class="field-key">Profile</span>
<span class="field-value"><span class="linked-name">${meta.securityProfileName}</span></span>
<span class="field-value"><span class="linked-name">${meta.sourceProfileName}</span></span>
</div>
`
: ''}
@@ -722,7 +846,10 @@ export class SzRouteCard extends DeesElement {
if (headers) {
features.push(html`<span class="feature"><span class="feature-icon">&#x2699;</span>Headers</span>`);
}
if (meta?.securityProfileName || meta?.networkTargetName) {
if (this.route?.vpnOnly) {
features.push(html`<span class="feature"><span class="feature-icon">&#x1f510;</span>VPN</span>`);
}
if (meta?.sourceProfileName || 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>
`