2024-04-20 12:21:41 +02:00
|
|
|
import * as plugins from '../plugins.js';
|
2024-10-16 14:35:38 +02:00
|
|
|
import * as shared from '../elements/shared/index.js';
|
2024-04-20 12:21:41 +02:00
|
|
|
|
|
|
|
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()
|
2025-09-08 12:46:23 +00:00
|
|
|
private data: appstate.IDataState = {};
|
2024-04-20 12:21:41 +02:00
|
|
|
|
|
|
|
constructor() {
|
|
|
|
super();
|
2025-09-08 06:46:14 +00:00
|
|
|
const subscription = appstate.dataState
|
2024-04-20 12:21:41 +02:00
|
|
|
.select((stateArg) => stateArg)
|
|
|
|
.subscribe((dataArg) => {
|
|
|
|
this.data = dataArg;
|
|
|
|
});
|
2025-09-08 06:46:14 +00:00
|
|
|
this.rxSubscriptions.push(subscription);
|
2024-04-20 12:21:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public static styles = [
|
|
|
|
cssManager.defaultStyles,
|
2024-10-16 14:35:38 +02:00
|
|
|
shared.viewHostCss,
|
2024-04-20 12:21:41 +02:00
|
|
|
css`
|
2025-09-08 06:46:14 +00:00
|
|
|
.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;
|
|
|
|
}
|
2024-04-20 12:21:41 +02:00
|
|
|
`,
|
|
|
|
];
|
|
|
|
|
2025-09-08 06:46:14 +00:00
|
|
|
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 {
|
|
|
|
// This would ideally look up the cluster node name
|
|
|
|
// For now just return the ID shortened
|
|
|
|
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>
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
|
2024-04-20 12:21:41 +02:00
|
|
|
public render() {
|
|
|
|
return html`
|
|
|
|
<cloudly-sectionheading>Deployments</cloudly-sectionheading>
|
|
|
|
<dees-table
|
|
|
|
.heading1=${'Deployments'}
|
2025-09-08 06:46:14 +00:00
|
|
|
.heading2=${'Service deployments running on cluster nodes'}
|
|
|
|
.data=${this.data.deployments || []}
|
|
|
|
.displayFunction=${(itemArg: plugins.interfaces.data.IDeployment) => {
|
2024-04-20 12:21:41 +02:00
|
|
|
return {
|
2025-09-08 06:46:14 +00:00
|
|
|
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',
|
2024-04-20 12:21:41 +02:00
|
|
|
};
|
|
|
|
}}
|
|
|
|
.dataActions=${[
|
|
|
|
{
|
2025-09-08 06:46:14 +00:00
|
|
|
name: 'Deploy Service',
|
2024-04-20 12:21:41 +02:00
|
|
|
iconName: 'plus',
|
|
|
|
type: ['header', 'footer'],
|
|
|
|
actionFunc: async (dataActionArg) => {
|
2025-09-08 06:46:14 +00:00
|
|
|
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) => {
|
|
|
|
await modalArg.destroy();
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-04-20 12:21:41 +02:00
|
|
|
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
2025-09-08 06:46:14 +00:00
|
|
|
heading: 'Deploy Service',
|
2024-04-20 12:21:41 +02:00
|
|
|
content: html`
|
|
|
|
<dees-form>
|
2025-09-08 06:46:14 +00:00
|
|
|
<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>
|
2024-04-20 12:21:41 +02:00
|
|
|
</dees-form>
|
|
|
|
`,
|
|
|
|
menuOptions: [
|
|
|
|
{
|
2025-09-08 06:46:14 +00:00
|
|
|
name: 'Deploy',
|
|
|
|
action: async (modalArg) => {
|
|
|
|
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', // This would come from the service
|
|
|
|
deploymentLog: [],
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
await modalArg.destroy();
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'Cancel',
|
2024-04-20 12:21:41 +02:00
|
|
|
action: async (modalArg) => {
|
|
|
|
modalArg.destroy();
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
2025-09-08 06:46:14 +00:00
|
|
|
name: 'Restart',
|
|
|
|
iconName: 'refresh-cw',
|
|
|
|
type: ['contextmenu', 'inRow'],
|
|
|
|
actionFunc: async (actionDataArg) => {
|
|
|
|
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) => {
|
|
|
|
await modalArg.destroy();
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'Restart',
|
|
|
|
action: async (modalArg) => {
|
|
|
|
// TODO: Implement restart action
|
|
|
|
console.log('Restart deployment:', deployment);
|
|
|
|
await modalArg.destroy();
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'Stop',
|
|
|
|
iconName: 'square',
|
|
|
|
type: ['contextmenu', 'inRow'],
|
|
|
|
actionFunc: async (actionDataArg) => {
|
|
|
|
const deployment = actionDataArg.item as plugins.interfaces.data.IDeployment;
|
|
|
|
await appstate.dataState.dispatchAction(appstate.updateDeploymentAction, {
|
|
|
|
deploymentId: deployment.id,
|
|
|
|
deploymentData: {
|
|
|
|
...deployment,
|
|
|
|
status: 'stopped',
|
|
|
|
},
|
|
|
|
});
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'Delete',
|
2024-04-20 12:21:41 +02:00
|
|
|
iconName: 'trash',
|
|
|
|
type: ['contextmenu', 'inRow'],
|
|
|
|
actionFunc: async (actionDataArg) => {
|
2025-09-08 06:46:14 +00:00
|
|
|
const deployment = actionDataArg.item as plugins.interfaces.data.IDeployment;
|
2024-04-20 12:21:41 +02:00
|
|
|
plugins.deesCatalog.DeesModal.createAndShow({
|
2025-09-08 06:46:14 +00:00
|
|
|
heading: `Delete Deployment`,
|
2024-04-20 12:21:41 +02:00
|
|
|
content: html`
|
|
|
|
<div style="text-align:center">
|
2025-09-08 06:46:14 +00:00
|
|
|
Are you sure you want to delete this deployment?
|
2024-04-20 12:21:41 +02:00
|
|
|
</div>
|
2025-09-08 06:46:14 +00:00
|
|
|
<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>
|
2024-04-20 12:21:41 +02:00
|
|
|
</div>
|
|
|
|
`,
|
|
|
|
menuOptions: [
|
|
|
|
{
|
2025-09-08 06:46:14 +00:00
|
|
|
name: 'Cancel',
|
2024-04-20 12:21:41 +02:00
|
|
|
action: async (modalArg) => {
|
|
|
|
await modalArg.destroy();
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
2025-09-08 06:46:14 +00:00
|
|
|
name: 'Delete',
|
2024-04-20 12:21:41 +02:00
|
|
|
action: async (modalArg) => {
|
2025-09-08 06:46:14 +00:00
|
|
|
await appstate.dataState.dispatchAction(appstate.deleteDeploymentAction, {
|
|
|
|
deploymentId: deployment.id,
|
2024-04-20 12:21:41 +02:00
|
|
|
});
|
|
|
|
await modalArg.destroy();
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
|
|
|
},
|
|
|
|
},
|
|
|
|
] as plugins.deesCatalog.ITableAction[]}
|
|
|
|
></dees-table>
|
|
|
|
`;
|
|
|
|
}
|
2025-09-08 06:46:14 +00:00
|
|
|
}
|