This commit is contained in:
2026-01-03 02:44:25 +00:00
commit 4c0e498c4e
140 changed files with 245676 additions and 0 deletions

54
ts_web/elements/index.ts Normal file
View File

@@ -0,0 +1,54 @@
// Dashboard Cards
export * from './sz-stat-card.js';
export * from './sz-resource-usage-card.js';
export * from './sz-traffic-card.js';
export * from './sz-platform-services-card.js';
export * from './sz-certificates-card.js';
export * from './sz-reverse-proxy-card.js';
export * from './sz-dns-ssl-card.js';
export * from './sz-quick-actions-card.js';
// Grid Components
export * from './sz-status-grid-cluster.js';
export * from './sz-status-grid-services.js';
export * from './sz-status-grid-network.js';
export * from './sz-status-grid-infra.js';
export * from './sz-dashboard-view.js';
// Network Views
export * from './sz-network-proxy-view.js';
export * from './sz-network-dns-view.js';
export * from './sz-network-domains-view.js';
// Registry Views
export * from './sz-registry-onebox-view.js';
export * from './sz-registry-external-view.js';
// Services Views
export * from './sz-services-list-view.js';
export * from './sz-services-backups-view.js';
export * from './sz-service-detail-view.js';
// Tokens View
export * from './sz-tokens-view.js';
// Settings View
export * from './sz-settings-view.js';
// Auth & Login
export * from './sz-login-view.js';
// Service Create
export * from './sz-service-create-view.js';
// Detail Views
export * from './sz-platform-service-detail-view.js';
export * from './sz-domain-detail-view.js';
// Demo Views
export * from './sz-demo-view-dashboard.js';
export * from './sz-demo-view-services.js';
export * from './sz-demo-view-network.js';
export * from './sz-demo-view-registries.js';
export * from './sz-demo-view-tokens.js';
export * from './sz-demo-view-settings.js';

View File

@@ -0,0 +1,155 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
declare global {
interface HTMLElementTagNameMap {
'sz-certificates-card': SzCertificatesCard;
}
}
@customElement('sz-certificates-card')
export class SzCertificatesCard extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; display: flex; gap: 16px; flex-wrap: wrap;">
<sz-certificates-card validCount="2"></sz-certificates-card>
<sz-certificates-card validCount="5" expiringCount="2"></sz-certificates-card>
<sz-certificates-card validCount="0" expiredCount="1"></sz-certificates-card>
</div>
`;
@property({ type: Number })
public accessor validCount: number = 0;
@property({ type: Number })
public accessor expiringCount: number = 0;
@property({ type: Number })
public accessor expiredCount: number = 0;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
min-width: 200px;
height: 100%;
}
.card {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
padding: 20px;
height: 100%;
box-sizing: border-box;
}
.header {
margin-bottom: 16px;
}
.title {
font-size: 16px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.subtitle {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-top: 2px;
}
.status {
display: flex;
align-items: center;
gap: 8px;
}
.status-icon {
width: 20px;
height: 20px;
}
.status-icon.valid {
color: ${cssManager.bdTheme('#22c55e', '#22c55e')};
}
.status-icon.warning {
color: ${cssManager.bdTheme('#facc15', '#facc15')};
}
.status-icon.error {
color: ${cssManager.bdTheme('#ef4444', '#ef4444')};
}
.status-text {
font-size: 14px;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.status-list {
display: flex;
flex-direction: column;
gap: 8px;
}
`,
];
public render(): TemplateResult {
return html`
<div class="card">
<div class="header">
<div class="title">Certificates</div>
<div class="subtitle">SSL/TLS certificate status</div>
</div>
<div class="status-list">
${this.validCount > 0 ? html`
<div class="status">
<svg class="status-icon valid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span class="status-text">${this.validCount} valid</span>
</div>
` : ''}
${this.expiringCount > 0 ? html`
<div class="status">
<svg class="status-icon warning" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
<span class="status-text">${this.expiringCount} expiring soon</span>
</div>
` : ''}
${this.expiredCount > 0 ? html`
<div class="status">
<svg class="status-icon error" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="15" y1="9" x2="9" y2="15"></line>
<line x1="9" y1="9" x2="15" y2="15"></line>
</svg>
<span class="status-text">${this.expiredCount} expired</span>
</div>
` : ''}
${this.validCount === 0 && this.expiringCount === 0 && this.expiredCount === 0 ? html`
<div class="status">
<span class="status-text">No certificates</span>
</div>
` : ''}
</div>
</div>
`;
}
}

View File

@@ -0,0 +1,217 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
import './sz-status-grid-cluster.js';
import './sz-status-grid-services.js';
import './sz-status-grid-network.js';
import './sz-status-grid-infra.js';
import type { IClusterStats } from './sz-status-grid-cluster.js';
import type { IResourceUsage } from './sz-resource-usage-card.js';
import type { IPlatformService } from './sz-platform-services-card.js';
import type { ITrafficData } from './sz-traffic-card.js';
import type { IProxyStatus, ICertificateStatus } from './sz-status-grid-network.js';
import type { IQuickAction } from './sz-quick-actions-card.js';
declare global {
interface HTMLElementTagNameMap {
'sz-dashboard-view': SzDashboardView;
}
}
export interface IDashboardData {
cluster: IClusterStats;
resourceUsage: IResourceUsage;
platformServices: IPlatformService[];
traffic: ITrafficData;
proxy: IProxyStatus;
certificates: ICertificateStatus;
dnsConfigured: boolean;
acmeConfigured: boolean;
quickActions: IQuickAction[];
}
@customElement('sz-dashboard-view')
export class SzDashboardView extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 1400px;">
<sz-dashboard-view
.data=${{
cluster: {
totalServices: 7,
running: 7,
stopped: 0,
dockerStatus: 'running',
},
resourceUsage: {
cpu: 0.5,
memoryUsed: '191 MB',
memoryTotal: '429.2 GB',
networkIn: '38.9 KB/s',
networkOut: '1.7 KB/s',
topConsumers: [
{ name: 'test-nginx', memory: '32.1 MB' },
{ name: 'test-v2', memory: '31.7 MB' },
],
},
platformServices: [
{ name: 'MongoDB', status: '1 DB', running: true },
{ name: 'S3 Storage (MinIO)', status: '1 bucket', running: true },
{ name: 'Caddy Reverse Proxy', status: 'Running', running: true },
{ name: 'ClickHouse', status: '1 DB', running: true },
],
traffic: {
requests: 1250,
errors: 15,
errorPercent: 1.2,
avgResponse: 145,
reqPerMin: 21,
status2xx: 85,
status3xx: 5,
status4xx: 8,
status5xx: 2,
},
proxy: {
httpPort: '80',
httpsPort: '443',
httpActive: true,
httpsActive: true,
routeCount: '12',
},
certificates: {
valid: 8,
expiring: 2,
expired: 0,
},
dnsConfigured: true,
acmeConfigured: true,
quickActions: [
{ label: 'Deploy Service', icon: 'plus', primary: true },
{ label: 'View All Services' },
{ label: 'Platform Services' },
{ label: 'Manage Domains' },
],
}}
></sz-dashboard-view>
</div>
`;
@property({ type: Object })
public accessor data: IDashboardData = {
cluster: {
totalServices: 0,
running: 0,
stopped: 0,
dockerStatus: 'stopped',
},
resourceUsage: {
cpu: 0,
memoryUsed: '0 MB',
memoryTotal: '0 GB',
networkIn: '0 KB/s',
networkOut: '0 KB/s',
topConsumers: [],
},
platformServices: [],
traffic: {
requests: 0,
errors: 0,
errorPercent: 0,
avgResponse: 0,
reqPerMin: 0,
status2xx: 0,
status3xx: 0,
status4xx: 0,
status5xx: 0,
},
proxy: {
httpPort: '80',
httpsPort: '443',
httpActive: false,
httpsActive: false,
routeCount: '0',
},
certificates: {
valid: 0,
expiring: 0,
expired: 0,
},
dnsConfigured: false,
acmeConfigured: false,
quickActions: [],
};
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
}
.dashboard {
display: flex;
flex-direction: column;
gap: 24px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
margin-bottom: 12px;
}
.section {
display: flex;
flex-direction: column;
}
`,
];
public render(): TemplateResult {
return html`
<div class="dashboard">
<section class="section">
<h2 class="section-title">Cluster Overview</h2>
<sz-status-grid-cluster
.stats=${this.data.cluster}
></sz-status-grid-cluster>
</section>
<section class="section">
<h2 class="section-title">Services & Resources</h2>
<sz-status-grid-services
.resourceUsage=${this.data.resourceUsage}
.platformServices=${this.data.platformServices}
></sz-status-grid-services>
</section>
<section class="section">
<h2 class="section-title">Network & Traffic</h2>
<sz-status-grid-network
.traffic=${this.data.traffic}
.proxy=${this.data.proxy}
.certificates=${this.data.certificates}
></sz-status-grid-network>
</section>
<section class="section">
<h2 class="section-title">Infrastructure</h2>
<sz-status-grid-infra
?dnsConfigured=${this.data.dnsConfigured}
?acmeConfigured=${this.data.acmeConfigured}
.actions=${this.data.quickActions}
@action-click=${(e: CustomEvent) => this.dispatchEvent(new CustomEvent('action-click', { detail: e.detail, bubbles: true, composed: true }))}
></sz-status-grid-infra>
</section>
</div>
`;
}
}

View File

@@ -0,0 +1,150 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
type TemplateResult,
} from '@design.estate/dees-element';
import type { DeesAppui } from '@design.estate/dees-catalog';
import './index.js';
declare global {
interface HTMLElementTagNameMap {
'sz-demo-view-dashboard': SzDemoViewDashboard;
}
}
@customElement('sz-demo-view-dashboard')
export class SzDemoViewDashboard extends DeesElement {
private appui: DeesAppui | null = null;
async onActivate(context: { appui: DeesAppui; viewId: string }) {
this.appui = context.appui;
// Dashboard secondary menu with quick actions
this.appui.setSecondaryMenu({
heading: 'Dashboard',
groups: [
{
name: 'Quick Actions',
items: [
{ type: 'action', key: 'Deploy Service', iconName: 'lucide:Rocket', action: () => { console.log('Deploy service'); } },
{ type: 'action', key: 'Add Domain', iconName: 'lucide:Globe', action: () => { console.log('Add domain'); } },
{ type: 'action', key: 'Create Token', iconName: 'lucide:Key', action: () => { console.log('Create token'); } },
],
},
{
name: 'System',
items: [
{ type: 'action', key: 'Refresh Stats', iconName: 'lucide:RefreshCw', action: () => { console.log('Refresh'); } },
{ type: 'action', key: 'View Logs', iconName: 'lucide:Terminal', action: () => { console.log('View logs'); } },
],
},
],
});
}
onDeactivate() {
// Cleanup if needed
}
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
padding: 24px;
height: 100%;
overflow-y: auto;
box-sizing: border-box;
}
.page-header {
margin-bottom: 24px;
}
.page-title {
font-size: 24px;
font-weight: 700;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
margin: 0 0 8px 0;
}
.page-subtitle {
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin: 0;
}
`,
];
public render(): TemplateResult {
return html`
<div class="page-header">
<h1 class="page-title">Dashboard</h1>
<p class="page-subtitle">Overview of your onebox infrastructure</p>
</div>
<sz-dashboard-view
.clusterStats=${{
totalServices: 12,
running: 9,
stopped: 3,
dockerStatus: 'running',
}}
.resourceUsage=${{
cpu: 45,
memoryUsed: '6.2 GB',
memoryTotal: '16 GB',
networkIn: '2.5 MB/s',
networkOut: '1.2 MB/s',
topConsumers: [
{ name: 'mongodb', memory: '1.2 GB' },
{ name: 'clickhouse', memory: '980 MB' },
{ name: 'nginx-proxy', memory: '256 MB' },
],
}}
.platformServices=${[
{ name: 'MongoDB', status: 'running', icon: 'database' },
{ name: 'S3 Storage', status: 'running', icon: 'storage' },
{ name: 'ClickHouse', status: 'stopped', icon: 'analytics' },
{ name: 'Redis Cache', status: 'running', icon: 'cache' },
]}
.traffic=${{
requests: 15420,
errors: 23,
errorPercent: 0.15,
avgResponse: 145,
reqPerMin: 856,
status2xx: 14850,
status3xx: 320,
status4xx: 227,
status5xx: 23,
}}
.proxy=${{
httpPort: '80',
httpsPort: '443',
httpActive: true,
httpsActive: true,
routeCount: '24',
}}
.certificates=${{
valid: 18,
expiring: 2,
expired: 0,
}}
.dns=${{
records: 45,
zones: 8,
pendingChanges: 0,
}}
.ssl=${{
activeCerts: 20,
autoRenew: true,
provider: "Let's Encrypt",
}}
@quick-action=${(e: CustomEvent) => console.log('Quick action:', e.detail)}
></sz-dashboard-view>
`;
}
}

View File

@@ -0,0 +1,354 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
state,
type TemplateResult,
} from '@design.estate/dees-element';
import type { DeesAppui } from '@design.estate/dees-catalog';
import './index.js';
declare global {
interface HTMLElementTagNameMap {
'sz-demo-view-network': SzDemoViewNetwork;
}
}
@customElement('sz-demo-view-network')
export class SzDemoViewNetwork extends DeesElement {
private appui: DeesAppui | null = null;
@state()
private accessor currentTab: 'proxy' | 'dns' | 'domains' | 'domain-detail' = 'proxy';
async onActivate(context: { appui: DeesAppui; viewId: string }) {
this.appui = context.appui;
// Set up content tabs
this.appui.setContentTabs([
{
key: 'Reverse Proxy',
action: () => {
this.currentTab = 'proxy';
this.updateSecondaryMenu();
},
},
{
key: 'DNS Records',
action: () => {
this.currentTab = 'dns';
this.updateSecondaryMenu();
},
},
{
key: 'Domains',
action: () => {
this.currentTab = 'domains';
this.updateSecondaryMenu();
},
},
]);
this.updateSecondaryMenu();
}
private updateSecondaryMenu() {
if (!this.appui) return;
if (this.currentTab === 'proxy') {
this.appui.setSecondaryMenu({
heading: 'Reverse Proxy',
groups: [
{
name: 'Actions',
items: [
{ type: 'action', key: 'Add Route', iconName: 'lucide:Plus', action: () => { console.log('Add route'); } },
{ type: 'action', key: 'Refresh', iconName: 'lucide:RefreshCw', action: () => { console.log('Refresh'); } },
],
},
{
name: 'Statistics',
items: [
{ type: 'header', label: '42 Active Connections' },
{ type: 'header', label: '15,420 Requests Today' },
],
},
],
});
} else if (this.currentTab === 'dns') {
this.appui.setSecondaryMenu({
heading: 'DNS Records',
groups: [
{
name: 'Actions',
items: [
{ type: 'action', key: 'Add Record', iconName: 'lucide:Plus', action: () => { console.log('Add record'); } },
{ type: 'action', key: 'Import Zone', iconName: 'lucide:Upload', action: () => { console.log('Import zone'); } },
],
},
{
name: 'Zones',
items: [
{ key: 'example.com', iconName: 'lucide:Globe', action: () => { console.log('Select example.com'); } },
{ key: 'example.net', iconName: 'lucide:Globe', action: () => { console.log('Select example.net'); } },
{ key: 'myapp.io', iconName: 'lucide:Globe', action: () => { console.log('Select myapp.io'); } },
],
},
],
});
} else if (this.currentTab === 'domains' || this.currentTab === 'domain-detail') {
this.appui.setSecondaryMenu({
heading: 'Domains',
groups: [
{
name: 'Actions',
items: [
{ type: 'action', key: 'Add Domain', iconName: 'lucide:Plus', action: () => { console.log('Add domain'); } },
{ type: 'action', key: 'Verify All', iconName: 'lucide:CheckCircle', action: () => { console.log('Verify all'); } },
],
},
{
name: 'Certificates',
items: [
{ key: 'Valid', iconName: 'lucide:ShieldCheck', badge: '3', badgeVariant: 'success', action: () => { console.log('Filter valid'); } },
{ key: 'Expiring Soon', iconName: 'lucide:AlertTriangle', badge: '1', badgeVariant: 'warning', action: () => { console.log('Filter expiring'); } },
],
},
],
});
}
}
onDeactivate() {
// Cleanup if needed
}
@state()
private accessor selectedDomain: any = null;
private demoProxyRoutes = [
{ id: '1', domain: 'api.example.com', target: 'http://api-gateway:3000', ssl: true, status: 'active' as const },
{ id: '2', domain: 'app.example.com', target: 'http://frontend:8080', ssl: true, status: 'active' as const },
{ id: '3', domain: 'admin.example.com', target: 'http://admin-panel:4000', ssl: true, status: 'active' as const },
{ id: '4', domain: 'legacy.example.com', target: 'http://legacy:5000', ssl: false, status: 'inactive' as const },
];
private demoAccessLogs = [
{ timestamp: '14:30:22', method: 'GET', path: '/api/users', status: 200, duration: '45ms', ip: '192.168.1.100' },
{ timestamp: '14:30:21', method: 'POST', path: '/api/orders', status: 201, duration: '120ms', ip: '192.168.1.105' },
{ timestamp: '14:30:20', method: 'GET', path: '/api/products', status: 200, duration: '89ms', ip: '192.168.1.100' },
{ timestamp: '14:30:19', method: 'DELETE', path: '/api/cache', status: 204, duration: '12ms', ip: '192.168.1.50' },
{ timestamp: '14:30:18', method: 'GET', path: '/health', status: 200, duration: '5ms', ip: '10.0.0.1' },
];
private demoDnsRecords = [
{ id: '1', type: 'A' as const, name: '@', value: '192.168.1.100', ttl: 3600, zone: 'example.com' },
{ id: '2', type: 'A' as const, name: 'api', value: '192.168.1.100', ttl: 3600, zone: 'example.com' },
{ id: '3', type: 'CNAME' as const, name: 'www', value: 'example.com', ttl: 3600, zone: 'example.com' },
{ id: '4', type: 'MX' as const, name: '@', value: 'mail.example.com', ttl: 3600, zone: 'example.com', priority: 10 },
{ id: '5', type: 'TXT' as const, name: '@', value: 'v=spf1 include:_spf.example.com ~all', ttl: 3600, zone: 'example.com' },
];
private demoDomains = [
{ id: '1', name: 'example.com', status: 'active' as const, ssl: true, sslExpiry: '2024-04-15', dnsProvider: 'Cloudflare', recordCount: 12 },
{ id: '2', name: 'api.example.com', status: 'active' as const, ssl: true, sslExpiry: '2024-05-20', dnsProvider: 'Cloudflare', recordCount: 3 },
{ id: '3', name: 'staging.example.com', status: 'pending' as const, ssl: false, sslExpiry: null, dnsProvider: 'Cloudflare', recordCount: 2 },
{ id: '4', name: 'legacy.example.net', status: 'active' as const, ssl: true, sslExpiry: '2024-02-10', dnsProvider: 'Manual', recordCount: 5 },
];
private demoDomainDetail = {
id: '1',
name: 'example.com',
status: 'active' as const,
verified: true,
createdAt: '2024-01-10',
proxyRoutes: ['/api/*', '/app/*', '/admin/*'],
};
private demoCertificate = {
id: '1',
domain: 'example.com',
issuer: "Let's Encrypt",
validFrom: '2024-01-10',
validUntil: '2024-04-10',
daysRemaining: 45,
status: 'valid' as const,
autoRenew: true,
chain: ['R3', 'ISRG Root X1'],
};
private demoDomainDnsRecords = [
{ id: '1', type: 'A' as const, name: '@', value: '192.168.1.100', ttl: 3600 },
{ id: '2', type: 'CNAME' as const, name: 'www', value: 'example.com', ttl: 3600 },
{ id: '3', type: 'MX' as const, name: '@', value: 'mail.example.com', ttl: 3600, priority: 10 },
{ id: '4', type: 'TXT' as const, name: '@', value: 'v=spf1 include:_spf.example.com ~all', ttl: 3600 },
];
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
padding: 24px;
height: 100%;
overflow-y: auto;
box-sizing: border-box;
}
.page-header {
margin-bottom: 24px;
}
.page-title {
font-size: 24px;
font-weight: 700;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
margin: 0 0 8px 0;
}
.page-subtitle {
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin: 0;
}
.tabs {
display: flex;
gap: 4px;
margin-bottom: 24px;
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
}
.tab {
padding: 10px 16px;
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
background: transparent;
border: none;
cursor: pointer;
position: relative;
transition: color 200ms ease;
}
.tab:hover {
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.tab.active {
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.tab.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background: ${cssManager.bdTheme('#18181b', '#fafafa')};
border-radius: 1px 1px 0 0;
}
.back-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 6px;
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
cursor: pointer;
margin-bottom: 16px;
transition: all 200ms ease;
}
.back-button:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
`,
];
public render(): TemplateResult {
if (this.currentTab === 'domain-detail') {
return this.renderDomainDetail();
}
return html`
${this.currentTab === 'proxy' ? this.renderProxyView() : ''}
${this.currentTab === 'dns' ? this.renderDnsView() : ''}
${this.currentTab === 'domains' ? this.renderDomainsView() : ''}
`;
}
private renderProxyView(): TemplateResult {
return html`
<sz-network-proxy-view
.routes=${this.demoProxyRoutes}
.accessLogs=${this.demoAccessLogs}
.stats=${{
activeConnections: 42,
requestsToday: 15420,
bytesTransferred: '2.4 GB',
}}
@add-route=${() => console.log('Add route')}
@edit-route=${(e: CustomEvent) => console.log('Edit route:', e.detail)}
@delete-route=${(e: CustomEvent) => console.log('Delete route:', e.detail)}
></sz-network-proxy-view>
`;
}
private renderDnsView(): TemplateResult {
return html`
<sz-network-dns-view
.records=${this.demoDnsRecords}
.zones=${['example.com', 'example.net', 'myapp.io']}
.selectedZone=${'example.com'}
@add-record=${() => console.log('Add record')}
@edit-record=${(e: CustomEvent) => console.log('Edit record:', e.detail)}
@delete-record=${(e: CustomEvent) => console.log('Delete record:', e.detail)}
@change-zone=${(e: CustomEvent) => console.log('Change zone:', e.detail)}
></sz-network-dns-view>
`;
}
private renderDomainsView(): TemplateResult {
return html`
<sz-network-domains-view
.domains=${this.demoDomains}
@add-domain=${() => console.log('Add domain')}
@view-domain=${(e: CustomEvent) => { this.selectedDomain = e.detail; this.currentTab = 'domain-detail'; }}
@renew-ssl=${(e: CustomEvent) => console.log('Renew SSL:', e.detail)}
@delete-domain=${(e: CustomEvent) => console.log('Delete domain:', e.detail)}
></sz-network-domains-view>
`;
}
private renderDomainDetail(): TemplateResult {
return html`
<button class="back-button" @click=${() => this.currentTab = 'domains'}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15 18 9 12 15 6"/>
</svg>
Back to Domains
</button>
<sz-domain-detail-view
.domain=${this.demoDomainDetail}
.certificate=${this.demoCertificate}
.dnsRecords=${this.demoDomainDnsRecords}
@verify-domain=${() => console.log('Verify domain')}
@delete-domain=${() => { console.log('Delete domain'); this.currentTab = 'domains'; }}
@renew-certificate=${() => console.log('Renew certificate')}
@add-dns-record=${() => console.log('Add DNS record')}
@edit-dns-record=${(e: CustomEvent) => console.log('Edit DNS record:', e.detail)}
@delete-dns-record=${(e: CustomEvent) => console.log('Delete DNS record:', e.detail)}
></sz-domain-detail-view>
`;
}
}

