f40ef6b7c0
Align Cloudly with the current typedserver, smartconfig, smartstate, and Docker tooling releases so builds and Docker output stay compatible with the upgraded stack.
222 lines
10 KiB
TypeScript
222 lines
10 KiB
TypeScript
import * as plugins from '../../../plugins.js';
|
|
import * as shared from '../../shared/index.js';
|
|
|
|
import {
|
|
DeesElement,
|
|
customElement,
|
|
html,
|
|
state,
|
|
css,
|
|
cssManager,
|
|
} from '@design.estate/dees-element';
|
|
|
|
import * as appstate from '../../../appstate.js';
|
|
|
|
@customElement('cloudly-view-deployments')
|
|
export class CloudlyViewDeployments extends DeesElement {
|
|
@state()
|
|
private accessor data: appstate.IDataState = {} as any;
|
|
|
|
constructor() {
|
|
super();
|
|
const subscription = appstate.dataState
|
|
.select((stateArg) => stateArg)
|
|
.subscribe((dataArg) => {
|
|
this.data = dataArg;
|
|
});
|
|
this.rxSubscriptions.push(subscription);
|
|
}
|
|
|
|
public static styles = [
|
|
cssManager.defaultStyles,
|
|
shared.viewHostCss,
|
|
css`
|
|
.status-badge { padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; }
|
|
.status-running { background: #4caf50; color: white; }
|
|
.status-stopped { background: #f44336; color: white; }
|
|
.status-paused { background: #ff9800; color: white; }
|
|
.status-deploying { background: #2196f3; color: white; }
|
|
.health-indicator { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; }
|
|
.health-healthy { background: #e8f5e9; color: #2e7d32; }
|
|
.health-unhealthy { background: #ffebee; color: #c62828; }
|
|
.health-unknown { background: #f5f5f5; color: #666; }
|
|
.resource-usage { display: flex; gap: 12px; font-size: 0.9em; color: #888; }
|
|
.resource-item { display: flex; align-items: center; gap: 4px; }
|
|
`,
|
|
];
|
|
|
|
private getServiceName(serviceId: string): string {
|
|
const service = this.data.services?.find(s => s.id === serviceId);
|
|
return service?.data?.name || serviceId;
|
|
}
|
|
|
|
private getNodeName(nodeId: string): string {
|
|
return nodeId.substring(0, 8);
|
|
}
|
|
|
|
private getStatusBadgeHtml(status: string): any {
|
|
const className = `status-badge status-${status}`;
|
|
return html`<span class="${className}">${status}</span>`;
|
|
}
|
|
|
|
private getHealthIndicatorHtml(health?: string): any {
|
|
if (!health) health = 'unknown';
|
|
const className = `health-indicator health-${health}`;
|
|
const icon = health === 'healthy' ? '✓' : health === 'unhealthy' ? '✗' : '?';
|
|
return html`<span class="${className}">${icon} ${health}</span>`;
|
|
}
|
|
|
|
private getResourceUsageHtml(deployment: plugins.interfaces.data.IDeployment): any {
|
|
if (!deployment.resourceUsage) {
|
|
return html`<span style="color: #aaa;">N/A</span>`;
|
|
}
|
|
const { cpuUsagePercent, memoryUsedMB } = deployment.resourceUsage;
|
|
return html`
|
|
<div class="resource-usage">
|
|
<div class="resource-item">
|
|
<lucide-icon name="Cpu" size="14"></lucide-icon>
|
|
${cpuUsagePercent?.toFixed(1) || 0}%
|
|
</div>
|
|
<div class="resource-item">
|
|
<lucide-icon name="MemoryStick" size="14"></lucide-icon>
|
|
${memoryUsedMB || 0} MB
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
public render() {
|
|
return html`
|
|
<cloudly-sectionheading>Deployments</cloudly-sectionheading>
|
|
<dees-table
|
|
.heading1=${'Deployments'}
|
|
.heading2=${'Service deployments running on cluster nodes'}
|
|
.data=${this.data.deployments || []}
|
|
.displayFunction=${(itemArg: plugins.interfaces.data.IDeployment) => {
|
|
return {
|
|
Service: this.getServiceName(itemArg.serviceId),
|
|
Node: this.getNodeName(itemArg.nodeId),
|
|
Status: this.getStatusBadgeHtml(itemArg.status),
|
|
Health: this.getHealthIndicatorHtml(itemArg.healthStatus),
|
|
'Container ID': itemArg.containerId ? html`<span style="font-family: monospace; font-size: 0.9em;">${itemArg.containerId.substring(0, 12)}</span>` : html`<span style="color: #aaa;">N/A</span>`,
|
|
Version: itemArg.version || 'latest',
|
|
'Resource Usage': this.getResourceUsageHtml(itemArg),
|
|
'Last Updated': itemArg.deployedAt ? new Date(itemArg.deployedAt).toLocaleString() : 'Never',
|
|
};
|
|
}}
|
|
.dataActions=${[
|
|
{
|
|
name: 'Deploy Service',
|
|
iconName: 'plus',
|
|
type: ['header', 'footer'],
|
|
actionFunc: async () => {
|
|
const availableServices = this.data.services || [];
|
|
if (availableServices.length === 0) {
|
|
plugins.deesCatalog.DeesModal.createAndShow({
|
|
heading: 'No Services Available',
|
|
content: html`<div style="text-align: center; padding: 24px;"><lucide-icon name="AlertCircle" size="48" style="color: #ff9800; margin-bottom: 16px;"></lucide-icon><div>Please create a service first before creating deployments.</div></div>`,
|
|
menuOptions: [ { name: 'OK', action: async (modalArg: any) => { await modalArg.destroy(); } } ],
|
|
});
|
|
return;
|
|
}
|
|
await plugins.deesCatalog.DeesModal.createAndShow({
|
|
heading: 'Deploy Service',
|
|
content: html`
|
|
<dees-form>
|
|
<dees-input-dropdown .key=${'serviceId'} .label=${'Service'} .options=${availableServices.map(s => ({ key: s.id, value: s.data.name }))} .required=${true}></dees-input-dropdown>
|
|
<dees-input-text .key=${'nodeId'} .label=${'Target Node ID'} .required=${true} .description=${'Enter the cluster node ID where this service should be deployed'}></dees-input-text>
|
|
<dees-input-text .key=${'version'} .label=${'Version'} .value=${'latest'} .required=${true}></dees-input-text>
|
|
<dees-input-dropdown .key=${'status'} .label=${'Initial Status'} .options=${['deploying', 'running']} .value=${'deploying'} .required=${true}></dees-input-dropdown>
|
|
</dees-form>
|
|
`,
|
|
menuOptions: [
|
|
{ name: 'Deploy', action: async (modalArg: any) => {
|
|
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
|
const formData = await form.gatherData();
|
|
await appstate.dataState.dispatchAction(appstate.createDeploymentAction, {
|
|
deploymentData: {
|
|
serviceId: formData.serviceId,
|
|
nodeId: formData.nodeId,
|
|
status: formData.status,
|
|
version: formData.version,
|
|
deployedAt: Date.now(),
|
|
usedImageId: 'placeholder',
|
|
deploymentLog: [],
|
|
},
|
|
});
|
|
await modalArg.destroy();
|
|
}},
|
|
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
|
],
|
|
});
|
|
},
|
|
},
|
|
{
|
|
name: 'Restart',
|
|
iconName: 'refresh-cw',
|
|
type: ['contextmenu', 'inRow'],
|
|
actionFunc: async (actionDataArg: any) => {
|
|
const deployment = actionDataArg.item as plugins.interfaces.data.IDeployment;
|
|
plugins.deesCatalog.DeesModal.createAndShow({
|
|
heading: `Restart Deployment`,
|
|
content: html`
|
|
<div style="text-align:center">Are you sure you want to restart this deployment?</div>
|
|
<div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;">
|
|
<div style="color: #fff; font-weight: bold;">${this.getServiceName(deployment.serviceId)}</div>
|
|
<div style="color: #aaa; font-size: 0.9em; margin-top: 4px;">Node: ${this.getNodeName(deployment.nodeId)}</div>
|
|
</div>
|
|
`,
|
|
menuOptions: [
|
|
{ name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } },
|
|
{ name: 'Restart', action: async (modalArg: any) => { console.log('Restart deployment:', deployment); await modalArg.destroy(); } },
|
|
],
|
|
});
|
|
},
|
|
},
|
|
{
|
|
name: 'Stop',
|
|
iconName: 'square',
|
|
type: ['contextmenu', 'inRow'],
|
|
actionFunc: async (actionDataArg: any) => {
|
|
const deployment = actionDataArg.item as plugins.interfaces.data.IDeployment;
|
|
await appstate.dataState.dispatchAction(appstate.updateDeploymentAction, {
|
|
deploymentId: deployment.id,
|
|
deploymentData: { ...deployment, status: 'stopped' },
|
|
});
|
|
},
|
|
},
|
|
{
|
|
name: 'Delete',
|
|
iconName: 'trash',
|
|
type: ['contextmenu', 'inRow'],
|
|
actionFunc: async (actionDataArg: any) => {
|
|
const deployment = actionDataArg.item as plugins.interfaces.data.IDeployment;
|
|
plugins.deesCatalog.DeesModal.createAndShow({
|
|
heading: `Delete Deployment`,
|
|
content: html`
|
|
<div style="text-align:center">Are you sure you want to delete this deployment?</div>
|
|
<div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;">
|
|
<div style="color: #fff; font-weight: bold;">${this.getServiceName(deployment.serviceId)}</div>
|
|
<div style="color: #aaa; font-size: 0.9em; margin-top: 4px;">Node: ${this.getNodeName(deployment.nodeId)}</div>
|
|
<div style="color: #f44336; margin-top: 8px;">This action cannot be undone.</div>
|
|
</div>
|
|
`,
|
|
menuOptions: [
|
|
{ name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } },
|
|
{ name: 'Delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteDeploymentAction, { deploymentId: deployment.id, }); await modalArg.destroy(); } },
|
|
],
|
|
});
|
|
},
|
|
},
|
|
] as plugins.deesCatalog.ITableAction[]}
|
|
></dees-table>
|
|
`;
|
|
}
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
'cloudly-view-deployments': CloudlyViewDeployments;
|
|
}
|
|
}
|