feat(services): add platform service navigation and stats in the services UI

This commit is contained in:
2026-03-16 11:45:56 +00:00
parent 59ed7233bd
commit 3f2cd074ce
9 changed files with 159 additions and 17 deletions

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/onebox',
version: '1.15.3',
version: '1.16.0',
description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers'
}

View File

@@ -26,6 +26,7 @@ export interface IServicesState {
currentServiceStats: interfaces.data.IContainerStats | null;
platformServices: interfaces.data.IPlatformService[];
currentPlatformService: interfaces.data.IPlatformService | null;
currentPlatformServiceStats: interfaces.data.IContainerStats | null;
}
export interface INetworkState {
@@ -88,6 +89,7 @@ export const servicesStatePart = await appState.getStatePart<IServicesState>(
currentServiceStats: null,
platformServices: [],
currentPlatformService: null,
currentPlatformServiceStats: null,
},
'soft',
);
@@ -476,6 +478,25 @@ export const stopPlatformServiceAction = servicesStatePart.createAction<{
}
});
export const fetchPlatformServiceStatsAction = servicesStatePart.createAction<{
serviceType: interfaces.data.TPlatformServiceType;
}>(async (statePartArg, dataArg) => {
const context = getActionContext();
try {
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetPlatformServiceStats
>('/typedrequest', 'getPlatformServiceStats');
const response = await typedRequest.fire({
identity: context.identity!,
serviceType: dataArg.serviceType,
});
return { ...statePartArg.getState(), currentPlatformServiceStats: response.stats };
} catch (err) {
console.error('Failed to fetch platform service stats:', err);
return { ...statePartArg.getState(), currentPlatformServiceStats: null };
}
});
// ============================================================================
// Network Actions
// ============================================================================

View File

