feat(appstore): add remote app store templates with service upgrades and Redis/MariaDB platform support

This commit is contained in:
2026-03-21 19:36:25 +00:00
parent c0f9f979c7
commit 22f34e7de5
35 changed files with 2496 additions and 254 deletions

View File

@@ -142,6 +142,12 @@ export class ObViewServices extends DeesElement {
@state()
accessor pendingTemplate: any = null;
@state()
accessor appStoreState: appstate.IAppStoreState = {
apps: [],
upgradeableServices: [],
};
constructor() {
super();
@@ -159,7 +165,12 @@ export class ObViewServices extends DeesElement {
});
this.rxSubscriptions.push(backupsSub);
// No subscription needed — pendingAppTemplate is checked in render()
const appStoreSub = appstate.appStoreStatePart
.select((s) => s)
.subscribe((newState) => {
this.appStoreState = newState;
});
this.rxSubscriptions.push(appStoreSub);
}
public static styles = [
@@ -215,6 +226,7 @@ export class ObViewServices extends DeesElement {
await Promise.all([
appstate.servicesStatePart.dispatchAction(appstate.fetchServicesAction, null),
appstate.servicesStatePart.dispatchAction(appstate.fetchPlatformServicesAction, null),
appstate.appStoreStatePart.dispatchAction(appstate.fetchUpgradeableServicesAction, null),
]);
// If a platform service was selected from the dashboard, navigate to its detail
@@ -230,20 +242,6 @@ export class ObViewServices extends DeesElement {
}
updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
// Check for pending app template from the App Store after each update
const uiState = appstate.uiStatePart.getState();
if (uiState.pendingAppTemplate && !this.pendingTemplate) {
this.pendingTemplate = uiState.pendingAppTemplate;
appstate.uiStatePart.setState({
...appstate.uiStatePart.getState(),
pendingAppTemplate: undefined,
});
this.currentView = 'create';
}
}
public render(): TemplateResult {
switch (this.currentView) {
case 'create':
@@ -277,7 +275,14 @@ export class ObViewServices extends DeesElement {
default: return status;
}
};
const mappedPlatformServices = this.servicesState.platformServices.map((ps) => ({
// Split platform services into active (running or core) and inactive (not in use)
const activePlatformServices = this.servicesState.platformServices.filter(
(ps) => ps.status === 'running' || ps.status === 'starting' || ps.status === 'stopping' || ps.isCore,
);
const inactivePlatformServices = this.servicesState.platformServices.filter(
(ps) => !ps.isCore && (ps.status === 'not-deployed' || ps.status === 'stopped' || ps.status === 'failed'),
);
const mappedActivePlatformServices = activePlatformServices.map((ps) => ({
name: ps.displayName,
status: displayStatus(ps.status),
running: ps.status === 'running',
@@ -313,17 +318,45 @@ export class ObViewServices extends DeesElement {
></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>
${mappedActivePlatformServices.length > 0 ? html`
<sz-platform-services-card
.services=${mappedActivePlatformServices}
@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>
` : ''}
${inactivePlatformServices.length > 0 ? html`
<div style="
background: var(--ci-shade-1, #09090b);
border: 1px solid var(--ci-shade-2, #27272a);
border-radius: 8px;
padding: 20px;
margin-top: ${mappedActivePlatformServices.length > 0 ? '12px' : '0'};
opacity: 0.5;
">
<div style="font-size: 13px; color: var(--ci-shade-4, #71717a); margin-bottom: 12px;">Available — not in use</div>
<div style="display: flex; flex-direction: column; gap: 12px;">
${inactivePlatformServices.map((ps) => html`
<div
style="display: flex; justify-content: space-between; align-items: center; padding: 8px 0; cursor: pointer; transition: opacity 200ms ease;"
@click=${() => this.navigateToPlatformDetail(ps.type)}
>
<div style="display: flex; align-items: center; gap: 10px;">
<div style="width: 8px; height: 8px; border-radius: 50%; background: var(--ci-shade-3, #3f3f46); flex-shrink: 0;"></div>
<span style="font-size: 14px; font-weight: 500; color: var(--ci-shade-4, #71717a);">${ps.displayName}</span>
</div>
<span style="font-size: 13px; color: var(--ci-shade-3, #3f3f46);">${displayStatus(ps.status)}</span>
</div>
`)}
</div>
</div>
` : ''}
</div>
`;
}
@@ -344,6 +377,8 @@ export class ObViewServices extends DeesElement {
enableMongoDB: template.enableMongoDB || false,
enableS3: template.enableS3 || false,
enableClickHouse: template.enableClickHouse || false,
enableRedis: template.enableRedis || false,
enableMariaDB: template.enableMariaDB || false,
};
await appstate.servicesStatePart.dispatchAction(appstate.createServiceAction, {
config: serviceConfig,
@@ -368,12 +403,14 @@ export class ObViewServices extends DeesElement {
<div><span style="color: var(--ci-shade-5, #a1a1aa);">Service Name:</span> <strong>${t.id}</strong></div>
<div><span style="color: var(--ci-shade-5, #a1a1aa);">Category:</span> <strong>${t.category}</strong></div>
</div>
${t.enableMongoDB || t.enableS3 || t.enableClickHouse ? html`
${t.enableMongoDB || t.enableS3 || t.enableClickHouse || t.enableRedis || t.enableMariaDB ? html`
<div style="margin-top: 12px; font-size: 13px; color: var(--ci-shade-5, #a1a1aa);">
Platform Services:
${t.enableMongoDB ? html`<span style="margin-right: 8px;">MongoDB</span>` : ''}
${t.enableS3 ? html`<span style="margin-right: 8px;">S3</span>` : ''}
${t.enableClickHouse ? html`<span>ClickHouse</span>` : ''}
${t.enableClickHouse ? html`<span style="margin-right: 8px;">ClickHouse</span>` : ''}
${t.enableRedis ? html`<span style="margin-right: 8px;">Redis</span>` : ''}
${t.enableMariaDB ? html`<span style="margin-right: 8px;">MariaDB</span>` : ''}
</div>
` : ''}
</div>
@@ -407,6 +444,8 @@ export class ObViewServices extends DeesElement {
enableMongoDB: formConfig.enableMongoDB || false,
enableS3: formConfig.enableS3 || false,
enableClickHouse: formConfig.enableClickHouse || false,
enableRedis: formConfig.enableRedis || false,
enableMariaDB: formConfig.enableMariaDB || false,
};
await appstate.servicesStatePart.dispatchAction(appstate.createServiceAction, {
config: serviceConfig,
@@ -428,8 +467,49 @@ export class ObViewServices extends DeesElement {
: defaultStats;
const transformedLogs = parseLogs(this.servicesState.currentServiceLogs);
// Check if this service has an available upgrade
const upgradeInfo = service
? this.appStoreState.upgradeableServices.find((u) => u.serviceName === service.name)
: null;
return html`
<ob-sectionheading>Service Details</ob-sectionheading>
${upgradeInfo ? html`
<div style="
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(139, 92, 246, 0.1));
border: 1px solid rgba(59, 130, 246, 0.3);
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: center;
">
<div>
<div style="font-size: 14px; font-weight: 600; color: var(--ci-shade-7, #e4e4e7);">
Update available: v${upgradeInfo.currentVersion} &rarr; v${upgradeInfo.latestVersion}
</div>
<div style="font-size: 12px; color: var(--ci-shade-4, #71717a); margin-top: 4px;">
${upgradeInfo.hasMigration ? 'Migration script available' : 'Config-only upgrade'}
</div>
</div>
<button
class="deploy-button"
style="padding: 8px 16px; font-size: 13px;"
@click=${async () => {
await appstate.appStoreStatePart.dispatchAction(appstate.upgradeServiceAction, {
serviceName: upgradeInfo.serviceName,
targetVersion: upgradeInfo.latestVersion,
});
// Refresh service data
appstate.servicesStatePart.dispatchAction(appstate.fetchServiceAction, {
name: upgradeInfo.serviceName,
});
appstate.servicesStatePart.dispatchAction(appstate.fetchServicesAction, null);
}}
>Upgrade</button>
</div>
` : ''}
<sz-service-detail-view
.service=${transformedService}
.logs=${transformedLogs}
@@ -530,6 +610,8 @@ export class ObViewServices extends DeesElement {
minio: { host: 'onebox-minio', port: 9000, version: 'latest', config: { consolePort: 9001, region: 'us-east-1' } },
clickhouse: { host: 'onebox-clickhouse', port: 8123, version: 'latest', config: { nativePort: 9000, httpPort: 8123 } },
caddy: { host: 'onebox-caddy', port: 80, version: '2-alpine', config: { httpsPort: 443, adminApi: 2019 } },
mariadb: { host: 'onebox-mariadb', port: 3306, version: '11', config: { engine: 'InnoDB', authEnabled: true } },
redis: { host: 'onebox-redis', port: 6379, version: '7-alpine', config: { appendonly: true, maxDatabases: 16 } },
};
const info = platformService
? serviceInfo[platformService.type] || { host: 'unknown', port: 0, version: '', config: {} }