393 lines
13 KiB
TypeScript
393 lines
13 KiB
TypeScript
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>
|
|
`;
|
|
|
|
public static demoGroups = ['Services'];
|
|
|
|
@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 }));
|
|
}
|
|
}
|