feat(appstore): add service volumes and published ports

This commit is contained in:
2026-05-24 07:28:18 +00:00
parent e6ebac76b4
commit 5228eeaa23
26 changed files with 1790 additions and 348 deletions
+113 -18
View File
@@ -288,6 +288,34 @@ export class ObViewAppStore extends DeesElement {
text-align: center;
color: var(--ci-shade-4, #71717a);
}
.footprint-list {
display: grid;
gap: 8px;
}
.footprint-item {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
border: 1px solid var(--ci-shade-2, #27272a);
border-radius: 6px;
font-size: 13px;
color: var(--ci-shade-6, #d4d4d8);
}
.footprint-meta {
color: var(--ci-shade-4, #71717a);
font-family: monospace;
}
.exposure-warning {
margin-top: 10px;
color: #fbbf24;
font-size: 12px;
line-height: 1.5;
}
`,
];
@@ -410,6 +438,8 @@ export class ObViewAppStore extends DeesElement {
</div>
` : ''}
${this.renderDeploymentFootprint(config)}
<!-- Version & Image -->
<div class="detail-card">
<div class="section-label">Version</div>
@@ -489,6 +519,8 @@ export class ObViewAppStore extends DeesElement {
Onebox routes this domain to the deployed app. Required when the app uses SERVICE_DOMAIN.
</div>
${this.renderDeployConfirmation(config)}
<div class="actions-row">
<button class="btn btn-secondary" @click=${() => { this.currentView = 'grid'; }}>Cancel</button>
<button class="btn btn-primary" @click=${() => this.handleDeploy()}>
@@ -509,6 +541,73 @@ export class ObViewAppStore extends DeesElement {
`;
}
private renderDeploymentFootprint(config: interfaces.requests.IAppVersionConfig): TemplateResult | '' {
const volumes = this.getConfigVolumes(config);
const publishedPorts = config.publishedPorts || [];
if (volumes.length === 0 && publishedPorts.length === 0) {
return '';
}
return html`
<div class="detail-card">
<div class="section-label">Deployment Footprint</div>
<div class="footprint-list">
${volumes.map((volume) => html`
<div class="footprint-item">
<span>Volume mount</span>
<span class="footprint-meta">
${volume.source || volume.name || 'managed volume'}:${volume.mountPath}${volume.readOnly ? ':ro' : ''}
</span>
</div>
`)}
${publishedPorts.map((port) => html`
<div class="footprint-item">
<span>Published host port</span>
<span class="footprint-meta">${this.formatPublishedPort(port)}</span>
</div>
`)}
</div>
${publishedPorts.length > 0 ? html`
<div class="exposure-warning">
This app publishes raw host ports outside the HTTP proxy. Confirm firewall and network policy before deploying.
</div>
` : ''}
</div>
`;
}
private renderDeployConfirmation(config: interfaces.requests.IAppVersionConfig): TemplateResult | '' {
const volumes = this.getConfigVolumes(config);
const publishedPorts = config.publishedPorts || [];
if (volumes.length === 0 && publishedPorts.length === 0) return '';
return html`
<div class="exposure-warning">
Deploying this app will create ${volumes.length} persistent volume(s)
${publishedPorts.length > 0 ? html`and expose ${publishedPorts.length} host port declaration(s)` : ''}.
</div>
`;
}
private getConfigVolumes(config: interfaces.requests.IAppVersionConfig): interfaces.data.IServiceVolume[] {
return (config.volumes || []).map((volume) => {
if (typeof volume === 'string') {
return { mountPath: volume };
}
return volume;
}).filter((volume) => Boolean(volume.mountPath));
}
private formatPublishedPort(port: interfaces.data.IServicePublishedPort): string {
const protocol = port.protocol || 'tcp';
const target = port.targetPortEnd ? `${port.targetPort}-${port.targetPortEnd}` : String(port.targetPort);
const publishedStart = port.publishedPort || port.targetPort;
const publishedEnd = port.publishedPortEnd || (port.targetPortEnd ? publishedStart + (port.targetPortEnd - port.targetPort) : undefined);
const published = publishedEnd ? `${publishedStart}-${publishedEnd}` : String(publishedStart);
return `${port.hostIp || '0.0.0.0'}:${published}/${protocol} -> ${target}/${protocol}`;
}
private async handleViewDetails(e: CustomEvent) {
const app = e.detail?.app;
if (!app) return;
@@ -625,25 +724,21 @@ export class ObViewAppStore extends DeesElement {
}
}
const platformReqs = config.platformRequirements || {};
const serviceConfig: interfaces.data.IServiceCreate = {
name: this.serviceName || app.id,
image: config.image,
port: config.port || 80,
domain: this.serviceDomain || undefined,
envVars,
enableMongoDB: platformReqs.mongodb || false,
enableS3: platformReqs.s3 || false,
enableClickHouse: platformReqs.clickhouse || false,
enableRedis: platformReqs.redis || false,
enableMariaDB: platformReqs.mariadb || false,
appTemplateId: app.id,
appTemplateVersion: this.selectedVersion,
};
try {
await appstate.servicesStatePart.dispatchAction(appstate.createServiceAction, {
config: serviceConfig,
const identity = appstate.loginStatePart.getState().identity;
if (!identity) return;
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_InstallAppTemplate
>('/typedrequest', 'installAppTemplate');
await typedRequest.fire({
identity,
install: {
appId: app.id,
version: this.selectedVersion,
serviceName: this.serviceName || app.id,
domain: this.serviceDomain || undefined,
envVars,
},
});
setTimeout(() => {
appRouter.navigateToView('services');