View File

@@ -0,0 +1,206 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
state,
type TemplateResult,
} from '@design.estate/dees-element';
import type { DeesAppui } from '@design.estate/dees-catalog';
import './index.js';
declare global {
interface HTMLElementTagNameMap {
'sz-demo-view-registries': SzDemoViewRegistries;
}
}
@customElement('sz-demo-view-registries')
export class SzDemoViewRegistries extends DeesElement {
private appui: DeesAppui | null = null;
@state()
private accessor currentTab: 'onebox' | 'external' = 'onebox';
async onActivate(context: { appui: DeesAppui; viewId: string }) {
this.appui = context.appui;
// Set up content tabs
this.appui.setContentTabs([
{
key: 'Onebox Registry',
action: () => {
this.currentTab = 'onebox';
this.updateSecondaryMenu();
},
},
{
key: 'External Registries',
action: () => {
this.currentTab = 'external';
this.updateSecondaryMenu();
},
},
]);
this.updateSecondaryMenu();
}
private updateSecondaryMenu() {
if (!this.appui) return;
if (this.currentTab === 'onebox') {
this.appui.setSecondaryMenu({
heading: 'Onebox Registry',
groups: [
{
name: 'Actions',
items: [
{ type: 'action', key: 'Push Image', iconName: 'lucide:Upload', action: () => { console.log('Push image'); } },
{ type: 'action', variant: 'danger', key: 'Garbage Collect', iconName: 'lucide:Trash2', action: () => { console.log('GC'); } },
],
},
{
name: 'Statistics',
items: [
{ type: 'header', label: '4 Images' },
{ type: 'header', label: '640 MB Total' },
],
},
],
});
} else {
this.appui.setSecondaryMenu({
heading: 'External Registries',
groups: [
{
name: 'Actions',
items: [
{ type: 'action', key: 'Add Registry', iconName: 'lucide:Plus', action: () => { console.log('Add registry'); } },
{ type: 'action', key: 'Test All', iconName: 'lucide:CheckCircle', action: () => { console.log('Test all'); } },
],
},
{
name: 'Connected',
items: [
{ key: 'Docker Hub', iconName: 'lucide:Box', action: () => { console.log('Docker Hub'); } },
{ key: 'GHCR', iconName: 'lucide:Github', action: () => { console.log('GHCR'); } },
],
},
],
});
}
}
onDeactivate() {
// Cleanup if needed
}
private demoOneboxImages = [
{ id: '1', name: 'api-gateway', tags: ['latest', 'v2.1.0', 'v2.0.0'], size: '256 MB', updated: '2024-01-20', pulls: 142 },
{ id: '2', name: 'frontend', tags: ['latest', 'v1.5.0'], size: '128 MB', updated: '2024-01-19', pulls: 89 },
{ id: '3', name: 'worker-service', tags: ['latest'], size: '64 MB', updated: '2024-01-18', pulls: 56 },
{ id: '4', name: 'admin-panel', tags: ['latest', 'v3.0.0', 'v2.9.0'], size: '192 MB', updated: '2024-01-17', pulls: 34 },
];
private demoExternalRegistries = [
{ id: '1', name: 'Docker Hub', url: 'docker.io', status: 'connected' as const, images: 12 },
{ id: '2', name: 'GitHub Container Registry', url: 'ghcr.io', status: 'connected' as const, images: 8 },
{ id: '3', name: 'AWS ECR', url: '123456789.dkr.ecr.us-east-1.amazonaws.com', status: 'error' as const, images: 0 },
];
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
padding: 24px;
height: 100%;
overflow-y: auto;
box-sizing: border-box;
}
.page-header {
margin-bottom: 24px;
}
.page-title {
font-size: 24px;
font-weight: 700;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
margin: 0 0 8px 0;
}
.page-subtitle {
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin: 0;
}
.tabs {
display: flex;
gap: 4px;
margin-bottom: 24px;
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
}
.tab {
padding: 10px 16px;
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
background: transparent;
border: none;
cursor: pointer;
position: relative;
transition: color 200ms ease;
}
.tab:hover {
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.tab.active {
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.tab.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background: ${cssManager.bdTheme('#18181b', '#fafafa')};
border-radius: 1px 1px 0 0;
}
`,
];
public render(): TemplateResult {
return html`
${this.currentTab === 'onebox' ? html`
<sz-registry-onebox-view
.images=${this.demoOneboxImages}
.stats=${{
totalImages: 4,
totalSize: '640 MB',
totalPulls: 321,
}}
@view-image=${(e: CustomEvent) => console.log('View image:', e.detail)}
@delete-image=${(e: CustomEvent) => console.log('Delete image:', e.detail)}
@delete-tag=${(e: CustomEvent) => console.log('Delete tag:', e.detail)}
></sz-registry-onebox-view>
` : html`
<sz-registry-external-view
.registries=${this.demoExternalRegistries}
@add-registry=${() => console.log('Add registry')}
@edit-registry=${(e: CustomEvent) => console.log('Edit registry:', e.detail)}
@delete-registry=${(e: CustomEvent) => console.log('Delete registry:', e.detail)}
@test-connection=${(e: CustomEvent) => console.log('Test connection:', e.detail)}
></sz-registry-external-view>
`}
`;
}
}

View File

@@ -0,0 +1,434 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
state,
type TemplateResult,
} from '@design.estate/dees-element';
import type { DeesAppui } from '@design.estate/dees-catalog';
import './index.js';
declare global {
interface HTMLElementTagNameMap {
'sz-demo-view-services': SzDemoViewServices;
}
}
@customElement('sz-demo-view-services')
export class SzDemoViewServices extends DeesElement {
private appui: DeesAppui | null = null;
@state()
private accessor currentView: 'list' | 'create' | 'detail' | 'backups' | 'platform-detail' = 'list';
@state()
private accessor selectedService: any = null;
@state()
private accessor selectedPlatformService: any = null;
private demoServices = [
{
id: '1',
name: 'nginx-proxy',
image: 'nginx:latest',
status: 'running',
cpu: '2.5%',
memory: '256 MB',
ports: '80, 443',
uptime: '5d 12h',
},
{
id: '2',
name: 'api-gateway',
image: 'api-gateway:v2.1.0',
status: 'running',
cpu: '8.2%',
memory: '512 MB',
ports: '3000',
uptime: '3d 8h',
},
{
id: '3',
name: 'worker-service',
image: 'worker:latest',
status: 'stopped',
cpu: '0%',
memory: '0 MB',
ports: '-',
uptime: '-',
},
{
id: '4',
name: 'redis-cache',
image: 'redis:7-alpine',
status: 'running',
cpu: '1.2%',
memory: '128 MB',
ports: '6379',
uptime: '10d 4h',
},
];
private demoPlatformService = {
id: '1',
name: 'MongoDB',
type: 'mongodb' as const,
status: 'running' as const,
version: '7.0.4',
host: 'localhost',
port: 27017,
credentials: { username: 'admin', password: '••••••••' },
config: { replicaSet: 'rs0', authEnabled: true, journaling: true },
metrics: { cpu: 12, memory: 45, storage: 23, connections: 8 },
};
private demoPlatformLogs = [
{ timestamp: '2024-01-20 14:30:22', level: 'info' as const, message: 'Connection accepted from 127.0.0.1:54321' },
{ timestamp: '2024-01-20 14:30:20', level: 'info' as const, message: 'Index build completed on collection users' },
{ timestamp: '2024-01-20 14:30:15', level: 'warn' as const, message: 'Slow query detected: 1.2s on collection orders' },
{ timestamp: '2024-01-20 14:30:10', level: 'info' as const, message: 'Checkpoint complete' },
];
private demoBackupSchedules = [
{ id: '1', scope: 'All Services', retention: 'D:7, W:4, M:12', schedule: '0 2 * * *', lastRun: '1/2/2026, 2:00:03 AM', nextRun: '1/3/2026, 2:00:00 AM', status: 'active' as const },
];
private demoBackups = [
{ id: '1', service: 'nginx-proxy', createdAt: '1/2/2026, 2:00:03 AM', size: '22.0 MB', includes: ['Image'] },
{ id: '2', service: 'api-gateway', createdAt: '1/2/2026, 2:00:02 AM', size: '156.5 MB', includes: ['Image', 'Volumes'] },
{ id: '3', service: 'redis-cache', createdAt: '1/2/2026, 2:00:00 AM', size: '48.0 MB', includes: ['Image', 'Data'] },
];
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
padding: 24px;
height: 100%;
overflow-y: auto;
box-sizing: border-box;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24px;
}
.header-info {
display: flex;
flex-direction: column;
gap: 8px;
}
.page-title {
font-size: 24px;
font-weight: 700;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
margin: 0;
}
.page-subtitle {
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin: 0;
}
.header-actions {
display: flex;
gap: 8px;
}
.action-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: ${cssManager.bdTheme('#18181b', '#fafafa')};
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('#fafafa', '#18181b')};
cursor: pointer;
transition: all 200ms ease;
}
.action-button:hover {
opacity: 0.9;
}
.action-button.secondary {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.action-button.secondary:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
.tabs {
display: flex;
gap: 4px;
margin-bottom: 24px;
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
padding-bottom: 0;
}
.tab {
padding: 10px 16px;
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
background: transparent;
border: none;
cursor: pointer;
position: relative;
transition: color 200ms ease;
}
.tab:hover {
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.tab.active {
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.tab.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background: ${cssManager.bdTheme('#18181b', '#fafafa')};
border-radius: 1px 1px 0 0;
}
.back-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 6px;
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
cursor: pointer;
margin-bottom: 16px;
transition: all 200ms ease;
}
.back-button:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
`,
];
public render(): TemplateResult {
return html`
${this.currentView === 'list' ? this.renderListView() : ''}
${this.currentView === 'create' ? this.renderCreateView() : ''}
${this.currentView === 'detail' ? this.renderDetailView() : ''}
${this.currentView === 'backups' ? this.renderBackupsView() : ''}
${this.currentView === 'platform-detail' ? this.renderPlatformDetailView() : ''}
`;
}
private renderListView(): TemplateResult {
return html`
<div class="page-header">
<div class="header-info">
<h1 class="page-title">Services</h1>
<p class="page-subtitle">Manage your Docker containers and platform services</p>
</div>
<div class="header-actions">
<button class="action-button secondary" @click=${() => this.currentView = 'backups'}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
Backups
</button>
<button class="action-button" @click=${() => this.currentView = 'create'}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>
Deploy Service
</button>
</div>
</div>
<div class="tabs">
<button class="tab active">Docker Services</button>
<button class="tab" @click=${() => { this.selectedPlatformService = this.demoPlatformService; this.currentView = 'platform-detail'; }}>Platform Services</button>
</div>
<sz-services-list-view
.services=${this.demoServices}
@view-service=${(e: CustomEvent) => { this.selectedService = e.detail; this.currentView = 'detail'; }}
@start-service=${(e: CustomEvent) => console.log('Start service:', e.detail)}
@stop-service=${(e: CustomEvent) => console.log('Stop service:', e.detail)}
@restart-service=${(e: CustomEvent) => console.log('Restart service:', e.detail)}
@delete-service=${(e: CustomEvent) => console.log('Delete service:', e.detail)}
></sz-services-list-view>
`;
}
private renderCreateView(): TemplateResult {
return html`
<button class="back-button" @click=${() => this.currentView = 'list'}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15 18 9 12 15 6"/>
</svg>
Back to Services
</button>
<sz-service-create-view
.registries=${[
{ id: '1', name: 'Onebox Registry', url: 'registry.onebox.local' },
{ id: '2', name: 'Docker Hub', url: 'docker.io' },
]}
@create-service=${(e: CustomEvent) => { console.log('Create service:', e.detail); this.currentView = 'list'; }}
@cancel=${() => this.currentView = 'list'}
></sz-service-create-view>
`;
}
private renderDetailView(): TemplateResult {
return html`
<button class="back-button" @click=${() => this.currentView = 'list'}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15 18 9 12 15 6"/>
</svg>
Back to Services
</button>
<sz-service-detail-view
.service=${{
id: this.selectedService?.id || '1',
name: this.selectedService?.name || 'nginx-proxy',
image: this.selectedService?.image || 'nginx:latest',
status: this.selectedService?.status || 'running',
ports: [{ host: '80', container: '80' }, { host: '443', container: '443' }],
envVars: [
{ key: 'NGINX_HOST', value: 'localhost' },
{ key: 'NGINX_PORT', value: '80' },
],
volumes: [
{ host: '/data/nginx/conf', container: '/etc/nginx/conf.d' },
],
createdAt: '2024-01-15 10:30:00',
restartPolicy: 'always',
}}
.logs=${[
{ timestamp: '2024-01-20 14:30:22', level: 'info', message: '127.0.0.1 - - [20/Jan/2024:14:30:22 +0000] "GET / HTTP/1.1" 200 612' },
{ timestamp: '2024-01-20 14:30:21', level: 'info', message: '127.0.0.1 - - [20/Jan/2024:14:30:21 +0000] "GET /api/health HTTP/1.1" 200 15' },
{ timestamp: '2024-01-20 14:30:20', level: 'warn', message: 'upstream timed out (110: Connection timed out)' },
{ timestamp: '2024-01-20 14:30:19', level: 'info', message: '127.0.0.1 - - [20/Jan/2024:14:30:19 +0000] "POST /api/data HTTP/1.1" 201 89' },
]}
@start=${() => console.log('Start')}
@stop=${() => console.log('Stop')}
@restart=${() => console.log('Restart')}
></sz-service-detail-view>
`;
}
private renderBackupsView(): TemplateResult {
return html`
<button class="back-button" @click=${() => this.currentView = 'list'}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15 18 9 12 15 6"/>
</svg>
Back to Services
</button>
<div class="page-header">
<div class="header-info">
<h1 class="page-title">Backups</h1>
<p class="page-subtitle">Manage backup schedules and restore points</p>
</div>
</div>
<sz-services-backups-view
.schedules=${this.demoBackupSchedules}
.backups=${this.demoBackups}
@create-schedule=${() => console.log('Create schedule')}
@run-now=${(e: CustomEvent) => console.log('Run now:', e.detail)}
@download=${(e: CustomEvent) => console.log('Download:', e.detail)}
></sz-services-backups-view>
`;
}
private renderPlatformDetailView(): TemplateResult {
return html`
<button class="back-button" @click=${() => this.currentView = 'list'}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15 18 9 12 15 6"/>
</svg>
Back to Services
</button>
<sz-platform-service-detail-view
.service=${this.demoPlatformService}
.logs=${this.demoPlatformLogs}
@start=${() => console.log('Start')}
@stop=${() => console.log('Stop')}
@restart=${() => console.log('Restart')}
></sz-platform-service-detail-view>
`;
}
async onActivate(context: { appui: DeesAppui; viewId: string }) {
this.appui = context.appui;
// Set up content tabs
this.appui.setContentTabs([
{ key: 'Docker Services', action: () => { this.currentView = 'list'; this.updateSecondaryMenu(); } },
{ key: 'Platform Services', action: () => { this.currentView = 'platform-detail'; this.updateSecondaryMenu(); } },
{ key: 'Backups', action: () => { this.currentView = 'backups'; this.updateSecondaryMenu(); } },
]);
this.updateSecondaryMenu();
}
private updateSecondaryMenu() {
if (!this.appui) return;
this.appui.setSecondaryMenu({
heading: 'Services',
groups: [
{
name: 'Actions',
items: [
{ type: 'action', key: 'Deploy Service', iconName: 'lucide:Plus', action: () => { this.currentView = 'create'; } },
{ type: 'action', key: 'Refresh', iconName: 'lucide:RefreshCw', action: () => { console.log('Refresh'); } },
],
},
{
name: 'Quick Filters',
items: [
{ key: 'Running', iconName: 'lucide:Play', badge: '3', badgeVariant: 'success', action: () => { console.log('Filter running'); } },
{ key: 'Stopped', iconName: 'lucide:Square', badge: '1', action: () => { console.log('Filter stopped'); } },
],
},
],
});
}
onDeactivate() {
// Cleanup if needed
}
}

View File

@@ -0,0 +1,118 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
type TemplateResult,
} from '@design.estate/dees-element';
import type { DeesAppui } from '@design.estate/dees-catalog';
import './index.js';
declare global {
interface HTMLElementTagNameMap {
'sz-demo-view-settings': SzDemoViewSettings;
}
}
@customElement('sz-demo-view-settings')
export class SzDemoViewSettings extends DeesElement {
private appui: DeesAppui | null = null;
async onActivate(context: { appui: DeesAppui; viewId: string }) {
this.appui = context.appui;
// Settings doesn't need content tabs, but we set up the secondary menu
this.appui.setSecondaryMenu({
heading: 'Settings',
groups: [
{
name: 'Categories',
items: [
{ key: 'General', iconName: 'lucide:Settings', action: () => { console.log('General settings'); } },
{ key: 'Network', iconName: 'lucide:Network', action: () => { console.log('Network settings'); } },
{ key: 'Security', iconName: 'lucide:Shield', action: () => { console.log('Security settings'); } },
{ key: 'Certificates', iconName: 'lucide:FileBadge', action: () => { console.log('Certificate settings'); } },
],
},
{
name: 'Actions',
items: [
{ type: 'action', key: 'Export Config', iconName: 'lucide:Download', action: () => { console.log('Export config'); } },
{ type: 'action', key: 'Import Config', iconName: 'lucide:Upload', action: () => { console.log('Import config'); } },
{ type: 'action', variant: 'danger', key: 'Reset to Default', iconName: 'lucide:RotateCcw', confirmMessage: 'Are you sure you want to reset all settings to default?', action: () => { console.log('Reset settings'); } },
],
},
],
});
}
onDeactivate() {
// Cleanup if needed
}
private demoSettings = {
darkMode: true,
cloudflareToken: '',
cloudflareZoneId: '',
autoRenewCerts: true,
renewalThreshold: 30,
acmeEmail: 'admin@serve.zone',
httpPort: 80,
httpsPort: 443,
forceHttps: true,
};
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
padding: 24px;
height: 100%;
overflow-y: auto;
box-sizing: border-box;
}
.page-header {
margin-bottom: 24px;
}
.page-title {
font-size: 24px;
font-weight: 700;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
margin: 0 0 8px 0;
}
.page-subtitle {
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin: 0;
}
.settings-container {
max-width: 800px;
}
`,
];
public render(): TemplateResult {
return html`
<div class="page-header">
<h1 class="page-title">Settings</h1>
<p class="page-subtitle">Configure your onebox instance</p>
</div>
<div class="settings-container">
<sz-settings-view
.settings=${this.demoSettings}
currentUser="admin"
@setting-change=${(e: CustomEvent) => console.log('Setting change:', e.detail)}
@change-password=${(e: CustomEvent) => console.log('Change password:', e.detail)}
@reset=${() => console.log('Reset settings')}
@save=${(e: CustomEvent) => console.log('Save settings:', e.detail)}
></sz-settings-view>
</div>
`;
}
}

View File

@@ -0,0 +1,109 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
type TemplateResult,
} from '@design.estate/dees-element';
import type { DeesAppui } from '@design.estate/dees-catalog';
import './index.js';
declare global {
interface HTMLElementTagNameMap {
'sz-demo-view-tokens': SzDemoViewTokens;
}
}
@customElement('sz-demo-view-tokens')
export class SzDemoViewTokens extends DeesElement {
private appui: DeesAppui | null = null;
async onActivate(context: { appui: DeesAppui; viewId: string }) {
this.appui = context.appui;
// Tokens secondary menu
this.appui.setSecondaryMenu({
heading: 'Tokens',
groups: [
{
name: 'Actions',
items: [
{ type: 'action', key: 'Create Global Token', iconName: 'lucide:Plus', action: () => { console.log('Create global token'); } },
{ type: 'action', key: 'Create CI Token', iconName: 'lucide:GitBranch', action: () => { console.log('Create CI token'); } },
],
},
{
name: 'Token Types',
items: [
{ key: 'Global Tokens', iconName: 'lucide:Key', badge: '3', action: () => { console.log('Filter global'); } },
{ key: 'CI Tokens', iconName: 'lucide:Cpu', badge: '3', action: () => { console.log('Filter CI'); } },
],
},
],
});
}
onDeactivate() {
// Cleanup if needed
}
private demoGlobalTokens = [
{ id: '1', name: 'CI/CD Pipeline', type: 'global' as const, createdAt: '2024-01-15', lastUsed: '2024-01-20' },
{ id: '2', name: 'Development', type: 'global' as const, createdAt: '2024-01-10' },
{ id: '3', name: 'Production Deploy', type: 'global' as const, createdAt: '2024-01-05', lastUsed: '2024-01-19' },
];
private demoCiTokens = [
{ id: '4', name: 'api-gateway-ci', type: 'ci' as const, service: 'api-gateway', createdAt: '2024-01-18', lastUsed: '2024-01-20' },
{ id: '5', name: 'frontend-ci', type: 'ci' as const, service: 'frontend', createdAt: '2024-01-12', lastUsed: '2024-01-19' },
{ id: '6', name: 'worker-service-ci', type: 'ci' as const, service: 'worker-service', createdAt: '2024-01-08' },
];
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
padding: 24px;
height: 100%;
overflow-y: auto;
box-sizing: border-box;
}
.page-header {
margin-bottom: 24px;
}
.page-title {
font-size: 24px;
font-weight: 700;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
margin: 0 0 8px 0;
}
.page-subtitle {
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin: 0;
}
`,
];
public render(): TemplateResult {
return html`
<div class="page-header">
<h1 class="page-title">Tokens</h1>
<p class="page-subtitle">Manage registry access tokens for CI/CD pipelines</p>
</div>
<sz-tokens-view
.globalTokens=${this.demoGlobalTokens}
.ciTokens=${this.demoCiTokens}
@create=${(e: CustomEvent) => console.log('Create token:', e.detail)}
@copy=${(e: CustomEvent) => console.log('Copy token:', e.detail)}
@regenerate=${(e: CustomEvent) => console.log('Regenerate token:', e.detail)}
@delete=${(e: CustomEvent) => console.log('Delete token:', e.detail)}
></sz-tokens-view>
`;
}
}

View File

@@ -0,0 +1,130 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
declare global {
interface HTMLElementTagNameMap {
'sz-dns-ssl-card': SzDnsSslCard;
}
}
@customElement('sz-dns-ssl-card')
export class SzDnsSslCard extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 400px;">
<sz-dns-ssl-card
dnsConfigured
acmeConfigured
></sz-dns-ssl-card>
</div>
`;
@property({ type: Boolean })
public accessor dnsConfigured: boolean = false;
@property({ type: Boolean })
public accessor acmeConfigured: boolean = false;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
height: 100%;
}
.card {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
padding: 20px;
height: 100%;
box-sizing: border-box;
}
.header {
margin-bottom: 16px;
}
.title {
font-size: 16px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.subtitle {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-top: 2px;
}
.items {
display: flex;
flex-direction: column;
gap: 10px;
}
.item {
display: flex;
justify-content: space-between;
align-items: center;
}
.item-label {
font-size: 14px;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.badge {
display: inline-flex;
align-items: center;
padding: 2px 10px;
border-radius: 9999px;
font-size: 12px;
font-weight: 500;
}
.badge.configured {
background: ${cssManager.bdTheme('#dbeafe', 'rgba(59, 130, 246, 0.2)')};
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
}
.badge.not-configured {
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
`,
];
public render(): TemplateResult {
return html`
<div class="card">
<div class="header">
<div class="title">DNS & SSL</div>
<div class="subtitle">Configuration status</div>
</div>
<div class="items">
<div class="item">
<span class="item-label">Cloudflare DNS</span>
<span class="badge ${this.dnsConfigured ? 'configured' : 'not-configured'}">
${this.dnsConfigured ? 'Configured' : 'Not configured'}
</span>
</div>
<div class="item">
<span class="item-label">ACME (Let's Encrypt)</span>
<span class="badge ${this.acmeConfigured ? 'configured' : 'not-configured'}">
${this.acmeConfigured ? 'Configured' : 'Not configured'}
</span>
</div>
</div>
</div>
`;
}
}

