- Implemented CloudlyViewSettings component for managing cloud provider settings including Hetzner, Cloudflare, AWS, DigitalOcean, Azure, and Google Cloud. - Added functionality to load, save, and test connections for each provider. - Enhanced UI with loading states and success/error notifications. feat: Create tasks view with execution history - Developed CloudlyViewTasks component to display and manage tasks and their executions. - Integrated auto-refresh functionality for task executions. - Added filtering and searching capabilities for tasks. feat: Implement execution details and task panel components - Created CloudlyExecutionDetails component to show detailed information about task executions including logs and metrics. - Developed CloudlyTaskPanel component to display individual tasks with execution status and actions to run or cancel tasks. feat: Utility functions for formatting and categorization - Added utility functions for formatting dates, durations, and cron expressions. - Implemented functions to retrieve category icons and hues for task categorization.
226 lines
12 KiB
TypeScript
226 lines
12 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-services')
|
|
export class CloudlyViewServices extends DeesElement {
|
|
@state()
|
|
private 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`
|
|
.category-badge { padding: 2px 8px; border-radius: 4px; font-size: 0.9em; font-weight: 500; }
|
|
.category-base { background: #2196f3; color: white; }
|
|
.category-distributed { background: #9c27b0; color: white; }
|
|
.category-workload { background: #4caf50; color: white; }
|
|
.strategy-badge { padding: 2px 8px; border-radius: 4px; font-size: 0.85em; background: #444; color: #ccc; margin-left: 4px; }
|
|
`,
|
|
];
|
|
|
|
private getCategoryIcon(category: string): string {
|
|
switch (category) {
|
|
case 'base': return 'lucide:ServerCog';
|
|
case 'distributed': return 'lucide:Network';
|
|
case 'workload': return 'lucide:Container';
|
|
default: return 'lucide:Box';
|
|
}
|
|
}
|
|
|
|
private getCategoryBadgeHtml(category: string): any {
|
|
const className = `category-badge category-${category}`;
|
|
return html`<span class="${className}">${category}</span>`;
|
|
}
|
|
|
|
private getStrategyBadgeHtml(strategy: string): any {
|
|
return html`<span class="strategy-badge">${strategy}</span>`;
|
|
}
|
|
|
|
public render() {
|
|
return html`
|
|
<cloudly-sectionheading>Services</cloudly-sectionheading>
|
|
<dees-table
|
|
.heading1=${'Services'}
|
|
.heading2=${'Service configuration and deployment management'}
|
|
.data=${this.data.services || []}
|
|
.displayFunction=${(itemArg: plugins.interfaces.data.IService) => {
|
|
return {
|
|
Name: itemArg.data.name,
|
|
Description: itemArg.data.description,
|
|
Category: this.getCategoryBadgeHtml(itemArg.data.serviceCategory || 'workload'),
|
|
'Deployment Strategy': html`
|
|
${this.getStrategyBadgeHtml(itemArg.data.deploymentStrategy || 'custom')}
|
|
${itemArg.data.maxReplicas ? html`<span style="color: #888; margin-left: 8px;">Max: ${itemArg.data.maxReplicas}</span>` : ''}
|
|
${itemArg.data.antiAffinity ? html`<span style="color: #f44336; margin-left: 8px;">⚡ Anti-affinity</span>` : ''}
|
|
`,
|
|
'Image': `${itemArg.data.imageId}:${itemArg.data.imageVersion}`,
|
|
'Scale Factor': itemArg.data.scaleFactor,
|
|
'Balancing': itemArg.data.balancingStrategy,
|
|
'Deployments': itemArg.data.deploymentIds?.length || 0,
|
|
};
|
|
}}
|
|
.dataActions=${[
|
|
{
|
|
name: 'Add Service',
|
|
iconName: 'plus',
|
|
type: ['header', 'footer'],
|
|
actionFunc: async () => {
|
|
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
|
heading: 'Add Service',
|
|
content: html`
|
|
<dees-form>
|
|
<dees-input-text .key=${'name'} .label=${'Service Name'} .required=${true}></dees-input-text>
|
|
<dees-input-text .key=${'description'} .label=${'Description'} .required=${true}></dees-input-text>
|
|
<dees-input-dropdown .key=${'serviceCategory'} .label=${'Service Category'} .options=${[{key: 'base', option: 'Base'}, {key: 'distributed', option: 'Distributed'}, {key: 'workload', option: 'Workload'}]} .value=${'workload'} .required=${true}></dees-input-dropdown>
|
|
<dees-input-dropdown .key=${'deploymentStrategy'} .label=${'Deployment Strategy'} .options=${[{key: 'all-nodes', option: 'All Nodes'}, {key: 'limited-replicas', option: 'Limited Replicas'}, {key: 'custom', option: 'Custom'}]} .value=${'custom'} .required=${true}></dees-input-dropdown>
|
|
<dees-input-text .key=${'maxReplicas'} .label=${'Max Replicas (for distributed services)'} .value=${'1'} .type=${'number'}></dees-input-text>
|
|
<dees-input-checkbox .key=${'antiAffinity'} .label=${'Enable Anti-Affinity'} .value=${false}></dees-input-checkbox>
|
|
<dees-input-text .key=${'imageId'} .label=${'Image ID'} .required=${true}></dees-input-text>
|
|
<dees-input-text .key=${'imageVersion'} .label=${'Image Version'} .value=${'latest'} .required=${true}></dees-input-text>
|
|
<dees-input-text .key=${'scaleFactor'} .label=${'Scale Factor'} .value=${'1'} .type=${'number'} .required=${true}></dees-input-text>
|
|
<dees-input-dropdown .key=${'balancingStrategy'} .label=${'Balancing Strategy'} .options=${[{key: 'round-robin', option: 'Round Robin'}, {key: 'least-connections', option: 'Least Connections'}]} .value=${'round-robin'} .required=${true}></dees-input-dropdown>
|
|
<dees-input-text .key=${'webPort'} .label=${'Web Port'} .value=${'80'} .type=${'number'} .required=${true}></dees-input-text>
|
|
</dees-form>
|
|
`,
|
|
menuOptions: [
|
|
{ name: 'Create Service', action: async (modalArg: any) => {
|
|
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
|
const formData = await form.gatherData();
|
|
await appstate.dataState.dispatchAction(appstate.createServiceAction, {
|
|
serviceData: {
|
|
name: formData.name,
|
|
description: formData.description,
|
|
serviceCategory: formData.serviceCategory,
|
|
deploymentStrategy: formData.deploymentStrategy,
|
|
maxReplicas: formData.maxReplicas ? parseInt(formData.maxReplicas) : undefined,
|
|
antiAffinity: formData.antiAffinity,
|
|
imageId: formData.imageId,
|
|
imageVersion: formData.imageVersion,
|
|
scaleFactor: parseInt(formData.scaleFactor),
|
|
balancingStrategy: formData.balancingStrategy,
|
|
ports: { web: parseInt(formData.webPort) },
|
|
environment: {},
|
|
domains: [],
|
|
deploymentIds: [],
|
|
},
|
|
});
|
|
await modalArg.destroy();
|
|
}},
|
|
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
|
],
|
|
});
|
|
},
|
|
},
|
|
{
|
|
name: 'Edit',
|
|
iconName: 'edit',
|
|
type: ['contextmenu', 'inRow'],
|
|
actionFunc: async (actionDataArg: any) => {
|
|
const service = actionDataArg.item as plugins.interfaces.data.IService;
|
|
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
|
heading: `Edit Service: ${service.data.name}`,
|
|
content: html`
|
|
<dees-form>
|
|
<dees-input-text .key=${'name'} .label=${'Service Name'} .value=${service.data.name} .required=${true}></dees-input-text>
|
|
<dees-input-text .key=${'description'} .label=${'Description'} .value=${service.data.description} .required=${true}></dees-input-text>
|
|
<dees-input-dropdown .key=${'serviceCategory'} .label=${'Service Category'} .options=${[{key: 'base', option: 'Base'}, {key: 'distributed', option: 'Distributed'}, {key: 'workload', option: 'Workload'}]} .value=${service.data.serviceCategory || 'workload'} .required=${true}></dees-input-dropdown>
|
|
<dees-input-dropdown .key=${'deploymentStrategy'} .label=${'Deployment Strategy'} .options=${[{key: 'all-nodes', option: 'All Nodes'}, {key: 'limited-replicas', option: 'Limited Replicas'}, {key: 'custom', option: 'Custom'}]} .value=${service.data.deploymentStrategy || 'custom'} .required=${true}></dees-input-dropdown>
|
|
<dees-input-text .key=${'maxReplicas'} .label=${'Max Replicas (for distributed services)'} .value=${service.data.maxReplicas || ''} .type=${'number'}></dees-input-text>
|
|
<dees-input-checkbox .key=${'antiAffinity'} .label=${'Enable Anti-Affinity'} .value=${service.data.antiAffinity || false}></dees-input-checkbox>
|
|
<dees-input-text .key=${'imageVersion'} .label=${'Image Version'} .value=${service.data.imageVersion} .required=${true}></dees-input-text>
|
|
<dees-input-text .key=${'scaleFactor'} .label=${'Scale Factor'} .value=${service.data.scaleFactor} .type=${'number'} .required=${true}></dees-input-text>
|
|
<dees-input-dropdown .key=${'balancingStrategy'} .label=${'Balancing Strategy'} .options=${[{key: 'round-robin', option: 'Round Robin'}, {key: 'least-connections', option: 'Least Connections'}]} .value=${service.data.balancingStrategy} .required=${true}></dees-input-dropdown>
|
|
</dees-form>
|
|
`,
|
|
menuOptions: [
|
|
{ name: 'Update Service', action: async (modalArg: any) => {
|
|
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
|
const formData = await form.gatherData();
|
|
await appstate.dataState.dispatchAction(appstate.updateServiceAction, {
|
|
serviceId: service.id,
|
|
serviceData: {
|
|
...service.data,
|
|
name: formData.name,
|
|
description: formData.description,
|
|
serviceCategory: formData.serviceCategory,
|
|
deploymentStrategy: formData.deploymentStrategy,
|
|
maxReplicas: formData.maxReplicas ? parseInt(formData.maxReplicas) : undefined,
|
|
antiAffinity: formData.antiAffinity,
|
|
imageVersion: formData.imageVersion,
|
|
scaleFactor: parseInt(formData.scaleFactor),
|
|
balancingStrategy: formData.balancingStrategy,
|
|
},
|
|
});
|
|
await modalArg.destroy();
|
|
}},
|
|
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
|
],
|
|
});
|
|
},
|
|
},
|
|
{
|
|
name: 'Deploy',
|
|
iconName: 'rocket',
|
|
type: ['contextmenu', 'inRow'],
|
|
actionFunc: async (actionDataArg: any) => {
|
|
const service = actionDataArg.item as plugins.interfaces.data.IService;
|
|
console.log('Deploy service:', service);
|
|
},
|
|
},
|
|
{
|
|
name: 'Delete',
|
|
iconName: 'trash',
|
|
type: ['contextmenu', 'inRow'],
|
|
actionFunc: async (actionDataArg: any) => {
|
|
const service = actionDataArg.item as plugins.interfaces.data.IService;
|
|
plugins.deesCatalog.DeesModal.createAndShow({
|
|
heading: `Delete Service: ${service.data.name}`,
|
|
content: html`
|
|
<div style="text-align:center">Are you sure you want to delete this service?</div>
|
|
<div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;">
|
|
<div style="color: #fff; font-weight: bold;">${service.data.name}</div>
|
|
<div style="color: #aaa; font-size: 0.9em; margin-top: 4px;">${service.data.description}</div>
|
|
<div style="color: #f44336; margin-top: 8px;">This will also delete ${service.data.deploymentIds?.length || 0} deployment(s)</div>
|
|
</div>
|
|
`,
|
|
menuOptions: [
|
|
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
|
{ name: 'Delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteServiceAction, { serviceId: service.id }); await modalArg.destroy(); } },
|
|
],
|
|
});
|
|
},
|
|
},
|
|
] as plugins.deesCatalog.ITableAction[]}
|
|
></dees-table>
|
|
`;
|
|
}
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
'cloudly-view-services': CloudlyViewServices;
|
|
}
|
|
}
|
|
|