850 lines
26 KiB
TypeScript
850 lines
26 KiB
TypeScript
import {
|
|
DeesElement,
|
|
customElement,
|
|
html,
|
|
css,
|
|
cssManager,
|
|
property,
|
|
state,
|
|
type TemplateResult,
|
|
} from '@design.estate/dees-element';
|
|
|
|
import type { IExecutionEnvironment } from '@design.estate/dees-catalog';
|
|
|
|
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>
|
|
`;
|
|
|
|
public static demoGroups = ['Services'];
|
|
|
|
@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;
|
|
|
|
@property({ type: Object })
|
|
public accessor workspaceEnvironment: IExecutionEnvironment | null = null;
|
|
|
|
@property({ type: String })
|
|
public accessor workspacePath: string = '/';
|
|
|
|
@state()
|
|
private accessor currentView: 'details' | 'workspace' = 'details';
|
|
|
|
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;
|
|
}
|
|
|
|
:host(.workspace-mode) {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
}
|
|
|
|
.workspace-wrapper {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
}
|
|
|
|
.workspace-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 12px 16px;
|
|
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
|
|
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.workspace-back-link {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
|
|
cursor: pointer;
|
|
transition: color 200ms ease;
|
|
}
|
|
|
|
.workspace-back-link:hover {
|
|
color: ${cssManager.bdTheme('#1d4ed8', '#93c5fd')};
|
|
}
|
|
|
|
.workspace-service-name {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
|
}
|
|
|
|
.workspace-status-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.workspace-status-badge.running {
|
|
background: ${cssManager.bdTheme('#22c55e', '#22c55e')};
|
|
}
|
|
|
|
.workspace-status-badge.stopped {
|
|
background: ${cssManager.bdTheme('#ef4444', '#ef4444')};
|
|
}
|
|
|
|
.workspace-container {
|
|
position: relative;
|
|
flex: 1;
|
|
min-height: 0;
|
|
}
|
|
`,
|
|
];
|
|
|
|
public render(): TemplateResult {
|
|
if (this.currentView === 'workspace') {
|
|
return this.renderWorkspaceView();
|
|
}
|
|
return this.renderDetailsView();
|
|
}
|
|
|
|
private renderWorkspaceView(): TemplateResult {
|
|
return html`
|
|
<div class="workspace-wrapper">
|
|
<div class="workspace-header">
|
|
<div class="workspace-back-link" @click=${() => this.handleCloseWorkspace()}>
|
|
<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 Details
|
|
</div>
|
|
<span class="workspace-service-name">${this.service.name}</span>
|
|
<span class="workspace-status-badge ${this.service.status}"></span>
|
|
</div>
|
|
<div class="workspace-container">
|
|
<dees-workspace .executionEnvironment=${this.workspaceEnvironment}></dees-workspace>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderDetailsView(): 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" @click=${() => this.handleOpenWorkspace()}>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display: inline; vertical-align: middle; margin-right: 4px;">
|
|
<polyline points="4 17 10 11 4 5"></polyline>
|
|
<line x1="12" y1="19" x2="20" y2="19"></line>
|
|
</svg>
|
|
Open Workspace
|
|
</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 }));
|
|
}
|
|
|
|
private handleOpenWorkspace() {
|
|
if (this.workspaceEnvironment) {
|
|
this.currentView = 'workspace';
|
|
this.classList.add('workspace-mode');
|
|
} else {
|
|
this.dispatchEvent(new CustomEvent('request-workspace', {
|
|
detail: { service: this.service },
|
|
bubbles: true,
|
|
composed: true,
|
|
}));
|
|
}
|
|
}
|
|
|
|
private handleCloseWorkspace() {
|
|
this.currentView = 'details';
|
|
this.classList.remove('workspace-mode');
|
|
}
|
|
|
|
updated(changedProperties: Map<string, any>) {
|
|
super.updated(changedProperties);
|
|
if (changedProperties.has('workspaceEnvironment') && this.workspaceEnvironment && this.currentView === 'details') {
|
|
this.currentView = 'workspace';
|
|
this.classList.add('workspace-mode');
|
|
}
|
|
}
|
|
}
|