View File

@@ -0,0 +1,766 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
declare global {
interface HTMLElementTagNameMap {
'sz-domain-detail-view': SzDomainDetailView;
}
}
export interface IDomainDetail {
id: string;
name: string;
status: 'active' | 'pending' | 'error';
verified: boolean;
createdAt: string;
proxyRoutes?: string[];
}
export interface ICertificateDetail {
id: string;
domain: string;
issuer: string;
validFrom: string;
validUntil: string;
daysRemaining: number;
status: 'valid' | 'expiring' | 'expired';
autoRenew: boolean;
chain?: string[];
}
export interface IDnsRecordDetail {
id: string;
type: 'A' | 'AAAA' | 'CNAME' | 'MX' | 'TXT' | 'NS' | 'SRV';
name: string;
value: string;
ttl: number;
priority?: number;
}
@customElement('sz-domain-detail-view')
export class SzDomainDetailView extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 1000px;">
<sz-domain-detail-view
.domain=${{
id: '1',
name: 'example.com',
status: 'active',
verified: true,
createdAt: '2024-01-10',
proxyRoutes: ['/api/*', '/app/*'],
}}
.certificate=${{
id: '1',
domain: 'example.com',
issuer: "Let's Encrypt",
validFrom: '2024-01-10',
validUntil: '2024-04-10',
daysRemaining: 45,
status: 'valid',
autoRenew: true,
chain: ['R3', 'ISRG Root X1'],
}}
.dnsRecords=${[
{ id: '1', type: 'A', name: '@', value: '192.168.1.100', ttl: 3600 },
{ id: '2', type: 'CNAME', name: 'www', value: 'example.com', ttl: 3600 },
{ id: '3', type: 'MX', name: '@', value: 'mail.example.com', ttl: 3600, priority: 10 },
{ id: '4', type: 'TXT', name: '@', value: 'v=spf1 include:_spf.example.com ~all', ttl: 3600 },
]}
></sz-domain-detail-view>
</div>
`;
@property({ type: Object })
public accessor domain: IDomainDetail | null = null;
@property({ type: Object })
public accessor certificate: ICertificateDetail | null = null;
@property({ type: Array })
public accessor dnsRecords: IDnsRecordDetail[] = [];
@property({ type: Boolean })
public accessor actionLoading: boolean = false;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
}
.header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24px;
}
.header-info {
display: flex;
flex-direction: column;
gap: 8px;
}
.domain-name {
font-size: 24px;
font-weight: 700;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
display: flex;
align-items: center;
gap: 12px;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 9999px;
font-size: 12px;
font-weight: 500;
}
.status-badge.active {
background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')};
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.status-badge.pending {
background: ${cssManager.bdTheme('#fef3c7', 'rgba(245, 158, 11, 0.2)')};
color: ${cssManager.bdTheme('#d97706', '#f59e0b')};
}
.status-badge.error {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
.domain-meta {
display: flex;
align-items: center;
gap: 16px;
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.verified-badge {
display: inline-flex;
align-items: center;
gap: 4px;
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.verified-badge svg {
width: 16px;
height: 16px;
}
.header-actions {
display: flex;
gap: 8px;
}
.action-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 6px;
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
cursor: pointer;
transition: all 200ms ease;
}
.action-button:hover:not(:disabled) {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
.action-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.action-button svg {
width: 14px;
height: 14px;
}
.action-button.danger {
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
border-color: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.3)')};
}
.action-button.danger:hover:not(:disabled) {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
@media (max-width: 768px) {
.grid {
grid-template-columns: 1fr;
}
}
.section {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
overflow: hidden;
}
.section.full-width {
grid-column: 1 / -1;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
.section-title {
font-size: 14px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
display: flex;
align-items: center;
gap: 8px;
}
.section-title svg {
width: 16px;
height: 16px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.section-action {
padding: 6px 10px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 4px;
font-size: 12px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
cursor: pointer;
transition: all 200ms ease;
}
.section-action:hover {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.section-content {
padding: 16px;
}
.cert-status {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: ${cssManager.bdTheme('#f0fdf4', 'rgba(34, 197, 94, 0.1)')};
border-radius: 8px;
margin-bottom: 16px;
}
.cert-status.expiring {
background: ${cssManager.bdTheme('#fffbeb', 'rgba(245, 158, 11, 0.1)')};
}
.cert-status.expired {
background: ${cssManager.bdTheme('#fef2f2', 'rgba(239, 68, 68, 0.1)')};
}
.cert-icon {
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.cert-icon.valid {
background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')};
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.cert-icon.expiring {
background: ${cssManager.bdTheme('#fef3c7', 'rgba(245, 158, 11, 0.2)')};
color: ${cssManager.bdTheme('#d97706', '#f59e0b')};
}
.cert-icon.expired {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.cert-icon svg {
width: 20px;
height: 20px;
}
.cert-info {
flex: 1;
}
.cert-title {
font-size: 14px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.cert-subtitle {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-top: 2px;
}
.cert-days {
font-size: 24px;
font-weight: 700;
text-align: center;
}
.cert-days.valid {
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.cert-days.expiring {
color: ${cssManager.bdTheme('#d97706', '#f59e0b')};
}
.cert-days.expired {
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.cert-days-label {
font-size: 11px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
text-align: center;
margin-top: 2px;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#27272a')};
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.info-value {
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.info-value.enabled {
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.chain-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.chain-badge {
padding: 4px 8px;
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
border-radius: 4px;
font-size: 12px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.dns-table {
width: 100%;
}
.dns-header {
display: grid;
grid-template-columns: 80px 1fr 2fr 80px 60px;
gap: 12px;
padding: 10px 0;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
}
.dns-row {
display: grid;
grid-template-columns: 80px 1fr 2fr 80px 60px;
gap: 12px;
padding: 12px 0;
font-size: 13px;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#27272a')};
align-items: center;
}
.dns-row:last-child {
border-bottom: none;
}
.dns-row:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
margin: 0 -16px;
padding-left: 16px;
padding-right: 16px;
}
.dns-type {
padding: 2px 8px;
background: ${cssManager.bdTheme('#dbeafe', 'rgba(59, 130, 246, 0.2)')};
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
border-radius: 4px;
font-size: 11px;
font-weight: 600;
text-align: center;
}
.dns-name {
font-family: monospace;
}
.dns-value {
font-family: monospace;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dns-ttl {
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.dns-actions {
display: flex;
gap: 4px;
}
.icon-button {
padding: 4px;
background: transparent;
border: none;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
cursor: pointer;
border-radius: 4px;
transition: all 200ms ease;
}
.icon-button:hover {
background: ${cssManager.bdTheme('#e4e4e7', '#27272a')};
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.icon-button.danger:hover {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.icon-button svg {
width: 14px;
height: 14px;
}
.routes-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.route-badge {
padding: 6px 10px;
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
border-radius: 4px;
font-size: 13px;
font-family: monospace;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.empty-state {
text-align: center;
padding: 24px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
font-size: 14px;
}
`,
];
public render(): TemplateResult {
if (!this.domain) {
return html`<div class="empty-state">No domain selected</div>`;
}
return html`
<div class="header">
<div class="header-info">
<div class="domain-name">
${this.domain.name}
<span class="status-badge ${this.domain.status}">
<span class="status-dot"></span>
${this.domain.status.charAt(0).toUpperCase() + this.domain.status.slice(1)}
</span>
</div>
<div class="domain-meta">
${this.domain.verified ? html`
<span class="verified-badge">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
Verified
</span>
` : html`<span>Not verified</span>`}
<span>Added ${this.domain.createdAt}</span>
</div>
</div>
<div class="header-actions">
${!this.domain.verified ? html`
<button class="action-button" ?disabled=${this.actionLoading} @click=${() => this.handleVerify()}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
Verify Domain
</button>
` : ''}
<button class="action-button danger" ?disabled=${this.actionLoading} @click=${() => this.handleDelete()}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
Delete
</button>
</div>
</div>
<div class="grid">
<!-- Certificate Section -->
<div class="section">
<div class="section-header">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
SSL Certificate
</div>
${this.certificate ? html`
<button class="section-action" @click=${() => this.handleRenewCertificate()}>Renew</button>
` : ''}
</div>
<div class="section-content">
${this.certificate ? html`
<div class="cert-status ${this.certificate.status}">
<div class="cert-icon ${this.certificate.status}">
${this.certificate.status === 'valid' ? html`
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
` : this.certificate.status === 'expiring' ? html`
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
` : html`
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="15" y1="9" x2="9" y2="15"></line>
<line x1="9" y1="9" x2="15" y2="15"></line>
</svg>
`}
</div>
<div class="cert-info">
<div class="cert-title">${this.certificate.status === 'valid' ? 'Certificate Valid' : this.certificate.status === 'expiring' ? 'Certificate Expiring Soon' : 'Certificate Expired'}</div>
<div class="cert-subtitle">Issued by ${this.certificate.issuer}</div>
</div>
<div>
<div class="cert-days ${this.certificate.status}">${Math.abs(this.certificate.daysRemaining)}</div>
<div class="cert-days-label">${this.certificate.daysRemaining >= 0 ? 'days left' : 'days ago'}</div>
</div>
</div>
<div class="info-row">
<span class="info-label">Valid From</span>
<span class="info-value">${this.certificate.validFrom}</span>
</div>
<div class="info-row">
<span class="info-label">Valid Until</span>
<span class="info-value">${this.certificate.validUntil}</span>
</div>
<div class="info-row">
<span class="info-label">Auto-Renew</span>
<span class="info-value ${this.certificate.autoRenew ? 'enabled' : ''}">${this.certificate.autoRenew ? 'Enabled' : 'Disabled'}</span>
</div>
${this.certificate.chain && this.certificate.chain.length > 0 ? html`
<div class="info-row">
<span class="info-label">Certificate Chain</span>
<div class="chain-list">
${this.certificate.chain.map(cert => html`<span class="chain-badge">${cert}</span>`)}
</div>
</div>
` : ''}
` : html`
<div class="empty-state">No certificate configured</div>
`}
</div>
</div>
<!-- Proxy Routes Section -->
<div class="section">
<div class="section-header">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="16 3 21 3 21 8"></polyline>
<line x1="4" y1="20" x2="21" y2="3"></line>
<polyline points="21 16 21 21 16 21"></polyline>
<line x1="15" y1="15" x2="21" y2="21"></line>
<line x1="4" y1="4" x2="9" y2="9"></line>
</svg>
Proxy Routes
</div>
</div>
<div class="section-content">
${this.domain.proxyRoutes && this.domain.proxyRoutes.length > 0 ? html`
<div class="routes-list">
${this.domain.proxyRoutes.map(route => html`
<span class="route-badge">${route}</span>
`)}
</div>
` : html`
<div class="empty-state">No proxy routes configured</div>
`}
</div>
</div>
<!-- DNS Records Section -->
<div class="section full-width">
<div class="section-header">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="2" y1="12" x2="22" y2="12"></line>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
</svg>
DNS Records
</div>
<button class="section-action" @click=${() => this.handleAddDnsRecord()}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 4px;">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Add Record
</button>
</div>
<div class="section-content">
${this.dnsRecords.length > 0 ? html`
<div class="dns-table">
<div class="dns-header">
<span>Type</span>
<span>Name</span>
<span>Value</span>
<span>TTL</span>
<span></span>
</div>
${this.dnsRecords.map(record => html`
<div class="dns-row">
<span class="dns-type">${record.type}</span>
<span class="dns-name">${record.name}</span>
<span class="dns-value" title="${record.value}">${record.priority ? `${record.priority} ` : ''}${record.value}</span>
<span class="dns-ttl">${record.ttl}s</span>
<span class="dns-actions">
<button class="icon-button" title="Edit" @click=${() => this.handleEditDnsRecord(record)}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
</button>
<button class="icon-button danger" title="Delete" @click=${() => this.handleDeleteDnsRecord(record)}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
</span>
</div>
`)}
</div>
` : html`
<div class="empty-state">No DNS records configured</div>
`}
</div>
</div>
</div>
`;
}
private handleVerify() {
this.dispatchEvent(new CustomEvent('verify-domain', { detail: this.domain, bubbles: true, composed: true }));
}
private handleDelete() {
this.dispatchEvent(new CustomEvent('delete-domain', { detail: this.domain, bubbles: true, composed: true }));
}
private handleRenewCertificate() {
this.dispatchEvent(new CustomEvent('renew-certificate', { detail: this.certificate, bubbles: true, composed: true }));
}
private handleAddDnsRecord() {
this.dispatchEvent(new CustomEvent('add-dns-record', { detail: this.domain, bubbles: true, composed: true }));
}
private handleEditDnsRecord(record: IDnsRecordDetail) {
this.dispatchEvent(new CustomEvent('edit-dns-record', { detail: record, bubbles: true, composed: true }));
}
private handleDeleteDnsRecord(record: IDnsRecordDetail) {
this.dispatchEvent(new CustomEvent('delete-dns-record', { detail: record, bubbles: true, composed: true }));
}
}

View File

