feat(route-ui): add VPN details and conditional card actions to route cards
This commit is contained in:
@@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# 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)
|
## 2026-04-02 - 2.10.0 - feat(docs)
|
||||||
document newly available catalog components and updated build configuration details
|
document newly available catalog components and updated build configuration details
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/catalog',
|
name: '@serve.zone/catalog',
|
||||||
version: '2.10.0',
|
version: '2.11.0',
|
||||||
description: 'UI component catalog for serve.zone'
|
description: 'UI component catalog for serve.zone'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,12 @@ export interface IRouteMetadata {
|
|||||||
lastResolvedAt?: number;
|
lastResolvedAt?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IRouteVpn {
|
||||||
|
enabled: boolean;
|
||||||
|
mandatory?: boolean;
|
||||||
|
allowedServerDefinedClientTags?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface IRouteConfig {
|
export interface IRouteConfig {
|
||||||
id?: string;
|
id?: string;
|
||||||
match: IRouteMatch;
|
match: IRouteMatch;
|
||||||
@@ -70,6 +76,7 @@ export interface IRouteConfig {
|
|||||||
security?: IRouteSecurity;
|
security?: IRouteSecurity;
|
||||||
headers?: { request?: Record<string, string>; response?: Record<string, string> };
|
headers?: { request?: Record<string, string>; response?: Record<string, string> };
|
||||||
metadata?: IRouteMetadata;
|
metadata?: IRouteMetadata;
|
||||||
|
vpn?: IRouteVpn;
|
||||||
name?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
priority?: number;
|
priority?: number;
|
||||||
@@ -136,6 +143,11 @@ export class SzRouteCard extends DeesElement {
|
|||||||
rateLimit: { enabled: true, maxRequests: 100, window: 60 },
|
rateLimit: { enabled: true, maxRequests: 100, window: 60 },
|
||||||
maxConnections: 1000,
|
maxConnections: 1000,
|
||||||
},
|
},
|
||||||
|
vpn: {
|
||||||
|
enabled: true,
|
||||||
|
mandatory: true,
|
||||||
|
allowedServerDefinedClientTags: ['admin', 'devops'],
|
||||||
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
securityProfileName: 'STANDARD',
|
securityProfileName: 'STANDARD',
|
||||||
networkTargetName: 'LOSSLESS_INFRA',
|
networkTargetName: 'LOSSLESS_INFRA',
|
||||||
@@ -150,6 +162,9 @@ export class SzRouteCard extends DeesElement {
|
|||||||
@property({ type: Object })
|
@property({ type: Object })
|
||||||
public accessor route: IRouteConfig | null = null;
|
public accessor route: IRouteConfig | null = null;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public accessor showActions: boolean = false;
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
css`
|
css`
|
||||||
@@ -459,6 +474,83 @@ export class SzRouteCard extends DeesElement {
|
|||||||
color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
|
color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
|
||||||
font-size: 13px;
|
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 -->
|
<!-- Linked References Section -->
|
||||||
${this.renderLinked()}
|
${this.renderLinked()}
|
||||||
|
|
||||||
<!-- Feature Icons Row -->
|
<!-- Feature Icons Row -->
|
||||||
${this.renderFeatures()}
|
${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>
|
</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 {
|
private renderLinked(): TemplateResult {
|
||||||
const meta = this.route?.metadata;
|
const meta = this.route?.metadata;
|
||||||
if (!meta) return html``;
|
if (!meta) return html``;
|
||||||
@@ -722,6 +868,9 @@ export class SzRouteCard extends DeesElement {
|
|||||||
if (headers) {
|
if (headers) {
|
||||||
features.push(html`<span class="feature"><span class="feature-icon">⚙</span>Headers</span>`);
|
features.push(html`<span class="feature"><span class="feature-icon">⚙</span>Headers</span>`);
|
||||||
}
|
}
|
||||||
|
if (this.route?.vpn?.enabled) {
|
||||||
|
features.push(html`<span class="feature"><span class="feature-icon">🔐</span>VPN</span>`);
|
||||||
|
}
|
||||||
if (meta?.securityProfileName || meta?.networkTargetName) {
|
if (meta?.securityProfileName || meta?.networkTargetName) {
|
||||||
features.push(html`<span class="feature"><span class="feature-icon">🔗</span>Linked</span>`);
|
features.push(html`<span class="feature"><span class="feature-icon">🔗</span>Linked</span>`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ export class SzRouteListView extends DeesElement {
|
|||||||
@property({ type: Array })
|
@property({ type: Array })
|
||||||
public accessor routes: IRouteConfig[] = [];
|
public accessor routes: IRouteConfig[] = [];
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public accessor showActionsFilter: ((route: IRouteConfig) => boolean) | null = null;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private accessor searchQuery: string = '';
|
private accessor searchQuery: string = '';
|
||||||
|
|
||||||
@@ -299,6 +302,7 @@ export class SzRouteListView extends DeesElement {
|
|||||||
(route) => html`
|
(route) => html`
|
||||||
<sz-route-card
|
<sz-route-card
|
||||||
.route=${route}
|
.route=${route}
|
||||||
|
.showActions=${this.showActionsFilter?.(route) ?? false}
|
||||||
@click=${() => this.handleRouteClick(route)}
|
@click=${() => this.handleRouteClick(route)}
|
||||||
></sz-route-card>
|
></sz-route-card>
|
||||||
`
|
`
|
||||||
|
|||||||
Reference in New Issue
Block a user