2026-02-24 18:15:44 +00:00
|
|
|
import * as plugins from '../plugins.js';
|
|
|
|
|
import * as shared from './shared/index.js';
|
|
|
|
|
import * as appstate from '../appstate.js';
|
2026-03-03 11:57:41 +00:00
|
|
|
import * as interfaces from '../../ts_interfaces/index.js';
|
2026-02-24 18:15:44 +00:00
|
|
|
import {
|
|
|
|
|
DeesElement,
|
|
|
|
|
customElement,
|
|
|
|
|
html,
|
|
|
|
|
state,
|
|
|
|
|
css,
|
|
|
|
|
cssManager,
|
|
|
|
|
type TemplateResult,
|
|
|
|
|
} from '@design.estate/dees-element';
|
|
|
|
|
|
2026-03-03 11:57:41 +00:00
|
|
|
// ============================================================================
|
|
|
|
|
// Data transformation helpers
|
|
|
|
|
// Maps backend data shapes to @serve.zone/catalog component interfaces
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
function formatBytes(bytes: number): string {
|
|
|
|
|
if (!bytes || bytes === 0) return '0 B';
|
|
|
|
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
|
|
|
const k = 1024;
|
|
|
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
|
|
const value = bytes / Math.pow(k, i);
|
|
|
|
|
return `${value.toFixed(1)} ${units[i]}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseImageString(image: string): { repository: string; tag: string } {
|
|
|
|
|
const lastColon = image.lastIndexOf(':');
|
|
|
|
|
const lastSlash = image.lastIndexOf('/');
|
|
|
|
|
if (lastColon > lastSlash && lastColon > 0) {
|
|
|
|
|
return {
|
|
|
|
|
repository: image.substring(0, lastColon),
|
|
|
|
|
tag: image.substring(lastColon + 1),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return { repository: image, tag: 'latest' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mapStatus(status: string): 'running' | 'stopped' | 'starting' | 'error' {
|
|
|
|
|
switch (status) {
|
|
|
|
|
case 'running': return 'running';
|
|
|
|
|
case 'starting': return 'starting';
|
|
|
|
|
case 'failed': return 'error';
|
|
|
|
|
case 'stopped':
|
|
|
|
|
case 'stopping':
|
|
|
|
|
default: return 'stopped';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toServiceDetail(service: interfaces.data.IService) {
|
|
|
|
|
const parsed = parseImageString(service.image);
|
|
|
|
|
return {
|
|
|
|
|
name: service.name,
|
|
|
|
|
status: mapStatus(service.status),
|
|
|
|
|
image: service.image,
|
|
|
|
|
port: service.port,
|
|
|
|
|
domain: service.domain || null,
|
|
|
|
|
containerId: service.containerID || '',
|
|
|
|
|
created: service.createdAt ? new Date(service.createdAt).toLocaleString() : '-',
|
|
|
|
|
updated: service.updatedAt ? new Date(service.updatedAt).toLocaleString() : '-',
|
|
|
|
|
registry: service.useOneboxRegistry ? 'Onebox Registry' : (service.registry || 'Docker Hub'),
|
|
|
|
|
repository: service.registryRepository || parsed.repository,
|
|
|
|
|
tag: service.registryImageTag || parsed.tag,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toServiceStats(stats: interfaces.data.IContainerStats) {
|
|
|
|
|
return {
|
|
|
|
|
cpu: stats.cpuPercent,
|
|
|
|
|
memory: formatBytes(stats.memoryUsed),
|
|
|
|
|
memoryLimit: formatBytes(stats.memoryLimit),
|
|
|
|
|
networkIn: formatBytes(stats.networkRx),
|
|
|
|
|
networkOut: formatBytes(stats.networkTx),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseLogs(logs: any): Array<{ timestamp: string; message: string }> {
|
|
|
|
|
if (Array.isArray(logs)) {
|
|
|
|
|
return logs.map((entry: any) => ({
|
|
|
|
|
timestamp: entry.timestamp ? String(entry.timestamp) : '',
|
|
|
|
|
message: entry.message || String(entry),
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
if (typeof logs === 'string' && logs.trim()) {
|
|
|
|
|
return logs.split('\n').filter((line: string) => line.trim()).map((line: string) => {
|
|
|
|
|
const match = line.match(/^(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)\s+(.*)/);
|
|
|
|
|
if (match) {
|
|
|
|
|
return { timestamp: match[1], message: match[2] };
|
|
|
|
|
}
|
|
|
|
|
return { timestamp: '', message: line };
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const defaultStats = { cpu: 0, memory: '0 B', memoryLimit: '0 B', networkIn: '0 B', networkOut: '0 B' };
|
|
|
|
|
|
2026-02-24 18:15:44 +00:00
|
|
|
@customElement('ob-view-services')
|
|
|
|
|
export class ObViewServices extends DeesElement {
|
|
|
|
|
@state()
|
|
|
|
|
accessor servicesState: appstate.IServicesState = {
|
|
|
|
|
services: [],
|
|
|
|
|
currentService: null,
|
|
|
|
|
currentServiceLogs: [],
|
|
|
|
|
currentServiceStats: null,
|
|
|
|
|
platformServices: [],
|
|
|
|
|
currentPlatformService: null,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@state()
|
|
|
|
|
accessor backupsState: appstate.IBackupsState = {
|
|
|
|
|
backups: [],
|
|
|
|
|
schedules: [],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@state()
|
|
|
|
|
accessor currentView: 'list' | 'create' | 'detail' | 'backups' | 'platform-detail' = 'list';
|
|
|
|
|
|
|
|
|
|
@state()
|
|
|
|
|
accessor selectedServiceName: string = '';
|
|
|
|
|
|
|
|
|
|
@state()
|
|
|
|
|
accessor selectedPlatformType: string = '';
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
|
|
|
|
|
const servicesSub = appstate.servicesStatePart
|
|
|
|
|
.select((s) => s)
|
|
|
|
|
.subscribe((newState) => {
|
|
|
|
|
this.servicesState = newState;
|
|
|
|
|
});
|
|
|
|
|
this.rxSubscriptions.push(servicesSub);
|
|
|
|
|
|
|
|
|
|
const backupsSub = appstate.backupsStatePart
|
|
|
|
|
.select((s) => s)
|
|
|
|
|
.subscribe((newState) => {
|
|
|
|
|
this.backupsState = newState;
|
|
|
|
|
});
|
|
|
|
|
this.rxSubscriptions.push(backupsSub);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static styles = [
|
|
|
|
|
cssManager.defaultStyles,
|
|
|
|
|
shared.viewHostCss,
|
|
|
|
|
css``,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
async connectedCallback() {
|
|
|
|
|
super.connectedCallback();
|
|
|
|
|
await Promise.all([
|
|
|
|
|
appstate.servicesStatePart.dispatchAction(appstate.fetchServicesAction, null),
|
|
|
|
|
appstate.servicesStatePart.dispatchAction(appstate.fetchPlatformServicesAction, null),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public render(): TemplateResult {
|
|
|
|
|
switch (this.currentView) {
|
|
|
|
|
case 'create':
|
|
|
|
|
return this.renderCreateView();
|
|
|
|
|
case 'detail':
|
|
|
|
|
return this.renderDetailView();
|
|
|
|
|
case 'backups':
|
|
|
|
|
return this.renderBackupsView();
|
|
|
|
|
case 'platform-detail':
|
|
|
|
|
return this.renderPlatformDetailView();
|
|
|
|
|
default:
|
|
|
|
|
return this.renderListView();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderListView(): TemplateResult {
|
2026-03-03 11:57:41 +00:00
|
|
|
const mappedServices = this.servicesState.services.map((s) => ({
|
|
|
|
|
name: s.name,
|
|
|
|
|
image: s.image,
|
|
|
|
|
domain: s.domain || null,
|
|
|
|
|
status: mapStatus(s.status),
|
|
|
|
|
}));
|
2026-02-24 18:15:44 +00:00
|
|
|
return html`
|
|
|
|
|
<ob-sectionheading>Services</ob-sectionheading>
|
|
|
|
|
<sz-services-list-view
|
2026-03-03 11:57:41 +00:00
|
|
|
.services=${mappedServices}
|
2026-02-24 18:15:44 +00:00
|
|
|
@service-click=${(e: CustomEvent) => {
|
|
|
|
|
this.selectedServiceName = e.detail.name || e.detail.service?.name;
|
|
|
|
|
appstate.servicesStatePart.dispatchAction(appstate.fetchServiceAction, {
|
|
|
|
|
name: this.selectedServiceName,
|
|
|
|
|
});
|
|
|
|
|
appstate.servicesStatePart.dispatchAction(appstate.fetchServiceLogsAction, {
|
|
|
|
|
name: this.selectedServiceName,
|
|
|
|
|
});
|
2026-03-03 11:57:41 +00:00
|
|
|
appstate.servicesStatePart.dispatchAction(appstate.fetchServiceStatsAction, {
|
|
|
|
|
name: this.selectedServiceName,
|
|
|
|
|
});
|
2026-02-24 18:15:44 +00:00
|
|
|
this.currentView = 'detail';
|
|
|
|
|
}}
|
|
|
|
|
@service-action=${(e: CustomEvent) => this.handleServiceAction(e)}
|
|
|
|
|
></sz-services-list-view>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderCreateView(): TemplateResult {
|
|
|
|
|
return html`
|
|
|
|
|
<ob-sectionheading>Create Service</ob-sectionheading>
|
|
|
|
|
<sz-service-create-view
|
|
|
|
|
.registries=${[]}
|
|
|
|
|
@create-service=${async (e: CustomEvent) => {
|
|
|
|
|
await appstate.servicesStatePart.dispatchAction(appstate.createServiceAction, {
|
|
|
|
|
config: e.detail,
|
|
|
|
|
});
|
|
|
|
|
this.currentView = 'list';
|
|
|
|
|
}}
|
|
|
|
|
@cancel=${() => {
|
|
|
|
|
this.currentView = 'list';
|
|
|
|
|
}}
|
|
|
|
|
></sz-service-create-view>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderDetailView(): TemplateResult {
|
2026-03-03 11:57:41 +00:00
|
|
|
const service = this.servicesState.currentService;
|
|
|
|
|
const transformedService = service ? toServiceDetail(service) : null;
|
|
|
|
|
const transformedStats = this.servicesState.currentServiceStats
|
|
|
|
|
? toServiceStats(this.servicesState.currentServiceStats)
|
|
|
|
|
: defaultStats;
|
|
|
|
|
const transformedLogs = parseLogs(this.servicesState.currentServiceLogs);
|
|
|
|
|
|
2026-02-24 18:15:44 +00:00
|
|
|
return html`
|
|
|
|
|
<ob-sectionheading>Service Details</ob-sectionheading>
|
|
|
|
|
<sz-service-detail-view
|
2026-03-03 11:57:41 +00:00
|
|
|
.service=${transformedService}
|
|
|
|
|
.logs=${transformedLogs}
|
|
|
|
|
.stats=${transformedStats}
|
2026-02-24 18:15:44 +00:00
|
|
|
@back=${() => {
|
|
|
|
|
this.currentView = 'list';
|
|
|
|
|
}}
|
|
|
|
|
@service-action=${(e: CustomEvent) => this.handleServiceAction(e)}
|
|
|
|
|
></sz-service-detail-view>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderBackupsView(): TemplateResult {
|
|
|
|
|
return html`
|
|
|
|
|
<ob-sectionheading>Backups</ob-sectionheading>
|
|
|
|
|
<sz-services-backups-view
|
|
|
|
|
.schedules=${this.backupsState.schedules}
|
|
|
|
|
.backups=${this.backupsState.backups}
|
|
|
|
|
@create-schedule=${(e: CustomEvent) => {
|
|
|
|
|
appstate.backupsStatePart.dispatchAction(appstate.createScheduleAction, {
|
|
|
|
|
config: e.detail,
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
@run-now=${(e: CustomEvent) => {
|
|
|
|
|
appstate.backupsStatePart.dispatchAction(appstate.triggerScheduleAction, {
|
|
|
|
|
scheduleId: e.detail.scheduleId,
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
@delete-backup=${(e: CustomEvent) => {
|
|
|
|
|
appstate.backupsStatePart.dispatchAction(appstate.deleteBackupAction, {
|
|
|
|
|
backupId: e.detail.backupId,
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
></sz-services-backups-view>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderPlatformDetailView(): TemplateResult {
|
|
|
|
|
const platformService = this.servicesState.platformServices.find(
|
|
|
|
|
(ps) => ps.type === this.selectedPlatformType,
|
|
|
|
|
);
|
|
|
|
|
return html`
|
|
|
|
|
<ob-sectionheading>Platform Service</ob-sectionheading>
|
|
|
|
|
<sz-platform-service-detail-view
|
|
|
|
|
.service=${platformService
|
|
|
|
|
? {
|
|
|
|
|
id: platformService.type,
|
|
|
|
|
name: platformService.displayName,
|
|
|
|
|
type: platformService.type,
|
|
|
|
|
status: platformService.status,
|
|
|
|
|
version: '',
|
|
|
|
|
host: 'localhost',
|
|
|
|
|
port: 0,
|
|
|
|
|
config: {},
|
|
|
|
|
}
|
|
|
|
|
: null}
|
|
|
|
|
.logs=${[]}
|
|
|
|
|
@start=${() => {
|
|
|
|
|
appstate.servicesStatePart.dispatchAction(appstate.startPlatformServiceAction, {
|
|
|
|
|
serviceType: this.selectedPlatformType as any,
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
@stop=${() => {
|
|
|
|
|
appstate.servicesStatePart.dispatchAction(appstate.stopPlatformServiceAction, {
|
|
|
|
|
serviceType: this.selectedPlatformType as any,
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
></sz-platform-service-detail-view>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleServiceAction(e: CustomEvent) {
|
|
|
|
|
const action = e.detail.action;
|
|
|
|
|
const name = e.detail.service?.name || e.detail.name || this.selectedServiceName;
|
|
|
|
|
switch (action) {
|
|
|
|
|
case 'start':
|
|
|
|
|
await appstate.servicesStatePart.dispatchAction(appstate.startServiceAction, { name });
|
|
|
|
|
break;
|
|
|
|
|
case 'stop':
|
|
|
|
|
await appstate.servicesStatePart.dispatchAction(appstate.stopServiceAction, { name });
|
|
|
|
|
break;
|
|
|
|
|
case 'restart':
|
|
|
|
|
await appstate.servicesStatePart.dispatchAction(appstate.restartServiceAction, { name });
|
|
|
|
|
break;
|
|
|
|
|
case 'delete':
|
|
|
|
|
await appstate.servicesStatePart.dispatchAction(appstate.deleteServiceAction, { name });
|
|
|
|
|
this.currentView = 'list';
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|