@@ -0,0 +1,329 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
declare global {
interface HTMLElementTagNameMap {
'sz-login-view': SzLoginView;
}
}
@customElement('sz-login-view')
export class SzLoginView extends DeesElement {
public static demo = () => html`
<div style="height: 600px; display: flex; align-items: center; justify-content: center; background: #09090b;">
<sz-login-view></sz-login-view>
</div>
`;
@property({ type: Boolean })
public accessor loading: boolean = false;
@property({ type: String })
public accessor error: string = '';
@property({ type: String })
public accessor title: string = 'serve.zone';
@property({ type: String })
public accessor subtitle: string = 'Sign in to your onebox';
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
width: 100%;
background: ${cssManager.bdTheme('#f4f4f5', '#09090b')};
}
.login-container {
width: 100%;
max-width: 400px;
padding: 24px;
}
.login-card {
background: ${cssManager.bdTheme('#ffffff', '#18181b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 12px;
padding: 32px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.logo-section {
text-align: center;
margin-bottom: 32px;
}
.logo {
width: 64px;
height: 64px;
background: ${cssManager.bdTheme('#18181b', '#fafafa')};
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 16px;
}
.logo svg {
width: 36px;
height: 36px;
color: ${cssManager.bdTheme('#fafafa', '#18181b')};
}
.title {
font-size: 24px;
font-weight: 700;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
margin-bottom: 4px;
}
.subtitle {
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.form {
display: flex;
flex-direction: column;
gap: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-label {
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.form-input {
width: 100%;
padding: 12px 14px;
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
font-size: 14px;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
outline: none;
transition: border-color 200ms ease, box-shadow 200ms ease;
box-sizing: border-box;
}
.form-input:focus {
border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(96, 165, 250, 0.1)')};
}
.form-input::placeholder {
color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
}
.form-input.error {
border-color: ${cssManager.bdTheme('#ef4444', '#f87171')};
}
.error-message {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 14px;
background: ${cssManager.bdTheme('#fef2f2', 'rgba(239, 68, 68, 0.1)')};
border: 1px solid ${cssManager.bdTheme('#fecaca', 'rgba(239, 68, 68, 0.2)')};
border-radius: 8px;
font-size: 14px;
color: ${cssManager.bdTheme('#dc2626', '#f87171')};
}
.error-message svg {
width: 18px;
height: 18px;
flex-shrink: 0;
}
.submit-button {
width: 100%;
padding: 12px 20px;
background: ${cssManager.bdTheme('#18181b', '#fafafa')};
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
color: ${cssManager.bdTheme('#fafafa', '#18181b')};
cursor: pointer;
transition: opacity 200ms ease, transform 200ms ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.submit-button:hover:not(:disabled) {
opacity: 0.9;
}
.submit-button:active:not(:disabled) {
transform: scale(0.98);
}
.submit-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.spinner {
width: 18px;
height: 18px;
border: 2px solid transparent;
border-top-color: currentColor;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.footer {
margin-top: 24px;
text-align: center;
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.footer a {
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
`,
];
public render(): TemplateResult {
return html`
<div class="login-container">
<div class="login-card">
<div class="logo-section">
<div class="logo">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
<line x1="12" y1="22.08" x2="12" y2="12"></line>
</svg>
</div>
<div class="title">${this.title}</div>
<div class="subtitle">${this.subtitle}</div>
</div>
<form class="form" @submit=${this.handleSubmit}>
${this.error ? html`
<div class="error-message">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
${this.error}
</div>
` : ''}
<div class="form-group">
<label class="form-label" for="username">Username</label>
<input
type="text"
id="username"
class="form-input ${this.error ? 'error' : ''}"
placeholder="Enter your username"
autocomplete="username"
?disabled=${this.loading}
required
>
</div>
<div class="form-group">
<label class="form-label" for="password">Password</label>
<input
type="password"
id="password"
class="form-input ${this.error ? 'error' : ''}"
placeholder="Enter your password"
autocomplete="current-password"
?disabled=${this.loading}
required
>
</div>
<button type="submit" class="submit-button" ?disabled=${this.loading}>
${this.loading ? html`
<div class="spinner"></div>
Signing in...
` : 'Sign in'}
</button>
</form>
<div class="footer">
Powered by <a href="https://serve.zone" target="_blank">serve.zone</a>
</div>
</div>
</div>
`;
}
private handleSubmit(e: Event) {
e.preventDefault();
const usernameInput = this.shadowRoot?.getElementById('username') as HTMLInputElement;
const passwordInput = this.shadowRoot?.getElementById('password') as HTMLInputElement;
if (!usernameInput || !passwordInput) return;
const username = usernameInput.value.trim();
const password = passwordInput.value;
if (!username || !password) {
this.error = 'Please enter both username and password';
return;
}
this.error = '';
this.dispatchEvent(new CustomEvent('login', {
detail: { username, password },
bubbles: true,
composed: true,
}));
}
public clearForm() {
const usernameInput = this.shadowRoot?.getElementById('username') as HTMLInputElement;
const passwordInput = this.shadowRoot?.getElementById('password') as HTMLInputElement;
if (usernameInput) usernameInput.value = '';
if (passwordInput) passwordInput.value = '';
this.error = '';
}
public focusUsername() {
const usernameInput = this.shadowRoot?.getElementById('username') as HTMLInputElement;
if (usernameInput) usernameInput.focus();
}
}

View File

@@ -0,0 +1,208 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
declare global {
interface HTMLElementTagNameMap {
'sz-network-dns-view': SzNetworkDnsView;
}
}
export interface IDnsRecord {
domain: string;
type: 'A' | 'AAAA' | 'CNAME' | 'MX' | 'TXT';
value: string;
id?: string;
}
@customElement('sz-network-dns-view')
export class SzNetworkDnsView extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 1000px;">
<sz-network-dns-view
.records=${[
{ domain: 'pr.task.vc', type: 'A', value: '195.201.98.232' },
{ domain: 'outline.task.vc', type: 'A', value: '195.201.98.232' },
{ domain: 'ns1.task.vc', type: 'A', value: '212.95.99.130' },
{ domain: 'bleu.de', type: 'A', value: '212.95.99.130' },
{ domain: 'mail.bleu.de', type: 'MX', value: '10 mail.bleu.de' },
]}
></sz-network-dns-view>
</div>
`;
@property({ type: Array })
public accessor records: IDnsRecord[] = [];
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.description {
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.sync-button {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 6px;
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
cursor: pointer;
transition: all 200ms ease;
}
.sync-button:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
}
.table-container {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
overflow: hidden;
}
.table-header {
display: grid;
grid-template-columns: 2fr 80px 2fr 100px;
gap: 16px;
padding: 12px 16px;
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.table-row {
display: grid;
grid-template-columns: 2fr 80px 2fr 100px;
gap: 16px;
padding: 12px 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#27272a')};
font-size: 14px;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
transition: background 200ms ease;
}
.table-row:last-child {
border-bottom: none;
}
.table-row:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
.domain {
font-weight: 500;
}
.type-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 2px 8px;
background: ${cssManager.bdTheme('#dbeafe', 'rgba(59, 130, 246, 0.2)')};
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
.value {
font-family: monospace;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.delete-button {
padding: 6px 12px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.3)')};
border-radius: 4px;
font-size: 13px;
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
cursor: pointer;
transition: all 200ms ease;
}
.delete-button:hover {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
}
.empty-state {
padding: 48px 24px;
text-align: center;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
`,
];
public render(): TemplateResult {
return html`
<div class="header">
<span class="description">Manage DNS records synced with Cloudflare</span>
<button class="sync-button" @click=${() => this.handleSync()}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12a9 9 0 0 1-9 9m9-9a9 9 0 0 0-9-9m9 9H3m9 9a9 9 0 0 1-9-9m9 9c1.66 0 3-4.03 3-9s-1.34-9-3-9m0 18c-1.66 0-3-4.03-3-9s1.34-9 3-9"/>
</svg>
Sync Cloudflare
</button>
</div>
<div class="table-container">
<div class="table-header">
<span>Domain</span>
<span>Type</span>
<span>Value</span>
<span>Actions</span>
</div>
${this.records.length > 0 ? this.records.map(record => html`
<div class="table-row">
<span class="domain">${record.domain}</span>
<span><span class="type-badge">${record.type}</span></span>
<span class="value">${record.value}</span>
<span>
<button class="delete-button" @click=${() => this.handleDelete(record)}>Delete</button>
</span>
</div>
`) : html`
<div class="empty-state">No DNS records found</div>
`}
</div>
`;
}
private handleSync() {
this.dispatchEvent(new CustomEvent('sync', { bubbles: true, composed: true }));
}
private handleDelete(record: IDnsRecord) {
this.dispatchEvent(new CustomEvent('delete', { detail: record, bubbles: true, composed: true }));
}
}

View File

@@ -0,0 +1,273 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
import './sz-stat-card.js';
declare global {
interface HTMLElementTagNameMap {
'sz-network-domains-view': SzNetworkDomainsView;
}
}
export interface IDomainRecord {
domain: string;
provider: string;
serviceCount: number;
certificateStatus: 'valid' | 'expiring' | 'expired' | 'pending';
expiresIn?: string;
}
@customElement('sz-network-domains-view')
export class SzNetworkDomainsView extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 1200px;">
<sz-network-domains-view
.domains=${[
{ domain: 'bleu.de', provider: 'cloudflare', serviceCount: 2, certificateStatus: 'valid', expiresIn: '52 days' },
{ domain: 'task.vc', provider: 'cloudflare', serviceCount: 1, certificateStatus: 'valid', expiresIn: '52 days' },
{ domain: 'example.com', provider: 'cloudflare', serviceCount: 0, certificateStatus: 'expiring', expiresIn: '7 days' },
]}
.stats=${{ total: 3, valid: 2, expiring: 1, expired: 0 }}
></sz-network-domains-view>
</div>
`;
@property({ type: Array })
public accessor domains: IDomainRecord[] = [];
@property({ type: Object })
public accessor stats: { total: number; valid: number; expiring: number; expired: number } = {
total: 0,
valid: 0,
expiring: 0,
expired: 0,
};
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.description {
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.sync-button {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 6px;
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
cursor: pointer;
transition: all 200ms ease;
}
.sync-button:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 24px;
}
@media (min-width: 768px) {
.stats-grid {
grid-template-columns: repeat(4, 1fr);
}
}
.table-container {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
overflow: hidden;
}
.table-header {
display: grid;
grid-template-columns: 2fr 1fr 80px 100px 100px 80px;
gap: 16px;
padding: 12px 16px;
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.table-row {
display: grid;
grid-template-columns: 2fr 1fr 80px 100px 100px 80px;
gap: 16px;
padding: 12px 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#27272a')};
font-size: 14px;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
align-items: center;
transition: background 200ms ease;
}
.table-row:last-child {
border-bottom: none;
}
.table-row:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
.domain-name {
font-weight: 500;
}
.provider {
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.status-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 9999px;
font-size: 12px;
font-weight: 500;
}
.status-badge.valid {
background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')};
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.status-badge.expiring {
background: ${cssManager.bdTheme('#fef9c3', 'rgba(250, 204, 21, 0.2)')};
color: ${cssManager.bdTheme('#ca8a04', '#facc15')};
}
.status-badge.expired,
.status-badge.pending {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.expires {
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.view-button {
padding: 6px 12px;
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 4px;
font-size: 13px;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
cursor: pointer;
transition: all 200ms ease;
}
.view-button:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
.empty-state {
padding: 48px 24px;
text-align: center;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
`,
];
public render(): TemplateResult {
return html`
<div class="header">
<span class="description">Manage domains and SSL certificates</span>
<button class="sync-button" @click=${() => this.handleSync()}>Sync Cloudflare</button>
</div>
<div class="stats-grid">
<sz-stat-card
label="Total Domains"
value="${this.stats.total}"
icon="server"
></sz-stat-card>
<sz-stat-card
label="Valid Certificates"
value="${this.stats.valid}"
icon="check"
variant="success"
></sz-stat-card>
<sz-stat-card
label="Expiring Soon"
value="${this.stats.expiring}"
icon="stop"
variant="${this.stats.expiring > 0 ? 'warning' : 'default'}"
></sz-stat-card>
<sz-stat-card
label="Expired/Pending"
value="${this.stats.expired}"
icon="stop"
variant="${this.stats.expired > 0 ? 'error' : 'default'}"
></sz-stat-card>
</div>
<div class="table-container">
<div class="table-header">
<span>Domain</span>
<span>Provider</span>
<span>Services</span>
<span>Certificate</span>
<span>Expires</span>
<span>Actions</span>
</div>
${this.domains.length > 0 ? this.domains.map(domain => html`
<div class="table-row">
<span class="domain-name">${domain.domain}</span>
<span class="provider">${domain.provider}</span>
<span>${domain.serviceCount}</span>
<span><span class="status-badge ${domain.certificateStatus}">${domain.certificateStatus}</span></span>
<span class="expires">${domain.expiresIn || '-'}</span>
<span>
<button class="view-button" @click=${() => this.handleView(domain)}>View</button>
</span>
</div>
`) : html`
<div class="empty-state">No domains configured</div>
`}
</div>
`;
}
private handleSync() {
this.dispatchEvent(new CustomEvent('sync', { bubbles: true, composed: true }));
}
private handleView(domain: IDomainRecord) {
this.dispatchEvent(new CustomEvent('view', { detail: domain, bubbles: true, composed: true }));
}
}

View File

@@ -0,0 +1,456 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
import './sz-stat-card.js';
declare global {
interface HTMLElementTagNameMap {
'sz-network-proxy-view': SzNetworkProxyView;
}
}
export interface ITrafficTarget {
type: 'service' | 'registry' | 'platform';
name: string;
domain: string | null;
target: string;
status: 'running' | 'stopped';
}
export interface IAccessLogEntry {
timestamp: string;
method: string;
path: string;
status: number;
duration: number;
ip: string;
}
@customElement('sz-network-proxy-view')
export class SzNetworkProxyView extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 1400px;">
<sz-network-proxy-view
proxyStatus="running"
routeCount="3"
certificateCount="2"
targetCount="11"
.targets=${[
{ type: 'service', name: 'test-nginx', domain: 'app.bleu.de', target: 'localhost:8080', status: 'running' },
{ type: 'service', name: 'hello-world', domain: 'hello.task.vc', target: 'localhost:8081', status: 'running' },
{ type: 'registry', name: 'onebox-registry', domain: null, target: 'localhost:4000', status: 'running' },
{ type: 'platform', name: 'MongoDB', domain: null, target: 'localhost:27017', status: 'running' },
{ type: 'platform', name: 'ClickHouse', domain: null, target: 'localhost:8123', status: 'running' },
]}
.logs=${[
{ timestamp: '2024-01-02 10:15:32', method: 'GET', path: '/api/services', status: 200, duration: 45, ip: '192.168.1.100' },
{ timestamp: '2024-01-02 10:15:30', method: 'POST', path: '/api/auth/login', status: 200, duration: 120, ip: '192.168.1.101' },
{ timestamp: '2024-01-02 10:15:28', method: 'GET', path: '/static/bundle.js', status: 304, duration: 5, ip: '192.168.1.100' },
]}
></sz-network-proxy-view>
</div>
`;
@property({ type: String })
public accessor proxyStatus: 'running' | 'stopped' = 'stopped';
@property({ type: String })
public accessor routeCount: string = '0';
@property({ type: String })
public accessor certificateCount: string = '0';
@property({ type: String })
public accessor targetCount: string = '0';
@property({ type: Array })
public accessor targets: ITrafficTarget[] = [];
@property({ type: Array })
public accessor logs: IAccessLogEntry[] = [];
@property({ type: Boolean })
public accessor streaming: boolean = false;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
}
.actions {
display: flex;
justify-content: flex-end;
margin-bottom: 16px;
}
.refresh-button {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 6px;
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
cursor: pointer;
transition: all 200ms ease;
}
.refresh-button:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 24px;
}
@media (min-width: 768px) {
.stats-grid {
grid-template-columns: repeat(4, 1fr);
}
}
.section {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
margin-bottom: 24px;
overflow: hidden;
}
.section-header {
padding: 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
}
.section-title {
font-size: 16px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.section-subtitle {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-top: 2px;
}
.table-header {
display: grid;
grid-template-columns: 80px 1.5fr 1.5fr 1.5fr 80px;
gap: 16px;
padding: 12px 16px;
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.table-row {
display: grid;
grid-template-columns: 80px 1.5fr 1.5fr 1.5fr 80px;
gap: 16px;
padding: 12px 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#27272a')};
font-size: 14px;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
cursor: pointer;
transition: background 200ms ease;
}
.table-row:last-child {
border-bottom: none;
}
.table-row:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
.type-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
.type-badge.service {
background: ${cssManager.bdTheme('#dbeafe', 'rgba(59, 130, 246, 0.2)')};
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
}
.type-badge.registry {
background: ${cssManager.bdTheme('#f3e8ff', 'rgba(168, 85, 247, 0.2)')};
color: ${cssManager.bdTheme('#9333ea', '#a855f7')};
}
.type-badge.platform {
background: ${cssManager.bdTheme('#fef3c7', 'rgba(245, 158, 11, 0.2)')};
color: ${cssManager.bdTheme('#d97706', '#f59e0b')};
}
.target-value {
font-family: monospace;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.status-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 9999px;
font-size: 12px;
font-weight: 500;
}
.status-badge.running {
background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')};
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.status-badge.stopped {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.logs-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
}
.logs-actions {
display: flex;
gap: 8px;
}
.stream-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: ${cssManager.bdTheme('#2563eb', '#3b82f6')};
border: none;
border-radius: 4px;
font-size: 13px;
font-weight: 500;
color: white;
cursor: pointer;
transition: all 200ms ease;
}
.stream-button:hover {
background: ${cssManager.bdTheme('#1d4ed8', '#2563eb')};
}
.stream-button.streaming {
background: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.stream-button.streaming:hover {
background: ${cssManager.bdTheme('#b91c1c', '#dc2626')};
}
.clear-button {
padding: 6px 12px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 4px;
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
cursor: pointer;
transition: all 200ms ease;
}
.clear-button:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
.logs-container {
padding: 16px;
font-family: monospace;
font-size: 13px;
max-height: 300px;
overflow-y: auto;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
}
.log-entry {
padding: 4px 0;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.log-timestamp {
color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
}
.log-method {
font-weight: 600;
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
}
.log-status-2xx {
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.log-status-3xx {
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
}
.log-status-4xx {
color: ${cssManager.bdTheme('#ca8a04', '#facc15')};
}
.log-status-5xx {
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.empty-logs {
padding: 24px;
text-align: center;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
`,
];
public render(): TemplateResult {
return html`
<div class="actions">
<button class="refresh-button" @click=${() => this.handleRefresh()}>Refresh</button>
</div>
<div class="stats-grid">
<sz-stat-card
label="Proxy Status"
value="${this.proxyStatus === 'running' ? 'Running' : 'Stopped'}"
icon="server"
variant="${this.proxyStatus === 'running' ? 'success' : 'error'}"
valueBadge
></sz-stat-card>
<sz-stat-card
label="Routes"
value="${this.routeCount}"
icon="server"
></sz-stat-card>
<sz-stat-card
label="Certificates"
value="${this.certificateCount}"
icon="check"
></sz-stat-card>
<sz-stat-card
label="Targets"
value="${this.targetCount}"
icon="server"
></sz-stat-card>
</div>
<div class="section">
<div class="section-header">
<div class="section-title">Traffic Targets</div>
<div class="section-subtitle">Services, registry, and platform services with their routing info</div>
</div>
<div class="table-header">
<span>Type</span>
<span>Name</span>
<span>Domain</span>
<span>Target</span>
<span>Status</span>
</div>
${this.targets.map(target => html`
<div class="table-row" @click=${() => this.handleTargetClick(target)}>
<span><span class="type-badge ${target.type}">${target.type}</span></span>
<span>${target.name}</span>
<span>${target.domain || '-'}</span>
<span class="target-value">${target.target}</span>
<span><span class="status-badge ${target.status}">${target.status}</span></span>
</div>
`)}
</div>
<div class="section">
<div class="logs-header">
<div>
<div class="section-title">Access Logs</div>
<div class="section-subtitle">Real-time Caddy access logs</div>
</div>
<div class="logs-actions">
<button class="stream-button ${this.streaming ? 'streaming' : ''}" @click=${() => this.toggleStreaming()}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
${this.streaming
? html`<rect x="6" y="6" width="12" height="12" rx="1"/>`
: html`<polygon points="5,3 19,12 5,21"/>`
}
</svg>
${this.streaming ? 'Stop' : 'Stream'}
</button>
<button class="clear-button" @click=${() => this.handleClearLogs()}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3,6 5,6 21,6"/><path d="M19,6v14a2,2,0,0,1-2,2H7a2,2,0,0,1-2-2V6m3,0V4a2,2,0,0,1,2-2h4a2,2,0,0,1,2,2v2"/>
</svg>
Clear logs
</button>
</div>
</div>
<div class="logs-container">
${this.logs.length > 0 ? this.logs.map(log => html`
<div class="log-entry">
<span class="log-timestamp">${log.timestamp}</span>
<span class="log-method">${log.method}</span>
${log.path}
<span class="${this.getStatusClass(log.status)}">${log.status}</span>
${log.duration}ms
${log.ip}
</div>
`) : html`
<div class="empty-logs">Click "Stream" to start live access log streaming</div>
`}
</div>
</div>
`;
}
private getStatusClass(status: number): string {
if (status >= 500) return 'log-status-5xx';
if (status >= 400) return 'log-status-4xx';
if (status >= 300) return 'log-status-3xx';
return 'log-status-2xx';
}
private handleRefresh() {
this.dispatchEvent(new CustomEvent('refresh', { bubbles: true, composed: true }));
}
private handleTargetClick(target: ITrafficTarget) {
this.dispatchEvent(new CustomEvent('target-click', { detail: target, bubbles: true, composed: true }));
}
private toggleStreaming() {
this.streaming = !this.streaming;
this.dispatchEvent(new CustomEvent('stream-toggle', { detail: { streaming: this.streaming }, bubbles: true, composed: true }));
}
private handleClearLogs() {
this.dispatchEvent(new CustomEvent('clear-logs', { bubbles: true, composed: true }));
}
}

View File

@@ -0,0 +1,714 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
declare global {
interface HTMLElementTagNameMap {
'sz-platform-service-detail-view': SzPlatformServiceDetailView;
}
}
export interface IPlatformServiceDetail {
id: string;
name: string;
type: 'mongodb' | 'minio' | 'clickhouse' | 'redis';
status: 'running' | 'stopped' | 'error';
version: string;
host: string;
port: number;
credentials?: {
username?: string;
password?: string;
accessKey?: string;
secretKey?: string;
};
config: Record<string, any>;
metrics?: {
cpu: number;
memory: number;
storage: number;
connections?: number;
};
}
export interface IPlatformLogEntry {
timestamp: string;
level: 'info' | 'warn' | 'error' | 'debug';
message: string;
}
@customElement('sz-platform-service-detail-view')
export class SzPlatformServiceDetailView extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 1000px;">
<sz-platform-service-detail-view
.service=${{
id: '1',
name: 'MongoDB',
type: 'mongodb',
status: 'running',
version: '7.0.4',
host: 'localhost',
port: 27017,
credentials: { username: 'admin', password: '••••••••' },
config: { replicaSet: 'rs0', authEnabled: true },
metrics: { cpu: 12, memory: 45, storage: 23, connections: 8 },
}}
.logs=${[
{ timestamp: '2024-01-20 14:30:22', level: 'info', message: 'Connection accepted from 127.0.0.1:54321' },
{ timestamp: '2024-01-20 14:30:20', level: 'info', message: 'Index build completed on collection users' },
{ timestamp: '2024-01-20 14:30:15', level: 'warn', message: 'Slow query detected: 1.2s on collection orders' },
{ timestamp: '2024-01-20 14:30:10', level: 'info', message: 'Checkpoint complete' },
]}
></sz-platform-service-detail-view>
</div>
`;
@property({ type: Object })
public accessor service: IPlatformServiceDetail | null = null;
@property({ type: Array })
public accessor logs: IPlatformLogEntry[] = [];
@property({ type: Boolean })
public accessor actionLoading: boolean = false;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
}
.header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24px;
}
.header-info {
display: flex;
align-items: center;
gap: 16px;
}
.service-icon {
width: 56px;
height: 56px;
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.service-icon svg {
width: 28px;
height: 28px;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.service-details {
display: flex;
flex-direction: column;
gap: 4px;
}
.service-name {
font-size: 22px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.service-meta {
display: flex;
align-items: center;
gap: 12px;
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 9999px;
font-size: 13px;
font-weight: 500;
}
.status-badge.running {
background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')};
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.status-badge.stopped {
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.status-badge.error {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
}
.header-actions {
display: flex;
gap: 8px;
}
.action-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 6px;
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
cursor: pointer;
transition: all 200ms ease;
}
.action-button:hover:not(:disabled) {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
.action-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.action-button svg {
width: 14px;
height: 14px;
}
.action-button.danger {
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
border-color: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.3)')};
}
.action-button.danger:hover:not(:disabled) {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
@media (max-width: 768px) {
.grid {
grid-template-columns: 1fr;
}
}
.section {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
overflow: hidden;
}
.section.full-width {
grid-column: 1 / -1;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
.section-title {
font-size: 14px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
display: flex;
align-items: center;
gap: 8px;
}
.section-title svg {
width: 16px;
height: 16px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.section-content {
padding: 16px;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#27272a')};
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.info-value {
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
font-family: monospace;
display: flex;
align-items: center;
gap: 8px;
}
.copy-button {
padding: 4px;
background: transparent;
border: none;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
cursor: pointer;
border-radius: 4px;
transition: all 200ms ease;
}
.copy-button:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
@media (max-width: 600px) {
.metrics-grid {
grid-template-columns: repeat(2, 1fr);
}
}
.metric-card {
text-align: center;
padding: 12px;
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
border-radius: 6px;
}
.metric-value {
font-size: 20px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.metric-label {
font-size: 12px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-top: 2px;
}
.progress-bar {
height: 4px;
background: ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 2px;
margin-top: 8px;
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 2px;
transition: width 300ms ease;
}
.progress-fill.low {
background: ${cssManager.bdTheme('#22c55e', '#22c55e')};
}
.progress-fill.medium {
background: ${cssManager.bdTheme('#eab308', '#eab308')};
}
.progress-fill.high {
background: ${cssManager.bdTheme('#ef4444', '#ef4444')};
}
.log-container {
background: ${cssManager.bdTheme('#18181b', '#09090b')};
border-radius: 6px;
padding: 12px;
max-height: 300px;
overflow-y: auto;
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
font-size: 12px;
line-height: 1.6;
}
.log-entry {
display: flex;
gap: 12px;
padding: 4px 0;
}
.log-timestamp {
color: #71717a;
flex-shrink: 0;
}
.log-level {
flex-shrink: 0;
width: 50px;
text-transform: uppercase;
font-weight: 500;
}
.log-level.info {
color: #60a5fa;
}
.log-level.warn {
color: #fbbf24;
}
.log-level.error {
color: #f87171;
}
.log-level.debug {
color: #a1a1aa;
}
.log-message {
color: #fafafa;
word-break: break-word;
}
.config-item {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#27272a')};
}
.config-item:last-child {
border-bottom: none;
}
.config-key {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.config-value {
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.config-value.true {
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.config-value.false {
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
`,
];
public render(): TemplateResult {
if (!this.service) {
return html`<div class="empty-state">No service selected</div>`;
}
return html`
<div class="header">
<div class="header-info">
<div class="service-icon">
${this.renderServiceIcon()}
</div>
<div class="service-details">
<div class="service-name">${this.service.name}</div>
<div class="service-meta">
<span class="status-badge ${this.service.status}">
<span class="status-dot"></span>
${this.service.status.charAt(0).toUpperCase() + this.service.status.slice(1)}
</span>
<span>Version ${this.service.version}</span>
</div>
</div>
</div>
<div class="header-actions">
${this.service.status === 'running' ? html`
<button class="action-button" ?disabled=${this.actionLoading} @click=${() => this.handleRestart()}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 4 23 10 17 10"></polyline>
<polyline points="1 20 1 14 7 14"></polyline>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
</svg>
Restart
</button>
<button class="action-button danger" ?disabled=${this.actionLoading} @click=${() => this.handleStop()}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="6" y="6" width="12" height="12" rx="1"></rect>
</svg>
Stop
</button>
` : html`
<button class="action-button" ?disabled=${this.actionLoading} @click=${() => this.handleStart()}>
<svg viewBox="0 0 24 24" fill="currentColor">
<polygon points="5,3 19,12 5,21"></polygon>
</svg>
Start
</button>
`}
</div>
</div>
<div class="grid">
<!-- Connection Info -->
<div class="section">
<div class="section-header">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
Connection
</div>
</div>
<div class="section-content">
<div class="info-row">
<span class="info-label">Host</span>
<span class="info-value">
${this.service.host}
<button class="copy-button" @click=${() => this.copyToClipboard(this.service!.host)}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</button>
</span>
</div>
<div class="info-row">
<span class="info-label">Port</span>
<span class="info-value">${this.service.port}</span>
</div>
${this.service.credentials?.username ? html`
<div class="info-row">
<span class="info-label">Username</span>
<span class="info-value">
${this.service.credentials.username}
<button class="copy-button" @click=${() => this.copyToClipboard(this.service!.credentials!.username!)}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</button>
</span>
</div>
<div class="info-row">
<span class="info-label">Password</span>
<span class="info-value">••••••••</span>
</div>
` : ''}
${this.service.credentials?.accessKey ? html`
<div class="info-row">
<span class="info-label">Access Key</span>
<span class="info-value">
${this.service.credentials.accessKey}
<button class="copy-button" @click=${() => this.copyToClipboard(this.service!.credentials!.accessKey!)}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</button>
</span>
</div>
<div class="info-row">
<span class="info-label">Secret Key</span>
<span class="info-value">••••••••</span>
</div>
` : ''}
</div>
</div>
<!-- Configuration -->
<div class="section">
<div class="section-header">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
Configuration
</div>
</div>
<div class="section-content">
${Object.entries(this.service.config).map(([key, value]) => html`
<div class="config-item">
<span class="config-key">${this.formatConfigKey(key)}</span>
<span class="config-value ${typeof value === 'boolean' ? (value ? 'true' : 'false') : ''}">${this.formatConfigValue(value)}</span>
</div>
`)}
</div>
</div>
<!-- Metrics -->
${this.service.metrics ? html`
<div class="section full-width">
<div class="section-header">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="20" x2="18" y2="10"></line>
<line x1="12" y1="20" x2="12" y2="4"></line>
<line x1="6" y1="20" x2="6" y2="14"></line>
</svg>
Resource Usage
</div>
</div>
<div class="section-content">
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-value">${this.service.metrics.cpu}%</div>
<div class="metric-label">CPU</div>
<div class="progress-bar">
<div class="progress-fill ${this.getProgressClass(this.service.metrics.cpu)}" style="width: ${this.service.metrics.cpu}%"></div>
</div>
</div>
<div class="metric-card">
<div class="metric-value">${this.service.metrics.memory}%</div>
<div class="metric-label">Memory</div>
<div class="progress-bar">
<div class="progress-fill ${this.getProgressClass(this.service.metrics.memory)}" style="width: ${this.service.metrics.memory}%"></div>
</div>
</div>
<div class="metric-card">
<div class="metric-value">${this.service.metrics.storage}%</div>
<div class="metric-label">Storage</div>
<div class="progress-bar">
<div class="progress-fill ${this.getProgressClass(this.service.metrics.storage)}" style="width: ${this.service.metrics.storage}%"></div>
</div>
</div>
${this.service.metrics.connections !== undefined ? html`
<div class="metric-card">
<div class="metric-value">${this.service.metrics.connections}</div>
<div class="metric-label">Connections</div>
</div>
` : ''}
</div>
</div>
</div>
` : ''}
<!-- Logs -->
<div class="section full-width">
<div class="section-header">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="4 17 10 11 4 5"></polyline>
<line x1="12" y1="19" x2="20" y2="19"></line>
</svg>
Logs
</div>
</div>
<div class="section-content">
<div class="log-container">
${this.logs.length > 0 ? this.logs.map(log => html`
<div class="log-entry">
<span class="log-timestamp">${log.timestamp}</span>
<span class="log-level ${log.level}">${log.level}</span>
<span class="log-message">${log.message}</span>
</div>
`) : html`
<div style="color: #71717a; text-align: center; padding: 20px;">No logs available</div>
`}
</div>
</div>
</div>
</div>
`;
}
private renderServiceIcon(): TemplateResult {
const type = this.service?.type;
switch (type) {
case 'mongodb':
return html`<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>`;
case 'minio':
return html`<svg viewBox="0 0 24 24" fill="currentColor"><path d="M21 16.5c0 .38-.21.71-.53.88l-7.9 4.44c-.16.12-.36.18-.57.18-.21 0-.41-.06-.57-.18l-7.9-4.44A.991.991 0 0 1 3 16.5v-9c0-.38.21-.71.53-.88l7.9-4.44c.16-.12.36-.18.57-.18.21 0 .41.06.57.18l7.9 4.44c.32.17.53.5.53.88v9z"/></svg>`;
case 'clickhouse':
return html`<svg viewBox="0 0 24 24" fill="currentColor"><rect x="2" y="2" width="6" height="20"/><rect x="9" y="7" width="6" height="15"/><rect x="16" y="12" width="6" height="10"/></svg>`;
case 'redis':
return html`<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>`;
default:
return html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg>`;
}
}
private getProgressClass(value: number): string {
if (value < 50) return 'low';
if (value < 80) return 'medium';
return 'high';
}
private formatConfigKey(key: string): string {
return key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
}
private formatConfigValue(value: any): string {
if (typeof value === 'boolean') return value ? 'Enabled' : 'Disabled';
return String(value);
}
private copyToClipboard(text: string) {
navigator.clipboard.writeText(text);
this.dispatchEvent(new CustomEvent('copy', { detail: text, bubbles: true, composed: true }));
}
private handleStart() {
this.dispatchEvent(new CustomEvent('start', { detail: this.service, bubbles: true, composed: true }));
}
private handleStop() {
this.dispatchEvent(new CustomEvent('stop', { detail: this.service, bubbles: true, composed: true }));
}
private handleRestart() {
this.dispatchEvent(new CustomEvent('restart', { detail: this.service, bubbles: true, composed: true }));
}
}

View File

@@ -0,0 +1,163 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
declare global {
interface HTMLElementTagNameMap {
'sz-platform-services-card': SzPlatformServicesCard;
}
}
export interface IPlatformService {
name: string;
status: string;
running: boolean;
url?: string;
}
@customElement('sz-platform-services-card')
export class SzPlatformServicesCard extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 500px;">
<sz-platform-services-card
.services=${[
{ name: 'MongoDB', status: '1 DB', running: true },
{ name: 'S3 Storage (MinIO)', status: '1 bucket', running: true },
{ name: 'Caddy Reverse Proxy', status: 'Running', running: true },
{ name: 'ClickHouse', status: '1 DB', running: true },
]}
></sz-platform-services-card>
</div>
`;
@property({ type: Array })
public accessor services: IPlatformService[] = [];
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
height: 100%;
}
.card {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
padding: 20px;
height: 100%;
box-sizing: border-box;
}
.header {
margin-bottom: 16px;
}
.title {
font-size: 16px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.subtitle {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-top: 2px;
}
.services-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.service-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
cursor: pointer;
transition: opacity 200ms ease;
}
.service-item:hover {
opacity: 0.8;
}
.service-left {
display: flex;
align-items: center;
gap: 10px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.status-dot.running {
background: ${cssManager.bdTheme('#22c55e', '#22c55e')};
box-shadow: 0 0 6px ${cssManager.bdTheme('rgba(34, 197, 94, 0.4)', 'rgba(34, 197, 94, 0.4)')};
}
.status-dot.stopped {
background: ${cssManager.bdTheme('#ef4444', '#ef4444')};
}
.service-name {
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.service-status {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
`,
];
public render(): TemplateResult {
return html`
<div class="card">
<div class="header">
<div class="title">Platform Services</div>
<div class="subtitle">Infrastructure status</div>
</div>
<div class="services-list">
${this.services.map(
(service) => html`
<div class="service-item" @click=${() => this.handleServiceClick(service)}>
<div class="service-left">
<div class="status-dot ${service.running ? 'running' : 'stopped'}"></div>
<span class="service-name">${service.name}</span>
</div>
<span class="service-status">${service.status}</span>
</div>
`
)}
</div>
</div>
`;
}
private handleServiceClick(service: IPlatformService) {
this.dispatchEvent(
new CustomEvent('service-click', {
detail: service,
bubbles: true,
composed: true,
})
);
}
}

View File

@@ -0,0 +1,161 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
declare global {
interface HTMLElementTagNameMap {
'sz-quick-actions-card': SzQuickActionsCard;
}
}
export interface IQuickAction {
label: string;
icon?: string;
primary?: boolean;
url?: string;
}
@customElement('sz-quick-actions-card')
export class SzQuickActionsCard extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 800px;">
<sz-quick-actions-card
.actions=${[
{ label: 'Deploy Service', icon: 'plus', primary: true },
{ label: 'View All Services' },
{ label: 'Platform Services' },
{ label: 'Manage Domains' },
]}
></sz-quick-actions-card>
</div>
`;
@property({ type: Array })
public accessor actions: IQuickAction[] = [];
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
height: 100%;
}
.card {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
padding: 20px;
height: 100%;
box-sizing: border-box;
}
.header {
margin-bottom: 16px;
}
.title {
font-size: 16px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.subtitle {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-top: 2px;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.action-button {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 200ms ease;
border: none;
outline: none;
}
.action-button.primary {
background: ${cssManager.bdTheme('#2563eb', '#3b82f6')};
color: white;
}
.action-button.primary:hover {
background: ${cssManager.bdTheme('#1d4ed8', '#2563eb')};
}
.action-button.secondary {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
}
.action-button.secondary:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
}
.action-icon {
width: 16px;
height: 16px;
}
`,
];
public render(): TemplateResult {
return html`
<div class="card">
<div class="header">
<div class="title">Quick Actions</div>
<div class="subtitle">Common tasks and shortcuts</div>
</div>
<div class="actions">
${this.actions.map(
(action) => html`
<button
class="action-button ${action.primary ? 'primary' : 'secondary'}"
@click=${() => this.handleActionClick(action)}
>
${action.icon === 'plus' ? html`
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
` : ''}
${action.label}
</button>
`
)}
</div>
</div>
`;
}
private handleActionClick(action: IQuickAction) {
this.dispatchEvent(
new CustomEvent('action-click', {
detail: action,
bubbles: true,
composed: true,
})
);
}
}

View File

@@ -0,0 +1,279 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
declare global {
interface HTMLElementTagNameMap {
'sz-registry-external-view': SzRegistryExternalView;
}
}
export interface IExternalRegistry {
id: string;
name: string;
url: string;
username: string;
type: 'dockerhub' | 'ghcr' | 'gcr' | 'ecr' | 'custom';
}
@customElement('sz-registry-external-view')
export class SzRegistryExternalView extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 800px;">
<sz-registry-external-view
.registries=${[
{ id: '1', name: 'Docker Hub', url: 'docker.io', username: 'myuser', type: 'dockerhub' },
{ id: '2', name: 'GitHub Container Registry', url: 'ghcr.io', username: 'myorg', type: 'ghcr' },
]}
></sz-registry-external-view>
</div>
`;
@property({ type: Array })
public accessor registries: IExternalRegistry[] = [];
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
}
.header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24px;
}
.header-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.header-title {
font-size: 18px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.header-subtitle {
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.add-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: ${cssManager.bdTheme('#18181b', '#fafafa')};
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('#fafafa', '#18181b')};
cursor: pointer;
transition: all 200ms ease;
}
.add-button:hover {
opacity: 0.9;
}
.registry-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.registry-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
}
.registry-info {
display: flex;
align-items: center;
gap: 12px;
}
.registry-icon {
width: 40px;
height: 40px;
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.registry-details {
display: flex;
flex-direction: column;
gap: 2px;
}
.registry-name {
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.registry-url {
font-size: 13px;
font-family: monospace;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.registry-actions {
display: flex;
gap: 8px;
}
.action-button {
padding: 6px 12px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 4px;
font-size: 12px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
cursor: pointer;
transition: all 200ms ease;
}
.action-button:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.action-button.delete {
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
border-color: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.3)')};
}
.action-button.delete:hover {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
}
.empty-state {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
padding: 48px 24px;
text-align: center;
}
.empty-icon {
width: 48px;
height: 48px;
margin: 0 auto 16px;
color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
}
.empty-title {
font-size: 16px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
margin-bottom: 8px;
}
.empty-description {
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-bottom: 16px;
}
.empty-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 6px;
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
cursor: pointer;
transition: all 200ms ease;
}
.empty-button:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
`,
];
public render(): TemplateResult {
return html`
<div class="header">
<div class="header-info">
<div class="header-title">External Registries</div>
<div class="header-subtitle">Add credentials for private Docker registries</div>
</div>
<button class="add-button" @click=${() => this.handleAdd()}>Add Registry</button>
</div>
${this.registries.length > 0 ? html`
<div class="registry-list">
${this.registries.map(registry => html`
<div class="registry-item">
<div class="registry-info">
<div class="registry-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
</svg>
</div>
<div class="registry-details">
<div class="registry-name">${registry.name}</div>
<div class="registry-url">${registry.url} (${registry.username})</div>
</div>
</div>
<div class="registry-actions">
<button class="action-button" @click=${() => this.handleEdit(registry)}>Edit</button>
<button class="action-button delete" @click=${() => this.handleDelete(registry)}>Delete</button>
</div>
</div>
`)}
</div>
` : html`
<div class="empty-state">
<svg class="empty-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
<line x1="12" y1="22.08" x2="12" y2="12"></line>
</svg>
<div class="empty-title">No external registries</div>
<div class="empty-description">Add credentials for Docker Hub, GitHub Container Registry, or other private registries.</div>
<button class="empty-button" @click=${() => this.handleAdd()}>Add External Registry</button>
</div>
`}
`;
}
private handleAdd() {
this.dispatchEvent(new CustomEvent('add', { bubbles: true, composed: true }));
}
private handleEdit(registry: IExternalRegistry) {
this.dispatchEvent(new CustomEvent('edit', { detail: registry, bubbles: true, composed: true }));
}
private handleDelete(registry: IExternalRegistry) {
this.dispatchEvent(new CustomEvent('delete', { detail: registry, bubbles: true, composed: true }));
}
}

View File

@@ -0,0 +1,258 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
declare global {
interface HTMLElementTagNameMap {
'sz-registry-onebox-view': SzRegistryOneboxView;
}
}
@customElement('sz-registry-onebox-view')
export class SzRegistryOneboxView extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 800px;">
<sz-registry-onebox-view
status="running"
registryUrl="localhost:3000/v2"
></sz-registry-onebox-view>
</div>
`;
@property({ type: String })
public accessor status: 'running' | 'stopped' = 'stopped';
@property({ type: String })
public accessor registryUrl: string = '';
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
}
.card {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
overflow: hidden;
}
.card-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
}
.registry-icon {
width: 40px;
height: 40px;
background: ${cssManager.bdTheme('#dbeafe', 'rgba(59, 130, 246, 0.2)')};
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
}
.header-info {
flex: 1;
}
.header-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.default-badge {
padding: 2px 8px;
background: ${cssManager.bdTheme('#dbeafe', 'rgba(59, 130, 246, 0.2)')};
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
border-radius: 4px;
font-size: 11px;
font-weight: 600;
}
.header-subtitle {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-top: 2px;
}
.card-content {
padding: 16px;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.info-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.info-label {
font-size: 12px;
font-weight: 500;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
text-transform: uppercase;
letter-spacing: 0.05em;
}
.info-value {
font-size: 14px;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.status-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 9999px;
font-size: 12px;
font-weight: 500;
}
.status-badge.running {
background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')};
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.status-badge.stopped {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.manage-link {
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
text-decoration: none;
cursor: pointer;
}
.manage-link:hover {
text-decoration: underline;
}
.quick-start {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
border-radius: 6px;
padding: 16px;
}
.quick-start-title {
font-size: 14px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
margin-bottom: 8px;
}
.quick-start-desc {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-bottom: 12px;
}
.code-block {
background: ${cssManager.bdTheme('#18181b', '#0a0a0a')};
border-radius: 6px;
padding: 12px;
font-family: monospace;
font-size: 13px;
overflow-x: auto;
}
.code-line {
color: ${cssManager.bdTheme('#a1a1aa', '#71717a')};
margin-bottom: 4px;
}
.code-line:last-child {
margin-bottom: 0;
}
.code-comment {
color: ${cssManager.bdTheme('#6b7280', '#52525b')};
}
.code-command {
color: ${cssManager.bdTheme('#fafafa', '#e4e4e7')};
}
`,
];
public render(): TemplateResult {
return html`
<div class="card">
<div class="card-header">
<div class="registry-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
</svg>
</div>
<div class="header-info">
<div class="header-title">
Onebox Registry (Built-in)
<span class="default-badge">Default</span>
</div>
<div class="header-subtitle">Built-in container registry for your services</div>
</div>
</div>
<div class="card-content">
<div class="info-grid">
<div class="info-item">
<span class="info-label">Status</span>
<span class="info-value">
<span class="status-badge ${this.status}">${this.status === 'running' ? 'Running' : 'Stopped'}</span>
</span>
</div>
<div class="info-item">
<span class="info-label">Registry URL</span>
<span class="info-value">${this.registryUrl}</span>
</div>
<div class="info-item">
<span class="info-label">Authentication</span>
<span class="info-value">
<a class="manage-link" @click=${() => this.handleManageTokens()}>Manage Tokens</a>
</span>
</div>
</div>
<div class="quick-start">
<div class="quick-start-title">Quick Start</div>
<div class="quick-start-desc">To push images to the Onebox registry, use a CI or Global token:</div>
<div class="code-block">
<div class="code-line code-comment"># Login to the registry</div>
<div class="code-line code-command">docker login ${this.registryUrl.split('/')[0]} -u onebox -p YOUR_TOKEN</div>
<div class="code-line code-comment"># Tag and push your image</div>
<div class="code-line code-command">docker tag myapp ${this.registryUrl.split('/')[0]}/myservice:latest</div>
<div class="code-line code-command">docker push ${this.registryUrl.split('/')[0]}/myservice:latest</div>
</div>
</div>
</div>
</div>
`;
}
private handleManageTokens() {
this.dispatchEvent(new CustomEvent('manage-tokens', { bubbles: true, composed: true }));
}
}

View File

@@ -0,0 +1,284 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
declare global {
interface HTMLElementTagNameMap {
'sz-resource-usage-card': SzResourceUsageCard;
}
}
export interface IResourceUsage {
cpu: number;
memoryUsed: string;
memoryTotal: string;
networkIn: string;
networkOut: string;
topConsumers: Array<{ name: string; memory: string }>;
}
@customElement('sz-resource-usage-card')
export class SzResourceUsageCard extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 800px;">
<sz-resource-usage-card
.data=${{
cpu: 0.5,
memoryUsed: '191 MB',
memoryTotal: '429.2 GB',
networkIn: '38.9 KB/s',
networkOut: '1.7 KB/s',
topConsumers: [
{ name: 'test-nginx', memory: '32.1 MB' },
{ name: 'test-v2', memory: '31.7 MB' },
],
}}
serviceCount="7"
></sz-resource-usage-card>
</div>
`;
@property({ type: Object })
public accessor data: IResourceUsage = {
cpu: 0,
memoryUsed: '0 MB',
memoryTotal: '0 GB',
networkIn: '0 KB/s',
networkOut: '0 KB/s',
topConsumers: [],
};
@property({ type: String })
public accessor serviceCount: string = '0';
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
height: 100%;
}
.card {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
padding: 20px;
height: 100%;
box-sizing: border-box;
}
.header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.title {
font-size: 16px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.subtitle {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-top: 2px;
}
.view-all {
font-size: 13px;
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
text-decoration: none;
cursor: pointer;
}
.view-all:hover {
text-decoration: underline;
}
.metrics {
display: flex;
flex-direction: column;
gap: 16px;
}
.metric-row {
display: flex;
flex-direction: column;
gap: 6px;
}
.metric-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.metric-label {
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.metric-value {
font-size: 14px;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.progress-bar {
height: 6px;
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
border-radius: 3px;
transition: width 300ms ease;
}
.network-row {
display: flex;
gap: 16px;
align-items: center;
}
.network-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 14px;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.network-icon {
width: 14px;
height: 14px;
}
.network-icon.down {
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.network-icon.up {
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
}
.top-consumers {
margin-top: 8px;
padding-top: 12px;
border-top: 1px solid ${cssManager.bdTheme('#f4f4f5', '#27272a')};
}
.consumers-label {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-bottom: 6px;
}
.consumers-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.consumer-item {
font-size: 13px;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.consumer-name {
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
`,
];
public render(): TemplateResult {
return html`
<div class="card">
<div class="header">
<div>
<div class="title">Resource Usage</div>
<div class="subtitle">Aggregated across ${this.serviceCount} services</div>
</div>
<a class="view-all">View All</a>
</div>
<div class="metrics">
<div class="metric-row">
<div class="metric-header">
<span class="metric-label">CPU</span>
<span class="metric-value">${this.data.cpu.toFixed(1)}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: ${Math.min(this.data.cpu, 100)}%"></div>
</div>
</div>
<div class="metric-row">
<div class="metric-header">
<span class="metric-label">Memory</span>
<span class="metric-value">${this.data.memoryUsed} / ${this.data.memoryTotal}</span>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: ${this.calculateMemoryPercent()}%"></div>
</div>
</div>
<div class="metric-row">
<div class="metric-header">
<span class="metric-label">Network</span>
<div class="network-row">
<span class="network-item">
<svg class="network-icon down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 5v14M19 12l-7 7-7-7"/>
</svg>
${this.data.networkIn}
</span>
<span class="network-item">
<svg class="network-icon up" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 19V5M5 12l7-7 7 7"/>
</svg>
${this.data.networkOut}
</span>
</div>
</div>
</div>
</div>
${this.data.topConsumers.length > 0 ? html`
<div class="top-consumers">
<div class="consumers-label">Top consumers</div>
<div class="consumers-list">
${this.data.topConsumers.map(
(consumer) => html`
<span class="consumer-item">
<span class="consumer-name">${consumer.name}:</span> ${consumer.memory}
</span>
`
)}
</div>
</div>
` : ''}
</div>
`;
}
private calculateMemoryPercent(): number {
// Simple extraction of numbers - in real app would parse properly
const used = parseFloat(this.data.memoryUsed);
const total = parseFloat(this.data.memoryTotal);
if (total === 0) return 0;
// Assuming both are in same unit for demo
return Math.min((used / total) * 100, 100);
}
}

View File

@@ -0,0 +1,151 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
declare global {
interface HTMLElementTagNameMap {
'sz-reverse-proxy-card': SzReverseProxyCard;
}
}
@customElement('sz-reverse-proxy-card')
export class SzReverseProxyCard extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 400px;">
<sz-reverse-proxy-card
httpPort="8080"
httpsPort="8443"
httpActive
httpsActive
routeCount="3"
></sz-reverse-proxy-card>
</div>
`;
@property({ type: String })
public accessor httpPort: string = '80';
@property({ type: String })
public accessor httpsPort: string = '443';
@property({ type: Boolean })
public accessor httpActive: boolean = false;
@property({ type: Boolean })
public accessor httpsActive: boolean = false;
@property({ type: String })
public accessor routeCount: string = '0';
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
height: 100%;
}
.card {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
padding: 20px;
height: 100%;
box-sizing: border-box;
}
.header {
margin-bottom: 16px;
}
.title {
font-size: 16px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.subtitle {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-top: 2px;
}
.items {
display: flex;
flex-direction: column;
gap: 10px;
}
.item {
display: flex;
justify-content: space-between;
align-items: center;
}
.item-label {
font-size: 14px;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.item-value {
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.badge {
display: inline-flex;
align-items: center;
padding: 2px 10px;
border-radius: 9999px;
font-size: 12px;
font-weight: 500;
}
.badge.active {
background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')};
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.badge.inactive {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
`,
];
public render(): TemplateResult {
return html`
<div class="card">
<div class="header">
<div class="title">Reverse Proxy</div>
<div class="subtitle">HTTP/HTTPS proxy status</div>
</div>
<div class="items">
<div class="item">
<span class="item-label">HTTP (${this.httpPort})</span>
<span class="badge ${this.httpActive ? 'active' : 'inactive'}">
${this.httpActive ? 'Active' : 'Inactive'}
</span>
</div>
<div class="item">
<span class="item-label">HTTPS (${this.httpsPort})</span>
<span class="badge ${this.httpsActive ? 'active' : 'inactive'}">
${this.httpsActive ? 'Active' : 'Inactive'}
</span>
</div>
<div class="item">
<span class="item-label">Routes</span>
<span class="item-value">${this.routeCount}</span>
</div>
</div>
</div>
`;
}
}

View File

@@ -0,0 +1,773 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
state,
type TemplateResult,
} from '@design.estate/dees-element';
declare global {
interface HTMLElementTagNameMap {
'sz-service-create-view': SzServiceCreateView;
}
}
export interface IRegistry {
id: string;
name: string;
url: string;
}
export interface IPortMapping {
hostPort: string;
containerPort: string;
protocol: 'tcp' | 'udp';
}
export interface IEnvVar {
key: string;
value: string;
}
export interface IVolumeMount {
hostPath: string;
containerPath: string;
readOnly: boolean;
}
export interface IServiceConfig {
name: string;
image: string;
ports: IPortMapping[];
envVars: IEnvVar[];
volumes: IVolumeMount[];
cpuLimit: string;
memoryLimit: string;
restartPolicy: 'always' | 'on-failure' | 'never';
networkMode: string;
}
@customElement('sz-service-create-view')
export class SzServiceCreateView extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 800px;">
<sz-service-create-view
.registries=${[
{ id: '1', name: 'Onebox Registry', url: 'registry.onebox.local' },
{ id: '2', name: 'Docker Hub', url: 'docker.io' },
]}
></sz-service-create-view>
</div>
`;
@property({ type: Array })
public accessor registries: IRegistry[] = [];
@property({ type: Boolean })
public accessor loading: boolean = false;
@state()
private accessor serviceName: string = '';
@state()
private accessor imageUrl: string = '';
@state()
private accessor selectedRegistry: string = '';
@state()
private accessor ports: IPortMapping[] = [{ hostPort: '', containerPort: '', protocol: 'tcp' }];
@state()
private accessor envVars: IEnvVar[] = [{ key: '', value: '' }];
@state()
private accessor volumes: IVolumeMount[] = [];
@state()
private accessor cpuLimit: string = '';
@state()
private accessor memoryLimit: string = '';
@state()
private accessor restartPolicy: 'always' | 'on-failure' | 'never' = 'always';
@state()
private accessor networkMode: string = 'bridge';
@state()
private accessor showAdvanced: boolean = false;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.header-title {
font-size: 20px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.header-subtitle {
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-top: 4px;
}
.section {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
padding: 20px;
margin-bottom: 16px;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.section-title svg {
width: 18px;
height: 18px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
.form-row.single {
grid-template-columns: 1fr;
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-label {
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.form-label .required {
color: ${cssManager.bdTheme('#ef4444', '#f87171')};
}
.form-hint {
font-size: 12px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.form-input,
.form-select {
width: 100%;
padding: 10px 12px;
background: ${cssManager.bdTheme('#ffffff', '#18181b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 6px;
font-size: 14px;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
outline: none;
transition: border-color 200ms ease;
box-sizing: border-box;
}
.form-input:focus,
.form-select:focus {
border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
}
.form-input::placeholder {
color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
}
.form-select {
cursor: pointer;
}
.dynamic-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.dynamic-row {
display: flex;
gap: 8px;
align-items: flex-start;
}
.dynamic-row .form-input {
flex: 1;
}
.dynamic-row .form-select {
width: 80px;
flex-shrink: 0;
}
.remove-button {
padding: 10px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 6px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
cursor: pointer;
transition: all 200ms ease;
flex-shrink: 0;
}
.remove-button:hover {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
border-color: ${cssManager.bdTheme('#fecaca', 'rgba(239, 68, 68, 0.3)')};
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.add-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: transparent;
border: 1px dashed ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 6px;
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
cursor: pointer;
transition: all 200ms ease;
margin-top: 8px;
}
.add-button:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
border-color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.add-button svg {
width: 14px;
height: 14px;
}
.toggle-advanced {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 0;
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
cursor: pointer;
background: none;
border: none;
}
.toggle-advanced svg {
width: 16px;
height: 16px;
transition: transform 200ms ease;
}
.toggle-advanced.open svg {
transform: rotate(180deg);
}
.checkbox-row {
display: flex;
align-items: center;
gap: 8px;
}
.checkbox {
width: 18px;
height: 18px;
accent-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
}
.actions {
display: flex;
justify-content: flex-end;
gap: 12px;
padding-top: 16px;
border-top: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
margin-top: 8px;
}
.button {
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 200ms ease;
display: inline-flex;
align-items: center;
gap: 8px;
}
.button.secondary {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.button.secondary:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
.button.primary {
background: ${cssManager.bdTheme('#18181b', '#fafafa')};
border: none;
color: ${cssManager.bdTheme('#fafafa', '#18181b')};
}
.button.primary:hover:not(:disabled) {
opacity: 0.9;
}
.button.primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top-color: currentColor;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
`,
];
public render(): TemplateResult {
return html`
<div class="header">
<div>
<div class="header-title">Deploy New Service</div>
<div class="header-subtitle">Configure and deploy a new Docker container</div>
</div>
</div>
<!-- Basic Info Section -->
<div class="section">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="9" y1="9" x2="15" y2="9"></line>
<line x1="9" y1="15" x2="15" y2="15"></line>
</svg>
Basic Information
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Service Name <span class="required">*</span></label>
<input
type="text"
class="form-input"
placeholder="my-service"
.value=${this.serviceName}
@input=${(e: Event) => this.serviceName = (e.target as HTMLInputElement).value}
>
<div class="form-hint">Unique name for the service (alphanumeric and hyphens)</div>
</div>
<div class="form-group">
<label class="form-label">Registry</label>
<select
class="form-select"
.value=${this.selectedRegistry}
@change=${(e: Event) => this.selectedRegistry = (e.target as HTMLSelectElement).value}
>
<option value="">Custom Image URL</option>
${this.registries.map(reg => html`
<option value=${reg.id}>${reg.name}</option>
`)}
</select>
</div>
</div>
<div class="form-row single">
<div class="form-group">
<label class="form-label">Image <span class="required">*</span></label>
<input
type="text"
class="form-input"
placeholder="nginx:latest or registry.example.com/image:tag"
.value=${this.imageUrl}
@input=${(e: Event) => this.imageUrl = (e.target as HTMLInputElement).value}
>
<div class="form-hint">Docker image to deploy (include tag)</div>
</div>
</div>
</div>
<!-- Port Configuration -->
<div class="section">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="2" y1="12" x2="22" y2="12"></line>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
</svg>
Port Configuration
</div>
<div class="dynamic-list">
${this.ports.map((port, index) => html`
<div class="dynamic-row">
<input
type="text"
class="form-input"
placeholder="Host Port"
.value=${port.hostPort}
@input=${(e: Event) => this.updatePort(index, 'hostPort', (e.target as HTMLInputElement).value)}
>
<input
type="text"
class="form-input"
placeholder="Container Port"
.value=${port.containerPort}
@input=${(e: Event) => this.updatePort(index, 'containerPort', (e.target as HTMLInputElement).value)}
>
<select
class="form-select"
.value=${port.protocol}
@change=${(e: Event) => this.updatePort(index, 'protocol', (e.target as HTMLSelectElement).value)}
>
<option value="tcp">TCP</option>
<option value="udp">UDP</option>
</select>
${this.ports.length > 1 ? html`
<button class="remove-button" @click=${() => this.removePort(index)}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
` : ''}
</div>
`)}
</div>
<button class="add-button" @click=${() => this.addPort()}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Add Port Mapping
</button>
</div>
<!-- Environment Variables -->
<div class="section">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="4 17 10 11 4 5"></polyline>
<line x1="12" y1="19" x2="20" y2="19"></line>
</svg>
Environment Variables
</div>
<div class="dynamic-list">
${this.envVars.map((env, index) => html`
<div class="dynamic-row">
<input
type="text"
class="form-input"
placeholder="KEY"
.value=${env.key}
@input=${(e: Event) => this.updateEnvVar(index, 'key', (e.target as HTMLInputElement).value)}
>
<input
type="text"
class="form-input"
placeholder="value"
.value=${env.value}
@input=${(e: Event) => this.updateEnvVar(index, 'value', (e.target as HTMLInputElement).value)}
>
${this.envVars.length > 1 ? html`
<button class="remove-button" @click=${() => this.removeEnvVar(index)}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
` : ''}
</div>
`)}
</div>
<button class="add-button" @click=${() => this.addEnvVar()}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Add Environment Variable
</button>
</div>
<!-- Advanced Options Toggle -->
<button
class="toggle-advanced ${this.showAdvanced ? 'open' : ''}"
@click=${() => this.showAdvanced = !this.showAdvanced}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
Advanced Options
</button>
${this.showAdvanced ? html`
<!-- Volumes -->
<div class="section">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
</svg>
Volume Mounts
</div>
<div class="dynamic-list">
${this.volumes.length === 0 ? html`
<div class="form-hint">No volumes configured</div>
` : this.volumes.map((vol, index) => html`
<div class="dynamic-row">
<input
type="text"
class="form-input"
placeholder="/host/path"
.value=${vol.hostPath}
@input=${(e: Event) => this.updateVolume(index, 'hostPath', (e.target as HTMLInputElement).value)}
>
<input
type="text"
class="form-input"
placeholder="/container/path"
.value=${vol.containerPath}
@input=${(e: Event) => this.updateVolume(index, 'containerPath', (e.target as HTMLInputElement).value)}
>
<div class="checkbox-row">
<input
type="checkbox"
class="checkbox"
?checked=${vol.readOnly}
@change=${(e: Event) => this.updateVolume(index, 'readOnly', (e.target as HTMLInputElement).checked)}
>
<span class="form-hint">RO</span>
</div>
<button class="remove-button" @click=${() => this.removeVolume(index)}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
`)}
</div>
<button class="add-button" @click=${() => this.addVolume()}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Add Volume Mount
</button>
</div>
<!-- Resource Limits -->
<div class="section">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="1" y="4" width="22" height="16" rx="2" ry="2"></rect>
<line x1="1" y1="10" x2="23" y2="10"></line>
</svg>
Resource Limits
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">CPU Limit</label>
<input
type="text"
class="form-input"
placeholder="e.g., 1.0 or 0.5"
.value=${this.cpuLimit}
@input=${(e: Event) => this.cpuLimit = (e.target as HTMLInputElement).value}
>
<div class="form-hint">Number of CPUs (leave empty for unlimited)</div>
</div>
<div class="form-group">
<label class="form-label">Memory Limit</label>
<input
type="text"
class="form-input"
placeholder="e.g., 512m or 1g"
.value=${this.memoryLimit}
@input=${(e: Event) => this.memoryLimit = (e.target as HTMLInputElement).value}
>
<div class="form-hint">Memory limit (leave empty for unlimited)</div>
</div>
</div>
</div>
<!-- Restart Policy & Network -->
<div class="section">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
Container Settings
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Restart Policy</label>
<select
class="form-select"
.value=${this.restartPolicy}
@change=${(e: Event) => this.restartPolicy = (e.target as HTMLSelectElement).value as any}
>
<option value="always">Always</option>
<option value="on-failure">On Failure</option>
<option value="never">Never</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Network Mode</label>
<select
class="form-select"
.value=${this.networkMode}
@change=${(e: Event) => this.networkMode = (e.target as HTMLSelectElement).value}
>
<option value="bridge">Bridge</option>
<option value="host">Host</option>
<option value="none">None</option>
</select>
</div>
</div>
</div>
` : ''}
<div class="actions">
<button class="button secondary" @click=${() => this.handleCancel()}>Cancel</button>
<button
class="button primary"
?disabled=${this.loading || !this.isValid()}
@click=${() => this.handleCreate()}
>
${this.loading ? html`<div class="spinner"></div>` : ''}
${this.loading ? 'Deploying...' : 'Deploy Service'}
</button>
</div>
`;
}
private isValid(): boolean {
return this.serviceName.trim() !== '' && this.imageUrl.trim() !== '';
}
private addPort() {
this.ports = [...this.ports, { hostPort: '', containerPort: '', protocol: 'tcp' }];
}
private removePort(index: number) {
this.ports = this.ports.filter((_, i) => i !== index);
}
private updatePort(index: number, field: keyof IPortMapping, value: string) {
const newPorts = [...this.ports];
(newPorts[index] as any)[field] = value;
this.ports = newPorts;
}
private addEnvVar() {
this.envVars = [...this.envVars, { key: '', value: '' }];
}
private removeEnvVar(index: number) {
this.envVars = this.envVars.filter((_, i) => i !== index);
}
private updateEnvVar(index: number, field: keyof IEnvVar, value: string) {
const newEnvVars = [...this.envVars];
newEnvVars[index][field] = value;
this.envVars = newEnvVars;
}
private addVolume() {
this.volumes = [...this.volumes, { hostPath: '', containerPath: '', readOnly: false }];
}
private removeVolume(index: number) {
this.volumes = this.volumes.filter((_, i) => i !== index);
}
private updateVolume(index: number, field: keyof IVolumeMount, value: string | boolean) {
const newVolumes = [...this.volumes];
(newVolumes[index] as any)[field] = value;
this.volumes = newVolumes;
}
private handleCancel() {
this.dispatchEvent(new CustomEvent('cancel', { bubbles: true, composed: true }));
}
private handleCreate() {
const config: IServiceConfig = {
name: this.serviceName.trim(),
image: this.imageUrl.trim(),
ports: this.ports.filter(p => p.hostPort && p.containerPort),
envVars: this.envVars.filter(e => e.key),
volumes: this.volumes.filter(v => v.hostPath && v.containerPath),
cpuLimit: this.cpuLimit,
memoryLimit: this.memoryLimit,
restartPolicy: this.restartPolicy,
networkMode: this.networkMode,
};
this.dispatchEvent(new CustomEvent('create-service', {
detail: config,
bubbles: true,
composed: true,
}));
}
public reset() {
this.serviceName = '';
this.imageUrl = '';
this.selectedRegistry = '';
this.ports = [{ hostPort: '', containerPort: '', protocol: 'tcp' }];
this.envVars = [{ key: '', value: '' }];
this.volumes = [];
this.cpuLimit = '';
this.memoryLimit = '';
this.restartPolicy = 'always';
this.networkMode = 'bridge';
this.showAdvanced = false;
}
}

View File

@@ -0,0 +1,710 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
import './sz-stat-card.js';
declare global {
interface HTMLElementTagNameMap {
'sz-service-detail-view': SzServiceDetailView;
}
}
export interface IServiceDetail {
name: string;
status: 'running' | 'stopped' | 'starting' | 'error';
image: string;
port: number;
domain: string | null;
containerId: string;
created: string;
updated: string;
registry: string;
repository: string;
tag: string;
}
export interface IServiceStats {
cpu: number;
memory: string;
memoryLimit: string;
networkIn: string;
networkOut: string;
}
export interface IServiceBackup {
id: string;
createdAt: string;
size: string;
type: string;
}
export interface ILogEntry {
timestamp: string;
message: string;
level?: 'info' | 'warn' | 'error';
}
@customElement('sz-service-detail-view')
export class SzServiceDetailView extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 1200px;">
<sz-service-detail-view
.service=${{
name: 'test-nginx',
status: 'running',
image: 'nginx:alpine',
port: 80,
domain: 'app.bleu.de',
containerId: 'pchbbr9fjr4g',
created: '11/18/2025, 2:06:55 PM',
updated: '11/26/2025, 4:05:46 PM',
registry: 'Docker Hub',
repository: 'nginx',
tag: 'alpine',
}}
.stats=${{
cpu: 0.5,
memory: '32.1 MB',
memoryLimit: '61.3 GB',
networkIn: '6.4 KB',
networkOut: '252 B',
}}
.backups=${[
{ id: '1', createdAt: '1/2/2026, 2:00:03 AM', size: '21.96 MB', type: 'Docker Image' },
{ id: '2', createdAt: '11/27/2025, 1:42:26 PM', size: '51.76 MB', type: 'Docker Image' },
]}
.logs=${[
{ timestamp: '2024-01-02 10:15:32', message: '192.168.1.100 - - [02/Jan/2024:10:15:32 +0000] "GET / HTTP/1.1" 200 612' },
{ timestamp: '2024-01-02 10:15:30', message: '192.168.1.100 - - [02/Jan/2024:10:15:30 +0000] "GET /favicon.ico HTTP/1.1" 404 153' },
]}
></sz-service-detail-view>
</div>
`;
@property({ type: Object })
public accessor service: IServiceDetail = {
name: '',
status: 'stopped',
image: '',
port: 0,
domain: null,
containerId: '',
created: '',
updated: '',
registry: '',
repository: '',
tag: '',
};
@property({ type: Object })
public accessor stats: IServiceStats = {
cpu: 0,
memory: '0 MB',
memoryLimit: '0 GB',
networkIn: '0 B',
networkOut: '0 B',
};
@property({ type: Array })
public accessor backups: IServiceBackup[] = [];
@property({ type: Array })
public accessor logs: ILogEntry[] = [];
@property({ type: Boolean })
public accessor streaming: boolean = false;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
}
.header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
}
.back-link {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
cursor: pointer;
transition: color 200ms ease;
}
.back-link:hover {
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.service-header {
display: flex;
align-items: center;
gap: 12px;
}
.service-name {
font-size: 24px;
font-weight: 700;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.status-badge {
display: inline-flex;
align-items: center;
padding: 4px 12px;
border-radius: 9999px;
font-size: 13px;
font-weight: 500;
}
.status-badge.running {
background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')};
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.status-badge.stopped {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.content {
display: grid;
grid-template-columns: 1fr;
gap: 24px;
}
@media (min-width: 1024px) {
.content {
grid-template-columns: 2fr 1fr;
}
}
.main-content {
display: flex;
flex-direction: column;
gap: 24px;
}
.sidebar {
display: flex;
flex-direction: column;
gap: 24px;
}
.card {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
overflow: hidden;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
}
.card-title {
font-size: 16px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.card-subtitle {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-top: 2px;
}
.card-content {
padding: 16px;
}
.detail-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.detail-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.detail-label {
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.detail-value {
font-size: 14px;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
text-align: right;
}
.detail-value a {
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
text-decoration: none;
}
.detail-value a:hover {
text-decoration: underline;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.stat-item {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
border-radius: 6px;
padding: 12px;
}
.stat-label {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-bottom: 4px;
}
.stat-value {
font-size: 18px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.stat-subvalue {
font-size: 12px;
color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
}
.actions-grid {
display: flex;
flex-direction: column;
gap: 8px;
}
.action-button {
width: 100%;
padding: 10px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 200ms ease;
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.action-button:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
.action-button.danger {
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
border-color: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.3)')};
}
.action-button.danger:hover {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
}
.backup-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.backup-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
border-radius: 6px;
}
.backup-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.backup-date {
font-size: 13px;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.backup-meta {
font-size: 12px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.backup-actions {
display: flex;
gap: 4px;
}
.icon-button {
padding: 6px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 4px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
cursor: pointer;
transition: all 200ms ease;
}
.icon-button:hover {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.logs-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.logs-actions {
display: flex;
gap: 8px;
align-items: center;
}
.stream-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: ${cssManager.bdTheme('#2563eb', '#3b82f6')};
border: none;
border-radius: 4px;
font-size: 13px;
font-weight: 500;
color: white;
cursor: pointer;
}
.stream-button.streaming {
background: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.clear-button {
padding: 6px 12px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 4px;
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
cursor: pointer;
}
.logs-container {
padding: 16px;
font-family: monospace;
font-size: 12px;
max-height: 300px;
overflow-y: auto;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
}
.log-entry {
padding: 2px 0;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
white-space: pre-wrap;
word-break: break-all;
}
.empty-logs {
padding: 24px;
text-align: center;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.tag-badge {
display: inline-flex;
padding: 2px 8px;
background: ${cssManager.bdTheme('#dbeafe', 'rgba(59, 130, 246, 0.2)')};
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
`,
];
public render(): TemplateResult {
return html`
<div class="header">
<div class="back-link" @click=${() => this.handleBack()}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
Back to Services
</div>
</div>
<div class="service-header" style="margin-bottom: 24px;">
<h1 class="service-name">${this.service.name}</h1>
<span class="status-badge ${this.service.status}">${this.service.status}</span>
</div>
<div class="content">
<div class="main-content">
<div class="card">
<div class="card-header">
<div>
<div class="card-title">Service Details</div>
</div>
<button class="action-button" style="width: auto; padding: 6px 12px;" @click=${() => this.handleEdit()}>Edit</button>
</div>
<div class="card-content">
<div class="detail-list">
<div class="detail-item">
<span class="detail-label">Image</span>
<span class="detail-value">${this.service.image}</span>
</div>
<div class="detail-item">
<span class="detail-label">Port</span>
<span class="detail-value">${this.service.port}</span>
</div>
<div class="detail-item">
<span class="detail-label">Domain</span>
<span class="detail-value">
${this.service.domain
? html`<a href="https://${this.service.domain}" target="_blank">${this.service.domain}</a>`
: '-'}
</span>
</div>
<div class="detail-item">
<span class="detail-label">Container ID</span>
<span class="detail-value">${this.service.containerId}</span>
</div>
<div class="detail-item">
<span class="detail-label">Created</span>
<span class="detail-value">${this.service.created}</span>
</div>
<div class="detail-item">
<span class="detail-label">Updated</span>
<span class="detail-value">${this.service.updated}</span>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<div class="logs-header" style="width: 100%;">
<div>
<div class="card-title">Logs</div>
<div class="card-subtitle">Container logs</div>
</div>
<div class="logs-actions">
<button class="stream-button ${this.streaming ? 'streaming' : ''}" @click=${() => this.toggleStreaming()}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
${this.streaming
? html`<rect x="6" y="6" width="12" height="12" rx="1"/>`
: html`<polygon points="5,3 19,12 5,21"/>`
}
</svg>
${this.streaming ? 'Stop' : 'Stream'}
</button>
<button class="clear-button" @click=${() => this.handleClearLogs()}>Clear logs</button>
</div>
</div>
</div>
<div class="logs-container">
${this.logs.length > 0 ? this.logs.map(log => html`
<div class="log-entry">${log.timestamp} ${log.message}</div>
`) : html`
<div class="empty-logs">Click "Stream" to start live log streaming</div>
`}
</div>
</div>
</div>
<div class="sidebar">
<div class="card">
<div class="card-header">
<div class="card-title">Live stats</div>
</div>
<div class="card-content">
<div class="stats-grid">
<div class="stat-item">
<div class="stat-label">CPU</div>
<div class="stat-value">${this.stats.cpu.toFixed(1)}%</div>
</div>
<div class="stat-item">
<div class="stat-label">Memory</div>
<div class="stat-value">${this.stats.memory}</div>
<div class="stat-subvalue">of ${this.stats.memoryLimit}</div>
</div>
<div class="stat-item">
<div class="stat-label">Network In</div>
<div class="stat-value">${this.stats.networkIn}</div>
</div>
<div class="stat-item">
<div class="stat-label">Network Out</div>
<div class="stat-value">${this.stats.networkOut}</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<div>
<div class="card-title">Actions</div>
<div class="card-subtitle">Manage service state</div>
</div>
</div>
<div class="card-content">
<div class="actions-grid">
${this.service.status === 'running'
? html`<button class="action-button" @click=${() => this.handleAction('stop')}>Stop Service</button>`
: html`<button class="action-button" @click=${() => this.handleAction('start')}>Start Service</button>`
}
<button class="action-button" @click=${() => this.handleAction('restart')}>Restart Service</button>
<button class="action-button danger" @click=${() => this.handleAction('delete')}>Delete Service</button>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<div>
<div class="card-title">Image Source</div>
<div class="card-subtitle">${this.service.registry === 'Docker Hub' ? 'External container registry' : 'Onebox registry'}</div>
</div>
</div>
<div class="card-content">
<div class="detail-list">
<div class="detail-item">
<span class="detail-label">Registry</span>
<span class="detail-value">${this.service.registry}</span>
</div>
<div class="detail-item">
<span class="detail-label">Repository</span>
<span class="detail-value">${this.service.repository}</span>
</div>
<div class="detail-item">
<span class="detail-label">Tag</span>
<span class="detail-value"><span class="tag-badge">${this.service.tag}</span></span>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<div>
<div class="card-title">Backups</div>
<div class="card-subtitle">Create and manage service backups</div>
</div>
<button class="action-button" style="width: auto; padding: 6px 12px;" @click=${() => this.handleCreateBackup()}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 4px;">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Create Backup
</button>
</div>
<div class="card-content">
<div class="backup-list">
${this.backups.map(backup => html`
<div class="backup-item">
<div class="backup-info">
<div class="backup-date">${backup.createdAt}</div>
<div class="backup-meta">${backup.size} · ${backup.type}</div>
</div>
<div class="backup-actions">
<button class="icon-button" title="Download" @click=${() => this.handleDownloadBackup(backup)}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
</button>
<button class="icon-button" title="Restore" @click=${() => this.handleRestoreBackup(backup)}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="1 4 1 10 7 10"/>
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
</svg>
</button>
<button class="icon-button" title="Delete" @click=${() => this.handleDeleteBackup(backup)}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3,6 5,6 21,6"/>
<path d="M19,6v14a2,2,0,0,1-2,2H7a2,2,0,0,1-2-2V6m3,0V4a2,2,0,0,1,2-2h4a2,2,0,0,1,2,2v2"/>
</svg>
</button>
</div>
</div>
`)}
</div>
</div>
</div>
</div>
</div>
`;
}
private handleBack() {
this.dispatchEvent(new CustomEvent('back', { bubbles: true, composed: true }));
}
private handleEdit() {
this.dispatchEvent(new CustomEvent('edit', { detail: this.service, bubbles: true, composed: true }));
}
private handleAction(action: 'start' | 'stop' | 'restart' | 'delete') {
this.dispatchEvent(new CustomEvent('service-action', { detail: { service: this.service, action }, bubbles: true, composed: true }));
}
private toggleStreaming() {
this.streaming = !this.streaming;
this.dispatchEvent(new CustomEvent('stream-toggle', { detail: { streaming: this.streaming }, bubbles: true, composed: true }));
}
private handleClearLogs() {
this.dispatchEvent(new CustomEvent('clear-logs', { bubbles: true, composed: true }));
}
private handleCreateBackup() {
this.dispatchEvent(new CustomEvent('create-backup', { bubbles: true, composed: true }));
}
private handleDownloadBackup(backup: IServiceBackup) {
this.dispatchEvent(new CustomEvent('download-backup', { detail: backup, bubbles: true, composed: true }));
}
private handleRestoreBackup(backup: IServiceBackup) {
this.dispatchEvent(new CustomEvent('restore-backup', { detail: backup, bubbles: true, composed: true }));
}
private handleDeleteBackup(backup: IServiceBackup) {
this.dispatchEvent(new CustomEvent('delete-backup', { detail: backup, bubbles: true, composed: true }));
}
}

View File

@@ -0,0 +1,390 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
declare global {
interface HTMLElementTagNameMap {
'sz-services-backups-view': SzServicesBackupsView;
}
}
export interface IBackupSchedule {
id: string;
scope: string;
retention: string;
schedule: string;
lastRun?: string;
nextRun?: string;
status: 'active' | 'failed' | 'disabled';
error?: string;
}
export interface IBackup {
id: string;
service: string;
createdAt: string;
size: string;
includes: string[];
}
@customElement('sz-services-backups-view')
export class SzServicesBackupsView extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 1200px;">
<sz-services-backups-view
.schedules=${[
{ id: '1', scope: 'All Services', retention: 'D:7, W:4, M:12', schedule: '0 2 * * *', lastRun: '1/2/2026, 2:00:03 AM', nextRun: '1/3/2026, 2:00:00 AM', status: 'active' },
]}
.backups=${[
{ id: '1', service: 'test-nginx', createdAt: '1/2/2026, 2:00:03 AM', size: '22.0 MB', includes: ['Image'] },
{ id: '2', service: 'hello-world', createdAt: '1/2/2026, 2:00:02 AM', size: '21.5 MB', includes: ['Image'] },
{ id: '3', service: 'test-ch-final', createdAt: '1/2/2026, 2:00:00 AM', size: '22.0 MB', includes: ['Image', 'clickhouse'] },
]}
></sz-services-backups-view>
</div>
`;
@property({ type: Array })
public accessor schedules: IBackupSchedule[] = [];
@property({ type: Array })
public accessor backups: IBackup[] = [];
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
}
.section {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
margin-bottom: 24px;
overflow: hidden;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
}
.section-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.section-subtitle {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.header-actions {
display: flex;
gap: 8px;
}
.action-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 6px;
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
cursor: pointer;
transition: all 200ms ease;
}
.action-button:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
.action-button.primary {
background: ${cssManager.bdTheme('#18181b', '#fafafa')};
color: ${cssManager.bdTheme('#fafafa', '#18181b')};
border: none;
}
.action-button.primary:hover {
opacity: 0.9;
}
.table-header {
display: grid;
gap: 16px;
padding: 12px 16px;
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.schedules-header {
grid-template-columns: 1fr 1fr 1fr 1.5fr 1.5fr 80px 120px;
}
.backups-header {
grid-template-columns: 1.5fr 1.5fr 100px 1fr 120px;
}
.table-row {
display: grid;
gap: 16px;
padding: 12px 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#27272a')};
font-size: 14px;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
align-items: center;
}
.schedules-row {
grid-template-columns: 1fr 1fr 1fr 1.5fr 1.5fr 80px 120px;
}
.backups-row {
grid-template-columns: 1.5fr 1.5fr 100px 1fr 120px;
}
.table-row:last-child {
border-bottom: none;
}
.table-row:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
.status-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 9999px;
font-size: 12px;
font-weight: 500;
}
.status-badge.active {
background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')};
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.status-badge.failed {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.status-badge.disabled {
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.meta-text {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.includes-list {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.include-badge {
padding: 2px 8px;
background: ${cssManager.bdTheme('#dbeafe', 'rgba(59, 130, 246, 0.2)')};
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
.row-actions {
display: flex;
gap: 4px;
}
.icon-button {
padding: 6px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 4px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
cursor: pointer;
transition: all 200ms ease;
}
.icon-button:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.icon-button.danger:hover {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
border-color: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.3)')};
}
`,
];
public render(): TemplateResult {
return html`
<div class="section">
<div class="section-header">
<div class="section-info">
<div class="section-title">Backup Schedules</div>
<div class="section-subtitle">Configure automated backup schedules for your services</div>
</div>
<div class="header-actions">
<button class="action-button" @click=${() => this.handleImport()}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
Import Backup
</button>
<button class="action-button primary" @click=${() => this.handleCreateSchedule()}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Create Schedule
</button>
</div>
</div>
<div class="table-header schedules-header">
<span>Scope</span>
<span>Retention</span>
<span>Schedule</span>
<span>Last Run</span>
<span>Next Run</span>
<span>Status</span>
<span>Actions</span>
</div>
${this.schedules.map(schedule => html`
<div class="table-row schedules-row">
<span>${schedule.scope}</span>
<span class="meta-text" title="Keep: ${schedule.retention}">${schedule.retention}</span>
<span class="meta-text">${schedule.schedule}</span>
<span class="meta-text">${schedule.lastRun || '-'}</span>
<span class="meta-text">${schedule.nextRun || '-'}</span>
<span>
<span class="status-badge ${schedule.status}" title="${schedule.error || ''}">
${schedule.status === 'active' ? 'Active' : schedule.status === 'failed' ? 'Failed' : 'Disabled'}
</span>
</span>
<span class="row-actions">
<button class="icon-button" title="Run backup now" @click=${() => this.handleRunNow(schedule)}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5,3 19,12 5,21"/></svg>
</button>
<button class="icon-button" title="${schedule.status === 'disabled' ? 'Enable' : 'Disable'}" @click=${() => this.handleToggle(schedule)}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
${schedule.status === 'disabled'
? html`<polyline points="20 6 9 17 4 12"></polyline>`
: html`<rect x="6" y="6" width="12" height="12" rx="1"/>`
}
</svg>
</button>
<button class="icon-button danger" title="Delete" @click=${() => this.handleDeleteSchedule(schedule)}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3,6 5,6 21,6"/><path d="M19,6v14a2,2,0,0,1-2,2H7a2,2,0,0,1-2-2V6m3,0V4a2,2,0,0,1,2-2h4a2,2,0,0,1,2,2v2"/>
</svg>
</button>
</span>
</div>
`)}
</div>
<div class="section">
<div class="section-header">
<div class="section-info">
<div class="section-title">All Backups</div>
<div class="section-subtitle">Browse and manage all backups across services</div>
</div>
</div>
<div class="table-header backups-header">
<span>Service</span>
<span>Created</span>
<span>Size</span>
<span>Includes</span>
<span>Actions</span>
</div>
${this.backups.map(backup => html`
<div class="table-row backups-row">
<span>${backup.service}</span>
<span class="meta-text">${backup.createdAt}</span>
<span class="meta-text">${backup.size}</span>
<span class="includes-list">
${backup.includes.map(inc => html`<span class="include-badge">${inc}</span>`)}
</span>
<span class="row-actions">
<button class="icon-button" title="Download backup" @click=${() => this.handleDownload(backup)}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
</button>
<button class="icon-button danger" title="Delete" @click=${() => this.handleDeleteBackup(backup)}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3,6 5,6 21,6"/><path d="M19,6v14a2,2,0,0,1-2,2H7a2,2,0,0,1-2-2V6m3,0V4a2,2,0,0,1,2-2h4a2,2,0,0,1,2,2v2"/>
</svg>
</button>
</span>
</div>
`)}
</div>
`;
}
private handleImport() {
this.dispatchEvent(new CustomEvent('import', { bubbles: true, composed: true }));
}
private handleCreateSchedule() {
this.dispatchEvent(new CustomEvent('create-schedule', { bubbles: true, composed: true }));
}
private handleRunNow(schedule: IBackupSchedule) {
this.dispatchEvent(new CustomEvent('run-now', { detail: schedule, bubbles: true, composed: true }));
}
private handleToggle(schedule: IBackupSchedule) {
this.dispatchEvent(new CustomEvent('toggle-schedule', { detail: schedule, bubbles: true, composed: true }));
}
private handleDeleteSchedule(schedule: IBackupSchedule) {
this.dispatchEvent(new CustomEvent('delete-schedule', { detail: schedule, bubbles: true, composed: true }));
}
private handleDownload(backup: IBackup) {
this.dispatchEvent(new CustomEvent('download', { detail: backup, bubbles: true, composed: true }));
}
private handleDeleteBackup(backup: IBackup) {
this.dispatchEvent(new CustomEvent('delete-backup', { detail: backup, bubbles: true, composed: true }));
}
}

View File

@@ -0,0 +1,237 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
declare global {
interface HTMLElementTagNameMap {
'sz-services-list-view': SzServicesListView;
}
}
export interface IService {
name: string;
image: string;
domain: string | null;
status: 'running' | 'stopped' | 'starting' | 'error';
}
@customElement('sz-services-list-view')
export class SzServicesListView extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 1200px;">
<sz-services-list-view
.services=${[
{ name: 'test-nginx', image: 'nginx:alpine', domain: 'app.bleu.de', status: 'running' },
{ name: 'hello-world', image: 'localhost:3000/hello-world:latest', domain: 'hello.task.vc', status: 'running' },
{ name: 'test-v2', image: 'localhost:3000/test-registry:v1', domain: null, status: 'running' },
{ name: 'api-service', image: 'node:18-alpine', domain: 'api.example.com', status: 'stopped' },
]}
></sz-services-list-view>
</div>
`;
@property({ type: Array })
public accessor services: IService[] = [];
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
}
.table-container {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
overflow: hidden;
}
.table-header {
display: grid;
grid-template-columns: 1.5fr 2fr 1.5fr 100px 200px;
gap: 16px;
padding: 12px 16px;
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.table-row {
display: grid;
grid-template-columns: 1.5fr 2fr 1.5fr 100px 200px;
gap: 16px;
padding: 12px 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#27272a')};
font-size: 14px;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
align-items: center;
transition: background 200ms ease;
}
.table-row:last-child {
border-bottom: none;
}
.table-row:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
.service-name {
font-weight: 500;
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
cursor: pointer;
}
.service-name:hover {
text-decoration: underline;
}
.image {
font-family: monospace;
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.domain-link {
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
text-decoration: none;
}
.domain-link:hover {
text-decoration: underline;
}
.status-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 9999px;
font-size: 12px;
font-weight: 500;
}
.status-badge.running {
background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')};
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.status-badge.stopped {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.status-badge.starting {
background: ${cssManager.bdTheme('#fef9c3', 'rgba(250, 204, 21, 0.2)')};
color: ${cssManager.bdTheme('#ca8a04', '#facc15')};
}
.status-badge.error {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.actions {
display: flex;
gap: 8px;
}
.action-button {
padding: 4px 10px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: all 200ms ease;
}
.action-button:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
.action-button.stop {
color: ${cssManager.bdTheme('#ca8a04', '#facc15')};
border-color: ${cssManager.bdTheme('#fef9c3', 'rgba(250, 204, 21, 0.3)')};
}
.action-button.start {
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
border-color: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.3)')};
}
.action-button.restart {
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
border-color: ${cssManager.bdTheme('#dbeafe', 'rgba(59, 130, 246, 0.3)')};
}
.action-button.delete {
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
border-color: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.3)')};
}
.empty-state {
padding: 48px 24px;
text-align: center;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
`,
];
public render(): TemplateResult {
return html`
<div class="table-container">
<div class="table-header">
<span>Name</span>
<span>Image</span>
<span>Domain</span>
<span>Status</span>
<span>Actions</span>
</div>
${this.services.length > 0 ? this.services.map(service => html`
<div class="table-row">
<span class="service-name" @click=${() => this.handleServiceClick(service)}>${service.name}</span>
<span class="image" title="${service.image}">${service.image}</span>
<span>
${service.domain
? html`<a class="domain-link" href="https://${service.domain}" target="_blank">${service.domain}</a>`
: '-'}
</span>
<span><span class="status-badge ${service.status}">${service.status}</span></span>
<span class="actions">
${service.status === 'running'
? html`<button class="action-button stop" @click=${() => this.handleAction(service, 'stop')}>Stop</button>`
: html`<button class="action-button start" @click=${() => this.handleAction(service, 'start')}>Start</button>`
}
<button class="action-button restart" @click=${() => this.handleAction(service, 'restart')}>Restart</button>
<button class="action-button delete" @click=${() => this.handleAction(service, 'delete')}>Delete</button>
</span>
</div>
`) : html`
<div class="empty-state">No services deployed</div>
`}
</div>
`;
}
private handleServiceClick(service: IService) {
this.dispatchEvent(new CustomEvent('service-click', { detail: service, bubbles: true, composed: true }));
}
private handleAction(service: IService, action: 'start' | 'stop' | 'restart' | 'delete') {
this.dispatchEvent(new CustomEvent('service-action', { detail: { service, action }, bubbles: true, composed: true }));
}
}

View File

@@ -0,0 +1,417 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
declare global {
interface HTMLElementTagNameMap {
'sz-settings-view': SzSettingsView;
}
}
export interface ISettings {
darkMode: boolean;
cloudflareToken: string;
cloudflareZoneId: string;
autoRenewCerts: boolean;
renewalThreshold: number;
acmeEmail: string;
httpPort: number;
httpsPort: number;
forceHttps: boolean;
}
@customElement('sz-settings-view')
export class SzSettingsView extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 800px;">
<sz-settings-view
.settings=${{
darkMode: true,
cloudflareToken: '',
cloudflareZoneId: '',
autoRenewCerts: true,
renewalThreshold: 30,
acmeEmail: 'certs@example.com',
httpPort: 80,
httpsPort: 443,
forceHttps: true,
}}
currentUser="admin"
></sz-settings-view>
</div>
`;
@property({ type: Object })
public accessor settings: ISettings = {
darkMode: false,
cloudflareToken: '',
cloudflareZoneId: '',
autoRenewCerts: true,
renewalThreshold: 30,
acmeEmail: '',
httpPort: 80,
httpsPort: 443,
forceHttps: true,
};
@property({ type: String })
public accessor currentUser: string = '';
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
}
.section {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
padding: 20px;
margin-bottom: 24px;
}
.section-header {
margin-bottom: 16px;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.section-subtitle {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-top: 2px;
}
.form-group {
margin-bottom: 16px;
}
.form-group:last-child {
margin-bottom: 0;
}
.form-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#27272a')};
}
.form-row:last-child {
border-bottom: none;
}
.form-label-group {
display: flex;
flex-direction: column;
gap: 2px;
}
.form-label {
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.form-hint {
font-size: 12px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.input-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.input-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
input[type="text"],
input[type="password"],
input[type="email"],
input[type="number"] {
width: 100%;
padding: 8px 12px;
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 6px;
font-size: 14px;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
outline: none;
transition: border-color 200ms ease;
box-sizing: border-box;
}
input:focus {
border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
}
input::placeholder {
color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
}
.toggle-switch {
position: relative;
width: 44px;
height: 24px;
background: ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 9999px;
cursor: pointer;
transition: background 200ms ease;
}
.toggle-switch.active {
background: ${cssManager.bdTheme('#2563eb', '#3b82f6')};
}
.toggle-switch::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
transition: transform 200ms ease;
}
.toggle-switch.active::after {
transform: translateX(20px);
}
.password-section {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid ${cssManager.bdTheme('#f4f4f5', '#27272a')};
}
.password-title {
font-size: 14px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
margin-bottom: 12px;
}
.password-fields {
display: flex;
flex-direction: column;
gap: 12px;
}
.field-label {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-bottom: 4px;
}
.actions {
display: flex;
justify-content: flex-end;
gap: 12px;
padding-top: 16px;
border-top: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
margin-top: 24px;
}
.button {
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 200ms ease;
}
.button.secondary {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.button.secondary:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
.button.primary {
background: ${cssManager.bdTheme('#18181b', '#fafafa')};
border: none;
color: ${cssManager.bdTheme('#fafafa', '#18181b')};
}
.button.primary:hover {
opacity: 0.9;
}
`,
];
public render(): TemplateResult {
return html`
<div class="section">
<div class="section-header">
<div class="section-title">Appearance</div>
<div class="section-subtitle">Customize the look and feel</div>
</div>
<div class="form-row">
<div class="form-label-group">
<span class="form-label">Dark Mode</span>
<span class="form-hint">Toggle dark mode on or off</span>
</div>
<div class="toggle-switch ${this.settings.darkMode ? 'active' : ''}" @click=${() => this.toggleDarkMode()}></div>
</div>
</div>
<div class="section">
<div class="section-header">
<div class="section-title">Cloudflare Integration</div>
<div class="section-subtitle">Configure Cloudflare API for DNS management</div>
</div>
<div class="input-group">
<div class="form-group">
<div class="field-label">API Token</div>
<input type="password" placeholder="Enter Cloudflare API token" .value=${this.settings.cloudflareToken} @input=${(e: Event) => this.updateSetting('cloudflareToken', (e.target as HTMLInputElement).value)}>
</div>
<div class="form-group">
<div class="field-label">Zone ID (Optional)</div>
<input type="text" placeholder="Default zone ID" .value=${this.settings.cloudflareZoneId} @input=${(e: Event) => this.updateSetting('cloudflareZoneId', (e.target as HTMLInputElement).value)}>
</div>
<div class="form-hint">Get your API token from the Cloudflare dashboard with DNS edit permissions.</div>
</div>
</div>
<div class="section">
<div class="section-header">
<div class="section-title">SSL/TLS Settings</div>
<div class="section-subtitle">Configure certificate management</div>
</div>
<div class="form-row">
<div class="form-label-group">
<span class="form-label">Auto-Renew Certificates</span>
<span class="form-hint">Automatically renew certificates before expiry</span>
</div>
<div class="toggle-switch ${this.settings.autoRenewCerts ? 'active' : ''}" @click=${() => this.toggleSetting('autoRenewCerts')}></div>
</div>
<div class="form-group" style="margin-top: 16px;">
<div class="field-label">Renewal Threshold (days)</div>
<input type="number" .value=${String(this.settings.renewalThreshold)} @input=${(e: Event) => this.updateSetting('renewalThreshold', parseInt((e.target as HTMLInputElement).value))}>
<div class="form-hint">Renew certificates when they have fewer than this many days remaining.</div>
</div>
<div class="form-group">
<div class="field-label">ACME Email</div>
<input type="email" placeholder="admin@example.com" .value=${this.settings.acmeEmail} @input=${(e: Event) => this.updateSetting('acmeEmail', (e.target as HTMLInputElement).value)}>
<div class="form-hint">Email address for Let's Encrypt notifications.</div>
</div>
</div>
<div class="section">
<div class="section-header">
<div class="section-title">Network Settings</div>
<div class="section-subtitle">Configure network and proxy settings</div>
</div>
<div class="input-row">
<div class="form-group">
<div class="field-label">HTTP Port</div>
<input type="number" .value=${String(this.settings.httpPort)} @input=${(e: Event) => this.updateSetting('httpPort', parseInt((e.target as HTMLInputElement).value))}>
</div>
<div class="form-group">
<div class="field-label">HTTPS Port</div>
<input type="number" .value=${String(this.settings.httpsPort)} @input=${(e: Event) => this.updateSetting('httpsPort', parseInt((e.target as HTMLInputElement).value))}>
</div>
</div>
<div class="form-row">
<div class="form-label-group">
<span class="form-label">Force HTTPS</span>
<span class="form-hint">Redirect all HTTP traffic to HTTPS</span>
</div>
<div class="toggle-switch ${this.settings.forceHttps ? 'active' : ''}" @click=${() => this.toggleSetting('forceHttps')}></div>
</div>
</div>
<div class="section">
<div class="section-header">
<div class="section-title">Account</div>
<div class="section-subtitle">Manage your account settings</div>
</div>
<div class="form-group">
<div class="field-label">Current User</div>
<div style="font-size: 14px; color: ${cssManager.bdTheme('#18181b', '#fafafa')};">${this.currentUser || 'Unknown'}</div>
</div>
<div class="password-section">
<div class="password-title">Change Password</div>
<div class="password-fields">
<div>
<div class="field-label">Current Password</div>
<input type="password" id="currentPassword">
</div>
<div>
<div class="field-label">New Password</div>
<input type="password" id="newPassword">
</div>
<div>
<div class="field-label">Confirm Password</div>
<input type="password" id="confirmPassword">
</div>
<button class="button secondary" style="width: fit-content;" @click=${() => this.handleChangePassword()}>Update Password</button>
</div>
</div>
</div>
<div class="actions">
<button class="button secondary" @click=${() => this.handleReset()}>Reset</button>
<button class="button primary" @click=${() => this.handleSave()}>Save Settings</button>
</div>
`;
}
private toggleDarkMode() {
this.settings = { ...this.settings, darkMode: !this.settings.darkMode };
this.dispatchEvent(new CustomEvent('setting-change', { detail: { key: 'darkMode', value: this.settings.darkMode }, bubbles: true, composed: true }));
}
private toggleSetting(key: keyof ISettings) {
(this.settings as any)[key] = !(this.settings as any)[key];
this.settings = { ...this.settings };
}
private updateSetting(key: keyof ISettings, value: any) {
(this.settings as any)[key] = value;
this.settings = { ...this.settings };
}
private handleChangePassword() {
const currentPassword = (this.shadowRoot?.getElementById('currentPassword') as HTMLInputElement)?.value;
const newPassword = (this.shadowRoot?.getElementById('newPassword') as HTMLInputElement)?.value;
const confirmPassword = (this.shadowRoot?.getElementById('confirmPassword') as HTMLInputElement)?.value;
this.dispatchEvent(new CustomEvent('change-password', {
detail: { currentPassword, newPassword, confirmPassword },
bubbles: true,
composed: true
}));
}
private handleReset() {
this.dispatchEvent(new CustomEvent('reset', { bubbles: true, composed: true }));
}
private handleSave() {
this.dispatchEvent(new CustomEvent('save', { detail: this.settings, bubbles: true, composed: true }));
}
}

View File

@@ -0,0 +1,187 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
declare global {
interface HTMLElementTagNameMap {
'sz-stat-card': SzStatCard;
}
}
@customElement('sz-stat-card')
export class SzStatCard extends DeesElement {
public static demo = () => html`
<style>
.demo-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
padding: 24px;
max-width: 800px;
}
</style>
<div class="demo-grid">
<sz-stat-card
label="Total Services"
value="7"
icon="server"
></sz-stat-card>
<sz-stat-card
label="Running"
value="7"
icon="check"
variant="success"
></sz-stat-card>
<sz-stat-card
label="Stopped"
value="0"
icon="stop"
></sz-stat-card>
<sz-stat-card
label="Docker"
value="Running"
icon="container"
variant="success"
valueBadge
></sz-stat-card>
</div>
`;
@property({ type: String })
public accessor label: string = '';
@property({ type: String })
public accessor value: string = '';
@property({ type: String })
public accessor icon: string = '';
@property({ type: String })
public accessor variant: 'default' | 'success' | 'warning' | 'error' = 'default';
@property({ type: Boolean })
public accessor valueBadge: boolean = false;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
height: 100%;
}
.card {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
padding: 20px;
transition: all 200ms ease;
height: 100%;
box-sizing: border-box;
}
.card:hover {
border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0,0,0,0.05)', 'rgba(0,0,0,0.2)')};
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.label {
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.icon {
width: 20px;
height: 20px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.value {
font-size: 28px;
font-weight: 700;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
line-height: 1.2;
}
.value.success {
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.value.warning {
color: ${cssManager.bdTheme('#ca8a04', '#facc15')};
}
.value.error {
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.badge {
display: inline-flex;
align-items: center;
padding: 4px 12px;
border-radius: 9999px;
font-size: 14px;
font-weight: 500;
}
.badge.success {
background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')};
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.badge.warning {
background: ${cssManager.bdTheme('#fef9c3', 'rgba(250, 204, 21, 0.2)')};
color: ${cssManager.bdTheme('#ca8a04', '#facc15')};
}
.badge.error {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.badge.default {
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
`,
];
public render(): TemplateResult {
const valueClass = this.valueBadge ? `badge ${this.variant}` : `value ${this.variant}`;
return html`
<div class="card">
<div class="header">
<span class="label">${this.label}</span>
${this.renderIcon()}
</div>
<div class="${valueClass}">${this.value}</div>
</div>
`;
}
private renderIcon(): TemplateResult {
const icons: Record<string, TemplateResult> = {
server: html`<svg class="icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg>`,
check: html`<svg class="icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>`,
stop: html`<svg class="icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="10" y1="15" x2="10" y2="9"></line><line x1="14" y1="15" x2="14" y2="9"></line></svg>`,
container: html`<svg class="icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path></svg>`,
};
return icons[this.icon] || html``;
}
}

View File

@@ -0,0 +1,105 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
import './sz-stat-card.js';
declare global {
interface HTMLElementTagNameMap {
'sz-status-grid-cluster': SzStatusGridCluster;
}
}
export interface IClusterStats {
totalServices: number;
running: number;
stopped: number;
dockerStatus: 'running' | 'stopped';
}
@customElement('sz-status-grid-cluster')
export class SzStatusGridCluster extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 900px;">
<sz-status-grid-cluster
.stats=${{
totalServices: 7,
running: 7,
stopped: 0,
dockerStatus: 'running',
}}
></sz-status-grid-cluster>
</div>
`;
@property({ type: Object })
public accessor stats: IClusterStats = {
totalServices: 0,
running: 0,
stopped: 0,
dockerStatus: 'stopped',
};
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
}
.grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
align-items: stretch;
}
.grid > * {
height: 100%;
}
@media (min-width: 768px) {
.grid {
grid-template-columns: repeat(4, 1fr);
}
}
`,
];
public render(): TemplateResult {
return html`
<div class="grid">
<sz-stat-card
label="Total Services"
value="${this.stats.totalServices}"
icon="server"
></sz-stat-card>
<sz-stat-card
label="Running"
value="${this.stats.running}"
icon="check"
variant="success"
></sz-stat-card>
<sz-stat-card
label="Stopped"
value="${this.stats.stopped}"
icon="stop"
variant="${this.stats.stopped > 0 ? 'warning' : 'default'}"
></sz-stat-card>
<sz-stat-card
label="Docker"
value="${this.stats.dockerStatus === 'running' ? 'Running' : 'Stopped'}"
icon="container"
variant="${this.stats.dockerStatus === 'running' ? 'success' : 'error'}"
valueBadge
></sz-stat-card>
</div>
`;
}
}

View File

@@ -0,0 +1,88 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
import './sz-dns-ssl-card.js';
import './sz-quick-actions-card.js';
import type { IQuickAction } from './sz-quick-actions-card.js';
declare global {
interface HTMLElementTagNameMap {
'sz-status-grid-infra': SzStatusGridInfra;
}
}
@customElement('sz-status-grid-infra')
export class SzStatusGridInfra extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 1200px;">
<sz-status-grid-infra
dnsConfigured
acmeConfigured
.actions=${[
{ label: 'Deploy Service', icon: 'plus', primary: true },
{ label: 'View All Services' },
{ label: 'Platform Services' },
{ label: 'Manage Domains' },
]}
></sz-status-grid-infra>
</div>
`;
@property({ type: Boolean })
public accessor dnsConfigured: boolean = false;
@property({ type: Boolean })
public accessor acmeConfigured: boolean = false;
@property({ type: Array })
public accessor actions: IQuickAction[] = [];
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
}
.grid {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
align-items: stretch;
}
.grid > * {
height: 100%;
}
@media (min-width: 768px) {
.grid {
grid-template-columns: 1fr 2fr;
}
}
`,
];
public render(): TemplateResult {
return html`
<div class="grid">
<sz-dns-ssl-card
?dnsConfigured=${this.dnsConfigured}
?acmeConfigured=${this.acmeConfigured}
></sz-dns-ssl-card>
<sz-quick-actions-card
.actions=${this.actions}
@action-click=${(e: CustomEvent) => this.dispatchEvent(new CustomEvent('action-click', { detail: e.detail, bubbles: true, composed: true }))}
></sz-quick-actions-card>
</div>
`;
}
}

View File

@@ -0,0 +1,152 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
import './sz-traffic-card.js';
import './sz-reverse-proxy-card.js';
import './sz-certificates-card.js';
import type { ITrafficData } from './sz-traffic-card.js';
declare global {
interface HTMLElementTagNameMap {
'sz-status-grid-network': SzStatusGridNetwork;
}
}
export interface IProxyStatus {
httpPort: string;
httpsPort: string;
httpActive: boolean;
httpsActive: boolean;
routeCount: string;
}
export interface ICertificateStatus {
valid: number;
expiring: number;
expired: number;
}
@customElement('sz-status-grid-network')
export class SzStatusGridNetwork extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 1400px;">
<sz-status-grid-network
.traffic=${{
totalRequests: 125420,
requestsPerMinute: 847,
errorRate: 0.12,
avgResponseTime: 45,
statusDistribution: {
'2xx': 95.2,
'3xx': 2.1,
'4xx': 2.3,
'5xx': 0.4,
},
}}
.proxy=${{
httpPort: '80',
httpsPort: '443',
httpActive: true,
httpsActive: true,
routeCount: '12',
}}
.certificates=${{
valid: 8,
expiring: 2,
expired: 0,
}}
></sz-status-grid-network>
</div>
`;
@property({ type: Object })
public accessor traffic: ITrafficData = {
requests: 0,
errors: 0,
errorPercent: 0,
avgResponse: 0,
reqPerMin: 0,
status2xx: 0,
status3xx: 0,
status4xx: 0,
status5xx: 0,
};
@property({ type: Object })
public accessor proxy: IProxyStatus = {
httpPort: '80',
httpsPort: '443',
httpActive: false,
httpsActive: false,
routeCount: '0',
};
@property({ type: Object })
public accessor certificates: ICertificateStatus = {
valid: 0,
expiring: 0,
expired: 0,
};
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
}
.grid {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
align-items: stretch;
}
.grid > * {
height: 100%;
}
@media (min-width: 768px) {
.grid {
grid-template-columns: 1fr 1fr;
}
}
@media (min-width: 1024px) {
.grid {
grid-template-columns: 2fr 1fr 1fr;
}
}
`,
];
public render(): TemplateResult {
return html`
<div class="grid">
<sz-traffic-card
.data=${this.traffic}
></sz-traffic-card>
<sz-reverse-proxy-card
httpPort="${this.proxy.httpPort}"
httpsPort="${this.proxy.httpsPort}"
?httpActive=${this.proxy.httpActive}
?httpsActive=${this.proxy.httpsActive}
routeCount="${this.proxy.routeCount}"
></sz-reverse-proxy-card>
<sz-certificates-card
valid="${this.certificates.valid}"
expiring="${this.certificates.expiring}"
expired="${this.certificates.expired}"
></sz-certificates-card>
</div>
`;
}
}

View File

@@ -0,0 +1,99 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
import './sz-resource-usage-card.js';
import './sz-platform-services-card.js';
import type { IResourceUsage } from './sz-resource-usage-card.js';
import type { IPlatformService } from './sz-platform-services-card.js';
declare global {
interface HTMLElementTagNameMap {
'sz-status-grid-services': SzStatusGridServices;
}
}
@customElement('sz-status-grid-services')
export class SzStatusGridServices extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 1200px;">
<sz-status-grid-services
.resourceUsage=${{
cpu: { percentage: 45, label: 'CPU Usage' },
memory: { percentage: 62, label: 'Memory', used: '4.96 GB', total: '8 GB' },
network: { percentage: 23, label: 'Network I/O', rate: '1.2 MB/s' },
topConsumers: [
{ name: 'api-service', cpu: 25, memory: 512 },
{ name: 'web-frontend', cpu: 15, memory: 256 },
{ name: 'worker', cpu: 5, memory: 128 },
],
}}
.platformServices=${[
{ name: 'MongoDB', status: 'running' },
{ name: 'S3 Storage', status: 'running' },
{ name: 'ClickHouse', status: 'stopped' },
{ name: 'Redis Cache', status: 'running' },
]}
></sz-status-grid-services>
</div>
`;
@property({ type: Object })
public accessor resourceUsage: IResourceUsage = {
cpu: 0,
memoryUsed: '0 GB',
memoryTotal: '0 GB',
networkIn: '0 MB/s',
networkOut: '0 MB/s',
topConsumers: [],
};
@property({ type: Array })
public accessor platformServices: IPlatformService[] = [];
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
}
.grid {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
align-items: stretch;
}
.grid > * {
height: 100%;
}
@media (min-width: 768px) {
.grid {
grid-template-columns: 1fr 1fr;
}
}
`,
];
public render(): TemplateResult {
return html`
<div class="grid">
<sz-resource-usage-card
.usage=${this.resourceUsage}
></sz-resource-usage-card>
<sz-platform-services-card
.services=${this.platformServices}
></sz-platform-services-card>
</div>
`;
}
}

View File

@@ -0,0 +1,308 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
declare global {
interface HTMLElementTagNameMap {
'sz-tokens-view': SzTokensView;
}
}
export interface IToken {
id: string;
name: string;
type: 'global' | 'ci';
service?: string;
createdAt: string;
lastUsed?: string;
}
@customElement('sz-tokens-view')
export class SzTokensView extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 1000px;">
<sz-tokens-view
.globalTokens=${[
{ id: '1', name: 'CI/CD Pipeline', type: 'global', createdAt: '2024-01-15', lastUsed: '2024-01-20' },
{ id: '2', name: 'Development', type: 'global', createdAt: '2024-01-10' },
]}
.ciTokens=${[
{ id: '3', name: 'hello-world-ci', type: 'ci', service: 'hello-world', createdAt: '2024-01-18' },
{ id: '4', name: 'api-service-ci', type: 'ci', service: 'api-service', createdAt: '2024-01-12', lastUsed: '2024-01-19' },
]}
></sz-tokens-view>
</div>
`;
@property({ type: Array })
public accessor globalTokens: IToken[] = [];
@property({ type: Array })
public accessor ciTokens: IToken[] = [];
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
}
.section {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
margin-bottom: 24px;
overflow: hidden;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
}
.section-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.section-subtitle {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.create-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: ${cssManager.bdTheme('#18181b', '#fafafa')};
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('#fafafa', '#18181b')};
cursor: pointer;
transition: all 200ms ease;
}
.create-button:hover {
opacity: 0.9;
}
.token-list {
padding: 16px;
}
.token-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
border-radius: 6px;
margin-bottom: 8px;
}
.token-item:last-child {
margin-bottom: 0;
}
.token-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.token-name {
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.token-meta {
font-size: 12px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.token-service {
display: inline-flex;
align-items: center;
padding: 2px 8px;
background: ${cssManager.bdTheme('#dbeafe', 'rgba(59, 130, 246, 0.2)')};
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
border-radius: 4px;
font-size: 12px;
font-weight: 500;
margin-right: 8px;
}
.token-actions {
display: flex;
gap: 8px;
}
.action-button {
padding: 6px 12px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 4px;
font-size: 12px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
cursor: pointer;
transition: all 200ms ease;
}
.action-button:hover {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.action-button.delete {
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
border-color: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.3)')};
}
.action-button.delete:hover {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
}
.empty-state {
padding: 32px 16px;
text-align: center;
}
.empty-text {
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-bottom: 16px;
}
.empty-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 6px;
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
cursor: pointer;
transition: all 200ms ease;
}
.empty-button:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
`,
];
public render(): TemplateResult {
return html`
<div class="section">
<div class="section-header">
<div class="section-info">
<div class="section-title">Global Tokens</div>
<div class="section-subtitle">Tokens that can push images to multiple services</div>
</div>
<button class="create-button" @click=${() => this.handleCreate('global')}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Create Token
</button>
</div>
${this.globalTokens.length > 0 ? html`
<div class="token-list">
${this.globalTokens.map(token => this.renderToken(token))}
</div>
` : html`
<div class="empty-state">
<div class="empty-text">No global tokens created</div>
<button class="empty-button" @click=${() => this.handleCreate('global')}>Create Global Token</button>
</div>
`}
</div>
<div class="section">
<div class="section-header">
<div class="section-info">
<div class="section-title">CI Tokens (Service-specific)</div>
<div class="section-subtitle">Tokens tied to individual services for CI/CD pipelines</div>
</div>
<button class="create-button" @click=${() => this.handleCreate('ci')}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Create Token
</button>
</div>
${this.ciTokens.length > 0 ? html`
<div class="token-list">
${this.ciTokens.map(token => this.renderToken(token))}
</div>
` : html`
<div class="empty-state">
<div class="empty-text">No CI tokens created</div>
<button class="empty-button" @click=${() => this.handleCreate('ci')}>Create CI Token</button>
</div>
`}
</div>
`;
}
private renderToken(token: IToken): TemplateResult {
return html`
<div class="token-item">
<div class="token-info">
<div class="token-name">${token.name}</div>
<div class="token-meta">
${token.service ? html`<span class="token-service">${token.service}</span>` : ''}
Created ${token.createdAt}
${token.lastUsed ? html` · Last used ${token.lastUsed}` : ''}
</div>
</div>
<div class="token-actions">
<button class="action-button" @click=${() => this.handleCopy(token)}>Copy</button>
<button class="action-button" @click=${() => this.handleRegenerate(token)}>Regenerate</button>
<button class="action-button delete" @click=${() => this.handleDelete(token)}>Delete</button>
</div>
</div>
`;
}
private handleCreate(type: 'global' | 'ci') {
this.dispatchEvent(new CustomEvent('create', { detail: { type }, bubbles: true, composed: true }));
}
private handleCopy(token: IToken) {
this.dispatchEvent(new CustomEvent('copy', { detail: token, bubbles: true, composed: true }));
}
private handleRegenerate(token: IToken) {
this.dispatchEvent(new CustomEvent('regenerate', { detail: token, bubbles: true, composed: true }));
}
private handleDelete(token: IToken) {
this.dispatchEvent(new CustomEvent('delete', { detail: token, bubbles: true, composed: true }));
}
}

View File

@@ -0,0 +1,222 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
declare global {
interface HTMLElementTagNameMap {
'sz-traffic-card': SzTrafficCard;
}
}
export interface ITrafficData {
requests: number;
errors: number;
errorPercent: number;
avgResponse: number;
reqPerMin: number;
status2xx: number;
status3xx: number;
status4xx: number;
status5xx: number;
}
@customElement('sz-traffic-card')
export class SzTrafficCard extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 500px;">
<sz-traffic-card
.data=${{
requests: 1250,
errors: 15,
errorPercent: 1.2,
avgResponse: 145,
reqPerMin: 21,
status2xx: 85,
status3xx: 5,
status4xx: 8,
status5xx: 2,
}}
></sz-traffic-card>
</div>
`;
@property({ type: Object })
public accessor data: ITrafficData = {
requests: 0,
errors: 0,
errorPercent: 0,
avgResponse: 0,
reqPerMin: 0,
status2xx: 0,
status3xx: 0,
status4xx: 0,
status5xx: 0,
};
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
height: 100%;
}
.card {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
padding: 20px;
height: 100%;
box-sizing: border-box;
}
.header {
margin-bottom: 16px;
}
.title {
font-size: 16px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.subtitle {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-top: 2px;
}
.metrics {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 16px;
}
.metric {
display: flex;
flex-direction: column;
gap: 2px;
}
.metric-label {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.metric-value {
font-size: 18px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.status-bar-container {
padding-top: 12px;
border-top: 1px solid ${cssManager.bdTheme('#f4f4f5', '#27272a')};
}
.status-bar {
display: flex;
height: 8px;
border-radius: 4px;
overflow: hidden;
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
margin-bottom: 8px;
}
.status-segment {
height: 100%;
transition: width 300ms ease;
}
.status-2xx {
background: ${cssManager.bdTheme('#22c55e', '#22c55e')};
}
.status-3xx {
background: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
}
.status-4xx {
background: ${cssManager.bdTheme('#facc15', '#facc15')};
}
.status-5xx {
background: ${cssManager.bdTheme('#ef4444', '#ef4444')};
}
.status-legend {
display: flex;
justify-content: space-between;
}
.legend-item {
font-size: 12px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
`,
];
public render(): TemplateResult {
const total = this.data.status2xx + this.data.status3xx + this.data.status4xx + this.data.status5xx;
const p2xx = total > 0 ? (this.data.status2xx / total) * 100 : 0;
const p3xx = total > 0 ? (this.data.status3xx / total) * 100 : 0;
const p4xx = total > 0 ? (this.data.status4xx / total) * 100 : 0;
const p5xx = total > 0 ? (this.data.status5xx / total) * 100 : 0;
return html`
<div class="card">
<div class="header">
<div class="title">Traffic (Last Hour)</div>
<div class="subtitle">Request metrics from access logs</div>
</div>
<div class="metrics">
<div class="metric">
<span class="metric-label">Requests</span>
<span class="metric-value">${this.formatNumber(this.data.requests)}</span>
</div>
<div class="metric">
<span class="metric-label">Errors</span>
<span class="metric-value">${this.data.errors} (${this.data.errorPercent}%)</span>
</div>
<div class="metric">
<span class="metric-label">Avg Response</span>
<span class="metric-value">${this.data.avgResponse}ms</span>
</div>
<div class="metric">
<span class="metric-label">Req/min</span>
<span class="metric-value">${this.data.reqPerMin}</span>
</div>
</div>
<div class="status-bar-container">
<div class="status-bar">
<div class="status-segment status-2xx" style="width: ${p2xx}%"></div>
<div class="status-segment status-3xx" style="width: ${p3xx}%"></div>
<div class="status-segment status-4xx" style="width: ${p4xx}%"></div>
<div class="status-segment status-5xx" style="width: ${p5xx}%"></div>
</div>
<div class="status-legend">
<span class="legend-item">2xx</span>
<span class="legend-item">3xx</span>
<span class="legend-item">4xx</span>
<span class="legend-item">5xx</span>
</div>
</div>
</div>
`;
}
private formatNumber(num: number): string {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
return num.toString();
}
}