@@ -24,6 +24,7 @@ export class ObViewDashboard extends DeesElement {
currentServiceStats: null,
platformServices: [],
currentPlatformService: null,
currentPlatformServiceStats: null,
};
@state()
@@ -149,6 +150,7 @@ export class ObViewDashboard extends DeesElement {
],
}}
@action-click=${(e: CustomEvent) => this.handleQuickAction(e)}
@service-click=${(e: CustomEvent) => this.handlePlatformServiceClick(e)}
></sz-dashboard-view>
`;
}
@@ -161,4 +163,21 @@ export class ObViewDashboard extends DeesElement {
appstate.uiStatePart.dispatchAction(appstate.setActiveViewAction, { view: 'network' });
}
}
private handlePlatformServiceClick(e: CustomEvent) {
// Find the platform service type from the click event
const name = e.detail?.name;
const ps = this.servicesState.platformServices.find(
(p) => p.displayName === name,
);
if (ps) {
// Navigate to services tab — the ObViewServices component will pick up the type
// Store the selected platform type so the services view can open it
appstate.servicesStatePart.setState({
...appstate.servicesStatePart.getState(),
currentPlatformService: ps,
});
appstate.uiStatePart.dispatchAction(appstate.setActiveViewAction, { view: 'services' });
}
}
}

View File

@@ -107,6 +107,7 @@ export class ObViewServices extends DeesElement {
currentServiceStats: null,
platformServices: [],
currentPlatformService: null,
currentPlatformServiceStats: null,
};
@state()
@@ -154,6 +155,18 @@ export class ObViewServices extends DeesElement {
appstate.servicesStatePart.dispatchAction(appstate.fetchServicesAction, null),
appstate.servicesStatePart.dispatchAction(appstate.fetchPlatformServicesAction, null),
]);
// If a platform service was selected from the dashboard, navigate to its detail
const state = appstate.servicesStatePart.getState();
if (state.currentPlatformService) {
const type = state.currentPlatformService.type;
// Clear the selection so it doesn't persist on next visit
appstate.servicesStatePart.setState({
...appstate.servicesStatePart.getState(),
currentPlatformService: null,
});
this.navigateToPlatformDetail(type);
}
}
public render(): TemplateResult {
@@ -178,6 +191,12 @@ export class ObViewServices extends DeesElement {
domain: s.domain || null,
status: mapStatus(s.status),
}));
const mappedPlatformServices = this.servicesState.platformServices.map((ps) => ({
name: ps.displayName,
status: ps.status === 'running' ? `Running` : ps.status,
running: ps.status === 'running',
type: ps.type,
}));
return html`
<ob-sectionheading>Services</ob-sectionheading>
<sz-services-list-view
@@ -197,6 +216,20 @@ export class ObViewServices extends DeesElement {
}}
@service-action=${(e: CustomEvent) => this.handleServiceAction(e)}
></sz-services-list-view>
<ob-sectionheading style="margin-top: 32px;">Platform Services</ob-sectionheading>
<div style="max-width: 500px;">
<sz-platform-services-card
.services=${mappedPlatformServices}
@service-click=${(e: CustomEvent) => {
const type = e.detail.type || this.servicesState.platformServices.find(
(ps) => ps.displayName === e.detail.name,
)?.type;
if (type) {
this.navigateToPlatformDetail(type);
}
}}
></sz-platform-services-card>
</div>
`;
}
@@ -206,8 +239,26 @@ export class ObViewServices extends DeesElement {
<sz-service-create-view
.registries=${[]}
@create-service=${async (e: CustomEvent) => {
const formConfig = e.detail;
const serviceConfig: interfaces.data.IServiceCreate = {
name: formConfig.name,
image: formConfig.image,
port: formConfig.ports?.[0]?.containerPort
? parseInt(formConfig.ports[0].containerPort, 10)
: 80,
envVars: formConfig.envVars?.reduce(
(acc: Record<string, string>, ev: { key: string; value: string }) => {
if (ev.key) acc[ev.key] = ev.value;
return acc;
},
{} as Record<string, string>,
),
enableMongoDB: formConfig.enableMongoDB || false,
enableS3: formConfig.enableS3 || false,
enableClickHouse: formConfig.enableClickHouse || false,
};
await appstate.servicesStatePart.dispatchAction(appstate.createServiceAction, {
config: e.detail,
config: serviceConfig,
});
this.currentView = 'list';
}}
@@ -265,10 +316,29 @@ export class ObViewServices extends DeesElement {
`;
}
private navigateToPlatformDetail(type: string): void {
this.selectedPlatformType = type;
// Fetch stats for this platform service
appstate.servicesStatePart.dispatchAction(appstate.fetchPlatformServiceStatsAction, {
serviceType: type as interfaces.data.TPlatformServiceType,
});
this.currentView = 'platform-detail';
}
private renderPlatformDetailView(): TemplateResult {
const platformService = this.servicesState.platformServices.find(
(ps) => ps.type === this.selectedPlatformType,
);
const stats = this.servicesState.currentPlatformServiceStats;
const metrics = stats
? {
cpu: Math.round(stats.cpuPercent),
memory: Math.round(stats.memoryPercent),
storage: 0,
connections: 0,
}
: undefined;
return html`
<ob-sectionheading>Platform Service</ob-sectionheading>
<sz-platform-service-detail-view
@@ -277,22 +347,45 @@ export class ObViewServices extends DeesElement {
id: platformService.type,
name: platformService.displayName,
type: platformService.type,
status: platformService.status,
status: platformService.status === 'running'
? 'running'
: platformService.status === 'failed'
? 'error'
: 'stopped',
version: '',
host: 'localhost',
port: 0,
config: {},
metrics,
}
: null}
.logs=${[]}
@start=${() => {
appstate.servicesStatePart.dispatchAction(appstate.startPlatformServiceAction, {
serviceType: this.selectedPlatformType as any,
@back=${() => {
this.currentView = 'list';
}}
@start=${async () => {
await appstate.servicesStatePart.dispatchAction(appstate.startPlatformServiceAction, {
serviceType: this.selectedPlatformType as interfaces.data.TPlatformServiceType,
});
// Refresh stats after starting
appstate.servicesStatePart.dispatchAction(appstate.fetchPlatformServiceStatsAction, {
serviceType: this.selectedPlatformType as interfaces.data.TPlatformServiceType,
});
}}
@stop=${() => {
appstate.servicesStatePart.dispatchAction(appstate.stopPlatformServiceAction, {
serviceType: this.selectedPlatformType as any,
@stop=${async () => {
await appstate.servicesStatePart.dispatchAction(appstate.stopPlatformServiceAction, {
serviceType: this.selectedPlatformType as interfaces.data.TPlatformServiceType,
});
}}
@restart=${async () => {
await appstate.servicesStatePart.dispatchAction(appstate.stopPlatformServiceAction, {
serviceType: this.selectedPlatformType as interfaces.data.TPlatformServiceType,
});
await appstate.servicesStatePart.dispatchAction(appstate.startPlatformServiceAction, {
serviceType: this.selectedPlatformType as interfaces.data.TPlatformServiceType,
});
appstate.servicesStatePart.dispatchAction(appstate.fetchPlatformServiceStatsAction, {
serviceType: this.selectedPlatformType as interfaces.data.TPlatformServiceType,
});
}}
></sz-platform-service-detail-view>