2
ts_web/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from './elements/index.js';
export * from './pages/index.js';

3
ts_web/pages/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from './mainpage.js';
export * from './sz-demo-app.js';
export * from './sz-demo-app-shell.js';

46
ts_web/pages/mainpage.ts Normal file
View File

@@ -0,0 +1,46 @@
import { html } from '@design.estate/dees-element';
export const mainpage = () => html`
<style>
body {
margin: 0;
padding: 0;
background: #f5f5f5;
}
.demo-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.demo-section {
background: white;
border-radius: 12px;
padding: 48px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1);
max-width: 600px;
width: 100%;
}
h1 {
margin: 0 0 32px 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 28px;
color: #1a1a1a;
}
.component-demo {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
<div class="demo-container">
<div class="demo-section">
<h1>Serve.Zone Catalog</h1>
<div class="component-demo">
<sz-hello></sz-hello>
<sz-hello text="Welcome to Serve.Zone!"></sz-hello>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,179 @@
import {
DeesElement,
customElement,
html,
css,
cssManager,
type TemplateResult,
} from '@design.estate/dees-element';
import type { DeesAppui } from '@design.estate/dees-catalog';
import '@design.estate/dees-catalog';
import '../elements/index.js';
declare global {
interface HTMLElementTagNameMap {
'sz-demo-app-shell': SzDemoAppShell;
}
}
@customElement('sz-demo-app-shell')
export class SzDemoAppShell extends DeesElement {
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
height: 100%;
width: 100%;
}
dees-appui {
height: 100%;
width: 100%;
}
`,
];
public render(): TemplateResult {
return html`
<dees-appui></dees-appui>
`;
}
async firstUpdated() {
const appui = this.shadowRoot?.querySelector('dees-appui') as DeesAppui;
if (!appui) return;
// Configure the application shell
appui.configure({
branding: {
logoIcon: 'lucide:Box',
logoText: 'serve.zone',
},
appBar: {
showSearch: true,
breadcrumbs: 'serve.zone',
menuItems: [
{
name: 'File',
action: async () => {},
submenu: [
{ name: 'New Service', shortcut: 'Cmd+N', action: async () => { console.log('New Service'); } },
{ name: 'Import Configuration', action: async () => { console.log('Import'); } },
{ name: 'Export Configuration', action: async () => { console.log('Export'); } },
{ divider: true },
{ name: 'Preferences', shortcut: 'Cmd+,', action: async () => { appui.navigateToView('settings'); } },
],
},
{
name: 'View',
action: async () => {},
submenu: [
{ name: 'Dashboard', shortcut: 'Cmd+1', action: async () => { appui.navigateToView('dashboard'); } },
{ name: 'Services', shortcut: 'Cmd+2', action: async () => { appui.navigateToView('services'); } },
{ name: 'Network', shortcut: 'Cmd+3', action: async () => { appui.navigateToView('network'); } },
{ divider: true },
{ name: 'Activity Log', shortcut: 'Cmd+Shift+A', action: async () => { appui.toggleActivityLog(); } },
{ name: 'Toggle Sidebar', shortcut: 'Cmd+B', action: async () => { appui.setMainMenuCollapsed(!(appui as any).mainmenuCollapsed); } },
],
},
{
name: 'Services',
action: async () => {},
submenu: [
{ name: 'Deploy New Service', action: async () => { console.log('Deploy'); } },
{ name: 'Start All', action: async () => { console.log('Start all'); } },
{ name: 'Stop All', action: async () => { console.log('Stop all'); } },
{ divider: true },
{ name: 'Garbage Collect', action: async () => { console.log('GC'); } },
],
},
{
name: 'Help',
action: async () => {},
submenu: [
{ name: 'Documentation', action: async () => { window.open('https://docs.serve.zone', '_blank'); } },
{ name: 'Release Notes', action: async () => { console.log('Release notes'); } },
{ divider: true },
{ name: 'About serve.zone', action: async () => { console.log('About'); } },
],
},
],
},
views: [
{
id: 'dashboard',
name: 'Dashboard',
iconName: 'lucide:LayoutDashboard',
content: 'sz-demo-view-dashboard',
},
{
id: 'services',
name: 'Services',
iconName: 'lucide:Server',
content: 'sz-demo-view-services',
},
{
id: 'network',
name: 'Network',
iconName: 'lucide:Network',
content: 'sz-demo-view-network',
},
{
id: 'registries',
name: 'Registries',
iconName: 'lucide:Archive',
content: 'sz-demo-view-registries',
},
{
id: 'tokens',
name: 'Tokens',
iconName: 'lucide:Key',
content: 'sz-demo-view-tokens',
},
{
id: 'settings',
name: 'Settings',
iconName: 'lucide:Settings',
content: 'sz-demo-view-settings',
},
],
mainMenu: {
sections: [
{
name: 'Overview',
views: ['dashboard'],
},
{
name: 'Infrastructure',
views: ['services', 'network', 'registries'],
},
{
name: 'Administration',
views: ['tokens', 'settings'],
},
],
},
defaultView: 'dashboard',
onViewChange: (viewId, view) => {
console.log('View changed to:', viewId, view);
},
});
// Set user profile
appui.setUser({
name: 'Admin User',
email: 'admin@serve.zone',
status: 'online',
});
// Set profile menu items
appui.setProfileMenuItems([
{ name: 'Profile', iconName: 'lucide:User', action: async () => { console.log('Profile'); } },
{ name: 'Preferences', iconName: 'lucide:SlidersHorizontal', action: async () => { console.log('Preferences'); } },
{ divider: true },
{ name: 'Sign Out', iconName: 'lucide:LogOut', action: async () => { console.log('Sign Out'); } },
]);
}
}

View File

@@ -0,0 +1,20 @@
import { html } from '@design.estate/dees-element';
import '../elements/index.js';
export const szDemoApp = () => html`
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
overflow: hidden;
}
sz-demo-app-shell {
display: block;
height: 100vh;
width: 100vw;
}
</style>
<sz-demo-app-shell></sz-demo-app-shell>
`;