feat: Add settings view for cloud provider configurations
- 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.
This commit is contained in:
@@ -11,23 +11,23 @@ import {
|
||||
html,
|
||||
state
|
||||
} from '@design.estate/dees-element';
|
||||
import { CloudlyViewBackups } from './cloudly-view-backups.js';
|
||||
import { CloudlyViewClusters } from './cloudly-view-clusters.js';
|
||||
import { CloudlyViewDbs } from './cloudly-view-dbs.js';
|
||||
import { CloudlyViewDeployments } from './cloudly-view-deployments.js';
|
||||
import { CloudlyViewDns } from './cloudly-view-dns.js';
|
||||
import { CloudlyViewDomains } from './cloudly-view-domains.js';
|
||||
import { CloudlyViewImages } from './cloudly-view-images.js';
|
||||
import { CloudlyViewLogs } from './cloudly-view-logs.js';
|
||||
import { CloudlyViewMails } from './cloudly-view-mails.js';
|
||||
import { CloudlyViewOverview } from './cloudly-view-overview.js';
|
||||
import { CloudlyViewS3 } from './cloudly-view-s3.js';
|
||||
import { CloudlyViewSecretBundles } from './cloudly-view-secretbundles.js';
|
||||
import { CloudlyViewSecretGroups } from './cloudly-view-secretgroups.js';
|
||||
import { CloudlyViewServices } from './cloudly-view-services.js';
|
||||
import { CloudlyViewExternalRegistries } from './cloudly-view-externalregistries.js';
|
||||
import { CloudlyViewSettings } from './cloudly-view-settings.js';
|
||||
import { CloudlyViewTasks } from './cloudly-view-tasks.js';
|
||||
import { CloudlyViewBackups } from './views/backups/index.js';
|
||||
import { CloudlyViewClusters } from './views/clusters/index.js';
|
||||
import { CloudlyViewDbs } from './views/dbs/index.js';
|
||||
import { CloudlyViewDeployments } from './views/deployments/index.js';
|
||||
import { CloudlyViewDns } from './views/dns/index.js';
|
||||
import { CloudlyViewDomains } from './views/domains/index.js';
|
||||
import { CloudlyViewImages } from './views/images/index.js';
|
||||
import { CloudlyViewLogs } from './views/logs/index.js';
|
||||
import { CloudlyViewMails } from './views/mails/index.js';
|
||||
import { CloudlyViewOverview } from './views/overview/index.js';
|
||||
import { CloudlyViewS3 } from './views/s3/index.js';
|
||||
import { CloudlyViewSecretBundles } from './views/secretbundles/index.js';
|
||||
import { CloudlyViewSecretGroups } from './views/secretgroups/index.js';
|
||||
import { CloudlyViewServices } from './views/services/index.js';
|
||||
import { CloudlyViewExternalRegistries } from './views/externalregistries/index.js';
|
||||
import { CloudlyViewSettings } from './views/settings/index.js';
|
||||
import { CloudlyViewTasks } from './views/tasks/index.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
|
@@ -1,130 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from '../elements/shared/index.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as appstate from '../appstate.js';
|
||||
|
||||
@customElement('cloudly-view-backups')
|
||||
export class CloudlyViewBackups extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = {
|
||||
secretGroups: [],
|
||||
secretBundles: [],
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subecription = appstate.dataState
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((dataArg) => {
|
||||
this.data = dataArg;
|
||||
});
|
||||
this.rxSubscriptions.push(subecription);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<cloudly-sectionheading>Backups</cloudly-sectionheading>
|
||||
<dees-table
|
||||
.heading1=${'Backups'}
|
||||
.heading2=${'decoded in client'}
|
||||
.data=${this.data.backups}
|
||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
||||
return {
|
||||
id: itemArg.id,
|
||||
serverAmount: itemArg.data.servers.length,
|
||||
};
|
||||
}}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'add configBundle',
|
||||
iconName: 'plus',
|
||||
type: ['header', 'footer'],
|
||||
actionFunc: async (dataActionArg) => {
|
||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: 'Add ConfigBundle',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'data.secretGroupIds'}
|
||||
.label=${'secretGroupIds'}
|
||||
.value=${''}
|
||||
></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'data.includedTags'}
|
||||
.label=${'includedTags'}
|
||||
.value=${''}
|
||||
></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'create', action: async (modalArg) => {} },
|
||||
{
|
||||
name: 'cancel',
|
||||
action: async (modalArg) => {
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
iconName: 'trash',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
|
||||
content: html`
|
||||
<div style="text-align:center">
|
||||
Do you really want to delete the ConfigBundle?
|
||||
</div>
|
||||
<div
|
||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
||||
>
|
||||
${actionDataArg.item.id}
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'cancel',
|
||||
action: async (modalArg) => {
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
action: async (modalArg) => {
|
||||
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
|
||||
configBundleId: actionDataArg.item.id,
|
||||
});
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
@@ -1,147 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from '../elements/shared/index.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as appstate from '../appstate.js';
|
||||
|
||||
@customElement('cloudly-view-clusters')
|
||||
export class CloudlyViewClusters extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = {};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subecription = appstate.dataState
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((dataArg) => {
|
||||
this.data = dataArg;
|
||||
});
|
||||
this.rxSubscriptions.push(subecription);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<cloudly-sectionheading>Clusters</cloudly-sectionheading>
|
||||
<dees-table
|
||||
.heading1=${'Clusters'}
|
||||
.heading2=${'decoded in client'}
|
||||
.data=${this.data.clusters}
|
||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
||||
console.log(itemArg);
|
||||
return {
|
||||
id: itemArg.id,
|
||||
serverAmount: itemArg.data.servers.length,
|
||||
};
|
||||
}}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'add cluster',
|
||||
iconName: 'plus',
|
||||
type: ['header', 'footer'],
|
||||
actionFunc: async (dataActionArg) => {
|
||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: 'Add Cluster',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text
|
||||
.key=${'clusterName'}
|
||||
.label=${'cluster name'}
|
||||
.description=${'a descriptive name for the cluster'}
|
||||
.value=${''}
|
||||
></dees-input-text>
|
||||
<dees-input-dropdown
|
||||
.key=${'setupMode'}
|
||||
.label=${'Setup Mode'}
|
||||
.description=${'How the cluster infrastructure should be managed'}
|
||||
.options=${[
|
||||
{option: 'manual', key: 'manual', description: 'Manual Setup - Add your own servers manually'},
|
||||
{option: 'hetzner', key: 'hetzner', description: 'Hetzner Cloud - Auto-provision servers on Hetzner'},
|
||||
{option: 'aws', key: 'aws', description: 'AWS - Auto-provision on Amazon Web Services (coming soon)', disabled: true},
|
||||
{option: 'digitalocean', key: 'digitalocean', description: 'DigitalOcean - Auto-provision on DigitalOcean (coming soon)', disabled: true}
|
||||
]}
|
||||
.selectedOption=${'manual'}
|
||||
></dees-input-dropdown>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'create',
|
||||
action: async (modalArg) => {
|
||||
const data: {
|
||||
clusterName: string;
|
||||
setupMode: 'manual' | 'hetzner' | 'aws' | 'digitalocean';
|
||||
} = (await modalArg.shadowRoot
|
||||
.querySelector('dees-form')
|
||||
.collectFormData()) as any;
|
||||
await appstate.dataState.dispatchAction(appstate.addClusterAction, data);
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'cancel',
|
||||
action: async (modalArg) => {
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
iconName: 'trash',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
|
||||
content: html`
|
||||
<div style="text-align:center">
|
||||
Do you really want to delete the ConfigBundle?
|
||||
</div>
|
||||
<div
|
||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
||||
>
|
||||
${actionDataArg.item.id}
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'cancel',
|
||||
action: async (modalArg) => {
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
action: async (modalArg) => {
|
||||
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
|
||||
configBundleId: actionDataArg.item.id,
|
||||
});
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
@@ -1,130 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from '../elements/shared/index.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as appstate from '../appstate.js';
|
||||
|
||||
@customElement('cloudly-view-dbs')
|
||||
export class CloudlyViewDbs extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = {
|
||||
secretGroups: [],
|
||||
secretBundles: [],
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subecription = appstate.dataState
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((dataArg) => {
|
||||
this.data = dataArg;
|
||||
});
|
||||
this.rxSubscriptions.push(subecription);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<cloudly-sectionheading>DBs</cloudly-sectionheading>
|
||||
<dees-table
|
||||
.heading1=${'DBs'}
|
||||
.heading2=${'decoded in client'}
|
||||
.data=${this.data.dbs}
|
||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
||||
return {
|
||||
id: itemArg.id,
|
||||
serverAmount: itemArg.data.servers.length,
|
||||
};
|
||||
}}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'add configBundle',
|
||||
iconName: 'plus',
|
||||
type: ['header', 'footer'],
|
||||
actionFunc: async (dataActionArg) => {
|
||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: 'Add ConfigBundle',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'data.secretGroupIds'}
|
||||
.label=${'secretGroupIds'}
|
||||
.value=${''}
|
||||
></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'data.includedTags'}
|
||||
.label=${'includedTags'}
|
||||
.value=${''}
|
||||
></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'create', action: async (modalArg) => {} },
|
||||
{
|
||||
name: 'cancel',
|
||||
action: async (modalArg) => {
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
iconName: 'trash',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
|
||||
content: html`
|
||||
<div style="text-align:center">
|
||||
Do you really want to delete the ConfigBundle?
|
||||
</div>
|
||||
<div
|
||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
||||
>
|
||||
${actionDataArg.item.id}
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'cancel',
|
||||
action: async (modalArg) => {
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
action: async (modalArg) => {
|
||||
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
|
||||
configBundleId: actionDataArg.item.id,
|
||||
});
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
@@ -1,349 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from '../elements/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 data: appstate.IDataState = {};
|
||||
|
||||
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 {
|
||||
// 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>
|
||||
`;
|
||||
}
|
||||
|
||||
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 (dataActionArg) => {
|
||||
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;
|
||||
}
|
||||
|
||||
const modal = 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) => {
|
||||
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',
|
||||
action: async (modalArg) => {
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
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',
|
||||
iconName: 'trash',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
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) => {
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
action: async (modalArg) => {
|
||||
await appstate.dataState.dispatchAction(appstate.deleteDeploymentAction, {
|
||||
deploymentId: deployment.id,
|
||||
});
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
@@ -1,429 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from '../elements/shared/index.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as appstate from '../appstate.js';
|
||||
|
||||
@customElement('cloudly-view-dns')
|
||||
export class CloudlyViewDns extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = {
|
||||
secretGroups: [],
|
||||
secretBundles: [],
|
||||
dnsEntries: [],
|
||||
domains: [],
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subscription = appstate.dataState
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((dataArg) => {
|
||||
this.data = dataArg;
|
||||
});
|
||||
this.rxSubscriptions.push(subscription);
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
// Load all data including domains and DNS entries
|
||||
await appstate.dataState.dispatchAction(appstate.getAllDataAction, {});
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
.dns-type-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
}
|
||||
.type-A { background: #4CAF50; }
|
||||
.type-AAAA { background: #45a049; }
|
||||
.type-CNAME { background: #2196F3; }
|
||||
.type-MX { background: #FF9800; }
|
||||
.type-TXT { background: #9C27B0; }
|
||||
.type-NS { background: #795548; }
|
||||
.type-SOA { background: #607D8B; }
|
||||
.type-SRV { background: #E91E63; }
|
||||
.type-CAA { background: #00BCD4; }
|
||||
.type-PTR { background: #673AB7; }
|
||||
|
||||
.status-active {
|
||||
color: #4CAF50;
|
||||
}
|
||||
.status-inactive {
|
||||
color: #f44336;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
private getRecordTypeBadge(type: string) {
|
||||
return html`<span class="dns-type-badge type-${type}">${type}</span>`;
|
||||
}
|
||||
|
||||
private getStatusBadge(active: boolean) {
|
||||
return html`<span class="${active ? 'status-active' : 'status-inactive'}">
|
||||
${active ? '✓ Active' : '✗ Inactive'}
|
||||
</span>`;
|
||||
}
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<cloudly-sectionheading>DNS Management</cloudly-sectionheading>
|
||||
<dees-table
|
||||
.heading1=${'DNS Entries'}
|
||||
.heading2=${'Manage DNS records for your domains'}
|
||||
.data=${this.data.dnsEntries || []}
|
||||
.displayFunction=${(itemArg: plugins.interfaces.data.IDnsEntry) => {
|
||||
return {
|
||||
Type: this.getRecordTypeBadge(itemArg.data.type),
|
||||
Name: itemArg.data.name === '@' ? '<root>' : itemArg.data.name,
|
||||
Value: itemArg.data.value,
|
||||
TTL: `${itemArg.data.ttl}s`,
|
||||
Priority: itemArg.data.priority || '-',
|
||||
Zone: itemArg.data.zone,
|
||||
Status: this.getStatusBadge(itemArg.data.active),
|
||||
Description: itemArg.data.description || '-',
|
||||
};
|
||||
}}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'Add DNS Entry',
|
||||
iconName: 'plus',
|
||||
type: ['header', 'footer'],
|
||||
actionFunc: async (dataActionArg) => {
|
||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: 'Add DNS Entry',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-dropdown
|
||||
.key=${'type'}
|
||||
.label=${'Record Type'}
|
||||
.options=${[
|
||||
{key: 'A', option: 'A - IPv4 Address'},
|
||||
{key: 'AAAA', option: 'AAAA - IPv6 Address'},
|
||||
{key: 'CNAME', option: 'CNAME - Canonical Name'},
|
||||
{key: 'MX', option: 'MX - Mail Exchange'},
|
||||
{key: 'TXT', option: 'TXT - Text Record'},
|
||||
{key: 'NS', option: 'NS - Name Server'},
|
||||
{key: 'SOA', option: 'SOA - Start of Authority'},
|
||||
{key: 'SRV', option: 'SRV - Service'},
|
||||
{key: 'CAA', option: 'CAA - Certification Authority'},
|
||||
{key: 'PTR', option: 'PTR - Pointer'},
|
||||
]}
|
||||
.value=${'A'}
|
||||
.required=${true}>
|
||||
</dees-input-dropdown>
|
||||
<dees-input-dropdown
|
||||
.key=${'domainId'}
|
||||
.label=${'Domain'}
|
||||
.options=${this.data.domains?.map(domain => ({
|
||||
key: domain.id,
|
||||
option: domain.data.name
|
||||
})) || []}
|
||||
.required=${true}>
|
||||
</dees-input-dropdown>
|
||||
<dees-input-text
|
||||
.key=${'name'}
|
||||
.label=${'Name'}
|
||||
.placeholder=${'@ for root, www, mail, etc.'}
|
||||
.value=${'@'}
|
||||
.required=${true}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'value'}
|
||||
.label=${'Value'}
|
||||
.placeholder=${'IP address, domain, or text value'}
|
||||
.required=${true}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'ttl'}
|
||||
.label=${'TTL (seconds)'}
|
||||
.value=${'3600'}
|
||||
.type=${'number'}
|
||||
.required=${true}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'priority'}
|
||||
.label=${'Priority (MX/SRV only)'}
|
||||
.type=${'number'}
|
||||
.placeholder=${'10'}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'weight'}
|
||||
.label=${'Weight (SRV only)'}
|
||||
.type=${'number'}
|
||||
.placeholder=${'0'}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'port'}
|
||||
.label=${'Port (SRV only)'}
|
||||
.type=${'number'}
|
||||
.placeholder=${'443'}>
|
||||
</dees-input-text>
|
||||
<dees-input-checkbox
|
||||
.key=${'active'}
|
||||
.label=${'Active'}
|
||||
.value=${true}>
|
||||
</dees-input-checkbox>
|
||||
<dees-input-text
|
||||
.key=${'description'}
|
||||
.label=${'Description (optional)'}
|
||||
.placeholder=${'What is this record for?'}>
|
||||
</dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Create DNS Entry',
|
||||
action: async (modalArg) => {
|
||||
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||
const formData = await form.gatherData();
|
||||
|
||||
await appstate.dataState.dispatchAction(appstate.createDnsEntryAction, {
|
||||
dnsEntryData: {
|
||||
type: formData.type,
|
||||
domainId: formData.domainId,
|
||||
zone: '', // Will be set by backend from domain
|
||||
name: formData.name || '@',
|
||||
value: formData.value,
|
||||
ttl: parseInt(formData.ttl) || 3600,
|
||||
priority: formData.priority ? parseInt(formData.priority) : undefined,
|
||||
weight: formData.weight ? parseInt(formData.weight) : undefined,
|
||||
port: formData.port ? parseInt(formData.port) : undefined,
|
||||
active: formData.active,
|
||||
description: formData.description || undefined,
|
||||
},
|
||||
});
|
||||
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Cancel',
|
||||
action: async (modalArg) => {
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Edit',
|
||||
iconName: 'edit',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
const dnsEntry = actionDataArg.item as plugins.interfaces.data.IDnsEntry;
|
||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Edit DNS Entry`,
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-dropdown
|
||||
.key=${'type'}
|
||||
.label=${'Record Type'}
|
||||
.options=${[
|
||||
{key: 'A', option: 'A - IPv4 Address'},
|
||||
{key: 'AAAA', option: 'AAAA - IPv6 Address'},
|
||||
{key: 'CNAME', option: 'CNAME - Canonical Name'},
|
||||
{key: 'MX', option: 'MX - Mail Exchange'},
|
||||
{key: 'TXT', option: 'TXT - Text Record'},
|
||||
{key: 'NS', option: 'NS - Name Server'},
|
||||
{key: 'SOA', option: 'SOA - Start of Authority'},
|
||||
{key: 'SRV', option: 'SRV - Service'},
|
||||
{key: 'CAA', option: 'CAA - Certification Authority'},
|
||||
{key: 'PTR', option: 'PTR - Pointer'},
|
||||
]}
|
||||
.value=${dnsEntry.data.type}
|
||||
.required=${true}>
|
||||
</dees-input-dropdown>
|
||||
<dees-input-dropdown
|
||||
.key=${'domainId'}
|
||||
.label=${'Domain'}
|
||||
.options=${this.data.domains?.map(domain => ({
|
||||
key: domain.id,
|
||||
option: domain.data.name
|
||||
})) || []}
|
||||
.value=${dnsEntry.data.domainId || ''}
|
||||
.required=${true}>
|
||||
</dees-input-dropdown>
|
||||
<dees-input-text
|
||||
.key=${'name'}
|
||||
.label=${'Name'}
|
||||
.value=${dnsEntry.data.name}
|
||||
.required=${true}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'value'}
|
||||
.label=${'Value'}
|
||||
.value=${dnsEntry.data.value}
|
||||
.required=${true}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'ttl'}
|
||||
.label=${'TTL (seconds)'}
|
||||
.value=${dnsEntry.data.ttl}
|
||||
.type=${'number'}
|
||||
.required=${true}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'priority'}
|
||||
.label=${'Priority (MX/SRV only)'}
|
||||
.value=${dnsEntry.data.priority || ''}
|
||||
.type=${'number'}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'weight'}
|
||||
.label=${'Weight (SRV only)'}
|
||||
.value=${dnsEntry.data.weight || ''}
|
||||
.type=${'number'}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'port'}
|
||||
.label=${'Port (SRV only)'}
|
||||
.value=${dnsEntry.data.port || ''}
|
||||
.type=${'number'}>
|
||||
</dees-input-text>
|
||||
<dees-input-checkbox
|
||||
.key=${'active'}
|
||||
.label=${'Active'}
|
||||
.value=${dnsEntry.data.active}>
|
||||
</dees-input-checkbox>
|
||||
<dees-input-text
|
||||
.key=${'description'}
|
||||
.label=${'Description (optional)'}
|
||||
.value=${dnsEntry.data.description || ''}>
|
||||
</dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Update DNS Entry',
|
||||
action: async (modalArg) => {
|
||||
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||
const formData = await form.gatherData();
|
||||
|
||||
await appstate.dataState.dispatchAction(appstate.updateDnsEntryAction, {
|
||||
dnsEntryId: dnsEntry.id,
|
||||
dnsEntryData: {
|
||||
...dnsEntry.data,
|
||||
type: formData.type,
|
||||
domainId: formData.domainId,
|
||||
zone: '', // Will be set by backend from domain
|
||||
name: formData.name || '@',
|
||||
value: formData.value,
|
||||
ttl: parseInt(formData.ttl) || 3600,
|
||||
priority: formData.priority ? parseInt(formData.priority) : undefined,
|
||||
weight: formData.weight ? parseInt(formData.weight) : undefined,
|
||||
port: formData.port ? parseInt(formData.port) : undefined,
|
||||
active: formData.active,
|
||||
description: formData.description || undefined,
|
||||
},
|
||||
});
|
||||
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Cancel',
|
||||
action: async (modalArg) => {
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Duplicate',
|
||||
iconName: 'copy',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
const dnsEntry = actionDataArg.item as plugins.interfaces.data.IDnsEntry;
|
||||
await appstate.dataState.dispatchAction(appstate.createDnsEntryAction, {
|
||||
dnsEntryData: {
|
||||
...dnsEntry.data,
|
||||
description: `Copy of ${dnsEntry.data.description || dnsEntry.data.name}`,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Toggle Active',
|
||||
iconName: 'power',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
const dnsEntry = actionDataArg.item as plugins.interfaces.data.IDnsEntry;
|
||||
await appstate.dataState.dispatchAction(appstate.updateDnsEntryAction, {
|
||||
dnsEntryId: dnsEntry.id,
|
||||
dnsEntryData: {
|
||||
...dnsEntry.data,
|
||||
active: !dnsEntry.data.active,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
iconName: 'trash',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
const dnsEntry = actionDataArg.item as plugins.interfaces.data.IDnsEntry;
|
||||
plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Delete DNS Entry`,
|
||||
content: html`
|
||||
<div style="text-align:center">
|
||||
Are you sure you want to delete this DNS entry?
|
||||
</div>
|
||||
<div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;">
|
||||
<div style="color: #fff; font-weight: bold;">
|
||||
${dnsEntry.data.type} - ${dnsEntry.data.name}.${dnsEntry.data.zone}
|
||||
</div>
|
||||
<div style="color: #aaa; font-size: 0.9em; margin-top: 4px;">
|
||||
${dnsEntry.data.value}
|
||||
</div>
|
||||
${dnsEntry.data.description ? html`
|
||||
<div style="color: #888; font-size: 0.85em; margin-top: 8px;">
|
||||
${dnsEntry.data.description}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Cancel',
|
||||
action: async (modalArg) => {
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
action: async (modalArg) => {
|
||||
await appstate.dataState.dispatchAction(appstate.deleteDnsEntryAction, {
|
||||
dnsEntryId: dnsEntry.id,
|
||||
});
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
@@ -1,529 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from '../elements/shared/index.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as appstate from '../appstate.js';
|
||||
|
||||
@customElement('cloudly-view-domains')
|
||||
export class CloudlyViewDomains extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = {
|
||||
secretGroups: [],
|
||||
secretBundles: [],
|
||||
domains: [],
|
||||
dnsEntries: [],
|
||||
};
|
||||
|
||||
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 {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
}
|
||||
.status-active { background: #4CAF50; }
|
||||
.status-pending { background: #FF9800; }
|
||||
.status-expired { background: #f44336; }
|
||||
.status-suspended { background: #9E9E9E; }
|
||||
.status-transferred { background: #607D8B; }
|
||||
|
||||
.verification-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
}
|
||||
.verification-verified { background: #4CAF50; color: white; }
|
||||
.verification-pending { background: #FF9800; color: white; }
|
||||
.verification-failed { background: #f44336; color: white; }
|
||||
.verification-not_required { background: #E0E0E0; color: #333; }
|
||||
|
||||
.ssl-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.ssl-active { color: #4CAF50; }
|
||||
.ssl-pending { color: #FF9800; }
|
||||
.ssl-expired { color: #f44336; }
|
||||
.ssl-none { color: #9E9E9E; }
|
||||
|
||||
.nameserver-list {
|
||||
font-size: 0.85em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.expiry-warning {
|
||||
color: #FF9800;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.expiry-critical {
|
||||
color: #f44336;
|
||||
font-weight: bold;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
private getStatusBadge(status: string) {
|
||||
return html`<span class="status-badge status-${status}">${status.toUpperCase()}</span>`;
|
||||
}
|
||||
|
||||
private getVerificationBadge(status: string) {
|
||||
const displayText = status === 'not_required' ? 'Not Required' : status.replace('_', ' ').toUpperCase();
|
||||
return html`<span class="verification-badge verification-${status}">${displayText}</span>`;
|
||||
}
|
||||
|
||||
private getSslBadge(sslStatus?: string) {
|
||||
if (!sslStatus) return html`<span class="ssl-badge ssl-none">—</span>`;
|
||||
const icon = sslStatus === 'active' ? '🔒' : sslStatus === 'expired' ? '⚠️' : '🔓';
|
||||
return html`<span class="ssl-badge ssl-${sslStatus}">${icon} ${sslStatus.toUpperCase()}</span>`;
|
||||
}
|
||||
|
||||
private formatDate(timestamp?: number) {
|
||||
if (!timestamp) return '—';
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
private getDaysUntilExpiry(expiresAt?: number) {
|
||||
if (!expiresAt) return null;
|
||||
const days = Math.floor((expiresAt - Date.now()) / (1000 * 60 * 60 * 24));
|
||||
return days;
|
||||
}
|
||||
|
||||
private getExpiryDisplay(expiresAt?: number) {
|
||||
const days = this.getDaysUntilExpiry(expiresAt);
|
||||
if (days === null) return '—';
|
||||
|
||||
if (days < 0) {
|
||||
return html`<span class="expiry-critical">Expired ${Math.abs(days)} days ago</span>`;
|
||||
} else if (days <= 30) {
|
||||
return html`<span class="expiry-warning">Expires in ${days} days</span>`;
|
||||
} else {
|
||||
return `${days} days`;
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<cloudly-sectionheading>Domain Management</cloudly-sectionheading>
|
||||
<dees-table
|
||||
.heading1=${'Domains'}
|
||||
.heading2=${'Manage your domains and DNS zones'}
|
||||
.data=${this.data.domains || []}
|
||||
.displayFunction=${(itemArg: plugins.interfaces.data.IDomain) => {
|
||||
const dnsCount = this.data.dnsEntries?.filter(dns => dns.data.zone === itemArg.data.name).length || 0;
|
||||
return {
|
||||
Domain: html`
|
||||
<div>
|
||||
<div style="font-weight: 500;">${itemArg.data.name}</div>
|
||||
${itemArg.data.description ? html`<div style="font-size: 0.85em; color: #666; margin-top: 2px;">${itemArg.data.description}</div>` : ''}
|
||||
</div>
|
||||
`,
|
||||
Status: this.getStatusBadge(itemArg.data.status),
|
||||
Verification: this.getVerificationBadge(itemArg.data.verificationStatus),
|
||||
SSL: this.getSslBadge(itemArg.data.sslStatus),
|
||||
'DNS Records': dnsCount,
|
||||
Registrar: itemArg.data.registrar?.name || '—',
|
||||
Expires: this.getExpiryDisplay(itemArg.data.expiresAt),
|
||||
'Auto-Renew': itemArg.data.autoRenew ? '✓' : '✗',
|
||||
Nameservers: html`<div class="nameserver-list">${itemArg.data.nameservers?.join(', ') || '—'}</div>`,
|
||||
};
|
||||
}}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'Add Domain',
|
||||
iconName: 'plus',
|
||||
type: ['header', 'footer'],
|
||||
actionFunc: async (dataActionArg) => {
|
||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: 'Add Domain',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text
|
||||
.key=${'name'}
|
||||
.label=${'Domain Name'}
|
||||
.placeholder=${'example.com'}
|
||||
.required=${true}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'description'}
|
||||
.label=${'Description'}
|
||||
.placeholder=${'Main company domain'}>
|
||||
</dees-input-text>
|
||||
<dees-input-dropdown
|
||||
.key=${'status'}
|
||||
.label=${'Status'}
|
||||
.options=${[
|
||||
{key: 'active', option: 'Active'},
|
||||
{key: 'pending', option: 'Pending'},
|
||||
{key: 'expired', option: 'Expired'},
|
||||
{key: 'suspended', option: 'Suspended'},
|
||||
]}
|
||||
.value=${'pending'}
|
||||
.required=${true}>
|
||||
</dees-input-dropdown>
|
||||
<dees-input-text
|
||||
.key=${'nameservers'}
|
||||
.label=${'Nameservers (comma separated)'}
|
||||
.placeholder=${'ns1.example.com, ns2.example.com'}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'registrarName'}
|
||||
.label=${'Registrar Name'}
|
||||
.placeholder=${'GoDaddy, Namecheap, etc.'}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'registrarUrl'}
|
||||
.label=${'Registrar URL'}
|
||||
.placeholder=${'https://registrar.com'}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'expiresAt'}
|
||||
.label=${'Expiration Date'}
|
||||
.type=${'date'}>
|
||||
</dees-input-text>
|
||||
<dees-input-checkbox
|
||||
.key=${'autoRenew'}
|
||||
.label=${'Auto-Renew Enabled'}
|
||||
.value=${true}>
|
||||
</dees-input-checkbox>
|
||||
<dees-input-checkbox
|
||||
.key=${'dnssecEnabled'}
|
||||
.label=${'DNSSEC Enabled'}
|
||||
.value=${false}>
|
||||
</dees-input-checkbox>
|
||||
<dees-input-checkbox
|
||||
.key=${'isPrimary'}
|
||||
.label=${'Primary Domain'}
|
||||
.value=${false}>
|
||||
</dees-input-checkbox>
|
||||
<dees-input-text
|
||||
.key=${'tags'}
|
||||
.label=${'Tags (comma separated)'}
|
||||
.placeholder=${'production, critical'}>
|
||||
</dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Create Domain',
|
||||
action: async (modalArg) => {
|
||||
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||
const formData = await form.gatherData();
|
||||
|
||||
const nameservers = formData.nameservers
|
||||
? formData.nameservers.split(',').map((ns: string) => ns.trim()).filter((ns: string) => ns)
|
||||
: [];
|
||||
|
||||
const tags = formData.tags
|
||||
? formData.tags.split(',').map((tag: string) => tag.trim()).filter((tag: string) => tag)
|
||||
: [];
|
||||
|
||||
await appstate.dataState.dispatchAction(appstate.createDomainAction, {
|
||||
domainData: {
|
||||
name: formData.name,
|
||||
description: formData.description || undefined,
|
||||
status: formData.status,
|
||||
verificationStatus: 'pending',
|
||||
nameservers,
|
||||
registrar: formData.registrarName ? {
|
||||
name: formData.registrarName,
|
||||
url: formData.registrarUrl || undefined,
|
||||
} : undefined,
|
||||
expiresAt: formData.expiresAt ? new Date(formData.expiresAt).getTime() : undefined,
|
||||
autoRenew: formData.autoRenew,
|
||||
dnssecEnabled: formData.dnssecEnabled,
|
||||
isPrimary: formData.isPrimary,
|
||||
tags: tags.length > 0 ? tags : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Cancel',
|
||||
action: async (modalArg) => {
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Edit',
|
||||
iconName: 'edit',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
const domain = actionDataArg.item as plugins.interfaces.data.IDomain;
|
||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Edit Domain: ${domain.data.name}`,
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text
|
||||
.key=${'name'}
|
||||
.label=${'Domain Name'}
|
||||
.value=${domain.data.name}
|
||||
.required=${true}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'description'}
|
||||
.label=${'Description'}
|
||||
.value=${domain.data.description || ''}>
|
||||
</dees-input-text>
|
||||
<dees-input-dropdown
|
||||
.key=${'status'}
|
||||
.label=${'Status'}
|
||||
.options=${[
|
||||
{key: 'active', option: 'Active'},
|
||||
{key: 'pending', option: 'Pending'},
|
||||
{key: 'expired', option: 'Expired'},
|
||||
{key: 'suspended', option: 'Suspended'},
|
||||
{key: 'transferred', option: 'Transferred'},
|
||||
]}
|
||||
.value=${domain.data.status}
|
||||
.required=${true}>
|
||||
</dees-input-dropdown>
|
||||
<dees-input-text
|
||||
.key=${'nameservers'}
|
||||
.label=${'Nameservers (comma separated)'}
|
||||
.value=${domain.data.nameservers?.join(', ') || ''}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'registrarName'}
|
||||
.label=${'Registrar Name'}
|
||||
.value=${domain.data.registrar?.name || ''}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'registrarUrl'}
|
||||
.label=${'Registrar URL'}
|
||||
.value=${domain.data.registrar?.url || ''}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'expiresAt'}
|
||||
.label=${'Expiration Date'}
|
||||
.type=${'date'}
|
||||
.value=${domain.data.expiresAt ? new Date(domain.data.expiresAt).toISOString().split('T')[0] : ''}>
|
||||
</dees-input-text>
|
||||
<dees-input-checkbox
|
||||
.key=${'autoRenew'}
|
||||
.label=${'Auto-Renew Enabled'}
|
||||
.value=${domain.data.autoRenew}>
|
||||
</dees-input-checkbox>
|
||||
<dees-input-checkbox
|
||||
.key=${'dnssecEnabled'}
|
||||
.label=${'DNSSEC Enabled'}
|
||||
.value=${domain.data.dnssecEnabled || false}>
|
||||
</dees-input-checkbox>
|
||||
<dees-input-checkbox
|
||||
.key=${'isPrimary'}
|
||||
.label=${'Primary Domain'}
|
||||
.value=${domain.data.isPrimary || false}>
|
||||
</dees-input-checkbox>
|
||||
<dees-input-text
|
||||
.key=${'tags'}
|
||||
.label=${'Tags (comma separated)'}
|
||||
.value=${domain.data.tags?.join(', ') || ''}>
|
||||
</dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Update Domain',
|
||||
action: async (modalArg) => {
|
||||
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||
const formData = await form.gatherData();
|
||||
|
||||
const nameservers = formData.nameservers
|
||||
? formData.nameservers.split(',').map((ns: string) => ns.trim()).filter((ns: string) => ns)
|
||||
: [];
|
||||
|
||||
const tags = formData.tags
|
||||
? formData.tags.split(',').map((tag: string) => tag.trim()).filter((tag: string) => tag)
|
||||
: [];
|
||||
|
||||
await appstate.dataState.dispatchAction(appstate.updateDomainAction, {
|
||||
domainId: domain.id,
|
||||
domainData: {
|
||||
...domain.data,
|
||||
name: formData.name,
|
||||
description: formData.description || undefined,
|
||||
status: formData.status,
|
||||
nameservers,
|
||||
registrar: formData.registrarName ? {
|
||||
name: formData.registrarName,
|
||||
url: formData.registrarUrl || undefined,
|
||||
} : undefined,
|
||||
expiresAt: formData.expiresAt ? new Date(formData.expiresAt).getTime() : undefined,
|
||||
autoRenew: formData.autoRenew,
|
||||
dnssecEnabled: formData.dnssecEnabled,
|
||||
isPrimary: formData.isPrimary,
|
||||
tags: tags.length > 0 ? tags : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Cancel',
|
||||
action: async (modalArg) => {
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Verify',
|
||||
iconName: 'check-circle',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
const domain = actionDataArg.item as plugins.interfaces.data.IDomain;
|
||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Verify Domain: ${domain.data.name}`,
|
||||
content: html`
|
||||
<div style="text-align:center; padding: 20px;">
|
||||
<p>Choose a verification method for <strong>${domain.data.name}</strong></p>
|
||||
<dees-form>
|
||||
<dees-input-dropdown
|
||||
.key=${'method'}
|
||||
.label=${'Verification Method'}
|
||||
.options=${[
|
||||
{key: 'dns', option: 'DNS TXT Record'},
|
||||
{key: 'http', option: 'HTTP File Upload'},
|
||||
{key: 'email', option: 'Email Verification'},
|
||||
{key: 'manual', option: 'Manual Verification'},
|
||||
]}
|
||||
.value=${'dns'}
|
||||
.required=${true}>
|
||||
</dees-input-dropdown>
|
||||
</dees-form>
|
||||
${domain.data.verificationToken ? html`
|
||||
<div style="margin-top: 20px; padding: 15px; background: #333; border-radius: 8px;">
|
||||
<div style="color: #aaa; font-size: 0.9em;">Verification Token:</div>
|
||||
<code style="color: #4CAF50; word-break: break-all;">${domain.data.verificationToken}</code>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Start Verification',
|
||||
action: async (modalArg) => {
|
||||
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||
const formData = await form.gatherData();
|
||||
|
||||
await appstate.dataState.dispatchAction(appstate.verifyDomainAction, {
|
||||
domainId: domain.id,
|
||||
verificationMethod: formData.method,
|
||||
});
|
||||
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Cancel',
|
||||
action: async (modalArg) => {
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'View DNS Records',
|
||||
iconName: 'list',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
const domain = actionDataArg.item as plugins.interfaces.data.IDomain;
|
||||
// Navigate to DNS view with filter for this domain
|
||||
// TODO: Implement navigation with filter
|
||||
console.log('View DNS records for domain:', domain.data.name);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
iconName: 'trash',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
const domain = actionDataArg.item as plugins.interfaces.data.IDomain;
|
||||
const dnsCount = this.data.dnsEntries?.filter(dns => dns.data.zone === domain.data.name).length || 0;
|
||||
|
||||
plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Delete Domain`,
|
||||
content: html`
|
||||
<div style="text-align:center">
|
||||
Are you sure you want to delete this domain?
|
||||
</div>
|
||||
<div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;">
|
||||
<div style="color: #fff; font-weight: bold; font-size: 1.1em;">
|
||||
${domain.data.name}
|
||||
</div>
|
||||
${domain.data.description ? html`
|
||||
<div style="color: #aaa; margin-top: 4px;">
|
||||
${domain.data.description}
|
||||
</div>
|
||||
` : ''}
|
||||
${dnsCount > 0 ? html`
|
||||
<div style="color: #f44336; margin-top: 12px; padding: 8px; background: #1a1a1a; border-radius: 4px;">
|
||||
⚠️ This domain has ${dnsCount} DNS record${dnsCount > 1 ? 's' : ''} that will also be deleted
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Cancel',
|
||||
action: async (modalArg) => {
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
action: async (modalArg) => {
|
||||
await appstate.dataState.dispatchAction(appstate.deleteDomainAction, {
|
||||
domainId: domain.id,
|
||||
});
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
@@ -1,436 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from '../elements/shared/index.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as appstate from '../appstate.js';
|
||||
|
||||
@customElement('cloudly-view-externalregistries')
|
||||
export class CloudlyViewExternalRegistries extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = {
|
||||
secretGroups: [],
|
||||
secretBundles: [],
|
||||
externalRegistries: [],
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subscription = appstate.dataState
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((dataArg) => {
|
||||
this.data = dataArg;
|
||||
});
|
||||
this.rxSubscriptions.push(subscription);
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
// Load external registries
|
||||
await appstate.dataState.dispatchAction(appstate.getAllDataAction, {});
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
}
|
||||
.status-active { background: #4CAF50; }
|
||||
.status-inactive { background: #9E9E9E; }
|
||||
.status-error { background: #f44336; }
|
||||
.status-unverified { background: #FF9800; }
|
||||
|
||||
.type-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
}
|
||||
.type-docker { background: #2196F3; }
|
||||
.type-npm { background: #CB3837; }
|
||||
|
||||
.default-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
background: #673AB7;
|
||||
color: white;
|
||||
margin-left: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<cloudly-sectionheading>External Registries</cloudly-sectionheading>
|
||||
<dees-table
|
||||
.heading1=${'External Registries'}
|
||||
.heading2=${'Configure external Docker and NPM registries'}
|
||||
.data=${this.data.externalRegistries || []}
|
||||
.displayFunction=${(registry: plugins.interfaces.data.IExternalRegistry) => {
|
||||
return {
|
||||
Name: html`${registry.data.name}${registry.data.isDefault ? html`<span class="default-badge">DEFAULT</span>` : ''}`,
|
||||
Type: html`<span class="type-badge type-${registry.data.type}">${registry.data.type.toUpperCase()}</span>`,
|
||||
URL: registry.data.url,
|
||||
Auth: registry.data.authType === 'none' ? 'Public' : (registry.data.username || 'Token Auth'),
|
||||
Namespace: registry.data.namespace || '-',
|
||||
Status: html`<span class="status-badge status-${registry.data.status || 'unverified'}">${(registry.data.status || 'unverified').toUpperCase()}</span>`,
|
||||
'Last Verified': registry.data.lastVerified ? new Date(registry.data.lastVerified).toLocaleString() : 'Never',
|
||||
};
|
||||
}}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'Add Registry',
|
||||
iconName: 'plus',
|
||||
type: ['header', 'footer'],
|
||||
actionFunc: async (dataActionArg) => {
|
||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: 'Add External Registry',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-dropdown
|
||||
.key=${'type'}
|
||||
.label=${'Registry Type'}
|
||||
.options=${[
|
||||
{key: 'docker', option: 'Docker'},
|
||||
{key: 'npm', option: 'NPM'}
|
||||
]}
|
||||
.value=${'docker'}
|
||||
.required=${true}>
|
||||
</dees-input-dropdown>
|
||||
<dees-input-text
|
||||
.key=${'name'}
|
||||
.label=${'Registry Name'}
|
||||
.placeholder=${'My Docker Hub'}
|
||||
.required=${true}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'url'}
|
||||
.label=${'Registry URL'}
|
||||
.placeholder=${'https://index.docker.io/v2/ or registry.gitlab.com'}
|
||||
.required=${true}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'username'}
|
||||
.label=${'Username (only needed for basic auth)'}
|
||||
.placeholder=${'username or leave empty for token auth'}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'password'}
|
||||
.label=${'Password / Token (NPM _authToken, Docker access token, etc.)'}
|
||||
.placeholder=${'Token or password'}
|
||||
.isPasswordBool=${true}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'namespace'}
|
||||
.label=${'Namespace/Organization (optional)'}
|
||||
.placeholder=${'myorg'}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'description'}
|
||||
.label=${'Description (optional)'}
|
||||
.placeholder=${'Production Docker registry'}>
|
||||
</dees-input-text>
|
||||
<dees-input-dropdown
|
||||
.key=${'authType'}
|
||||
.label=${'Authentication Type'}
|
||||
.options=${[
|
||||
{key: 'none', option: 'No Authentication (Public Registry)'},
|
||||
{key: 'basic', option: 'Basic Auth (Username + Password)'},
|
||||
{key: 'token', option: 'Token Only (NPM, GitHub, GitLab tokens)'},
|
||||
{key: 'oauth2', option: 'OAuth2 (Advanced)'}
|
||||
]}
|
||||
.value=${'none'}>
|
||||
</dees-input-dropdown>
|
||||
<dees-input-checkbox
|
||||
.key=${'isDefault'}
|
||||
.label=${'Set as default registry for this type'}
|
||||
.value=${false}>
|
||||
</dees-input-checkbox>
|
||||
<dees-input-checkbox
|
||||
.key=${'insecure'}
|
||||
.label=${'Allow insecure connections (HTTP/self-signed certs)'}
|
||||
.value=${false}>
|
||||
</dees-input-checkbox>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Create Registry',
|
||||
action: async (modalArg) => {
|
||||
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||
const formData = await form.gatherData();
|
||||
|
||||
await appstate.dataState.dispatchAction(appstate.createExternalRegistryAction, {
|
||||
registryData: {
|
||||
type: formData.type,
|
||||
name: formData.name,
|
||||
url: formData.url,
|
||||
username: formData.username,
|
||||
password: formData.password,
|
||||
namespace: formData.namespace || undefined,
|
||||
description: formData.description || undefined,
|
||||
authType: formData.authType,
|
||||
isDefault: formData.isDefault,
|
||||
insecure: formData.insecure,
|
||||
},
|
||||
});
|
||||
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Cancel',
|
||||
action: async (modalArg) => {
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Edit',
|
||||
iconName: 'edit',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
const registry = actionDataArg.item as plugins.interfaces.data.IExternalRegistry;
|
||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Edit Registry: ${registry.data.name}`,
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-dropdown
|
||||
.key=${'type'}
|
||||
.label=${'Registry Type'}
|
||||
.options=${[
|
||||
{key: 'docker', option: 'Docker'},
|
||||
{key: 'npm', option: 'NPM'}
|
||||
]}
|
||||
.value=${registry.data.type}
|
||||
.required=${true}>
|
||||
</dees-input-dropdown>
|
||||
<dees-input-text
|
||||
.key=${'name'}
|
||||
.label=${'Registry Name'}
|
||||
.value=${registry.data.name}
|
||||
.required=${true}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'url'}
|
||||
.label=${'Registry URL'}
|
||||
.value=${registry.data.url}
|
||||
.required=${true}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'username'}
|
||||
.label=${'Username (only needed for basic auth)'}
|
||||
.value=${registry.data.username || ''}
|
||||
.placeholder=${'Leave empty for token auth'}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'password'}
|
||||
.label=${'Password / Token (leave empty to keep current)'}
|
||||
.placeholder=${'New token or password'}
|
||||
.isPasswordBool=${true}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'namespace'}
|
||||
.label=${'Namespace/Organization (optional)'}
|
||||
.value=${registry.data.namespace || ''}>
|
||||
</dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'description'}
|
||||
.label=${'Description (optional)'}
|
||||
.value=${registry.data.description || ''}>
|
||||
</dees-input-text>
|
||||
<dees-input-dropdown
|
||||
.key=${'authType'}
|
||||
.label=${'Authentication Type'}
|
||||
.options=${[
|
||||
{key: 'none', option: 'No Authentication (Public Registry)'},
|
||||
{key: 'basic', option: 'Basic Auth (Username + Password)'},
|
||||
{key: 'token', option: 'Token Only (NPM, GitHub, GitLab tokens)'},
|
||||
{key: 'oauth2', option: 'OAuth2 (Advanced)'}
|
||||
]}
|
||||
.value=${registry.data.authType || 'none'}>
|
||||
</dees-input-dropdown>
|
||||
<dees-input-checkbox
|
||||
.key=${'isDefault'}
|
||||
.label=${'Set as default registry for this type'}
|
||||
.value=${registry.data.isDefault || false}>
|
||||
</dees-input-checkbox>
|
||||
<dees-input-checkbox
|
||||
.key=${'insecure'}
|
||||
.label=${'Allow insecure connections (HTTP/self-signed certs)'}
|
||||
.value=${registry.data.insecure || false}>
|
||||
</dees-input-checkbox>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Update Registry',
|
||||
action: async (modalArg) => {
|
||||
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||
const formData = await form.gatherData();
|
||||
|
||||
const updateData: any = {
|
||||
type: formData.type,
|
||||
name: formData.name,
|
||||
url: formData.url,
|
||||
username: formData.username,
|
||||
namespace: formData.namespace || undefined,
|
||||
description: formData.description || undefined,
|
||||
authType: formData.authType,
|
||||
isDefault: formData.isDefault,
|
||||
insecure: formData.insecure,
|
||||
};
|
||||
|
||||
// Only include password if it was changed
|
||||
if (formData.password) {
|
||||
updateData.password = formData.password;
|
||||
} else {
|
||||
updateData.password = registry.data.password;
|
||||
}
|
||||
|
||||
await appstate.dataState.dispatchAction(appstate.updateExternalRegistryAction, {
|
||||
registryId: registry.id,
|
||||
registryData: updateData,
|
||||
});
|
||||
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Cancel',
|
||||
action: async (modalArg) => {
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Test Connection',
|
||||
iconName: 'check-circle',
|
||||
type: ['contextmenu'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
const registry = actionDataArg.item as plugins.interfaces.data.IExternalRegistry;
|
||||
|
||||
// Show loading modal
|
||||
const loadingModal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: 'Testing Registry Connection',
|
||||
content: html`
|
||||
<div style="text-align: center; padding: 20px;">
|
||||
<dees-spinner></dees-spinner>
|
||||
<p style="margin-top: 20px;">Testing connection to ${registry.data.name}...</p>
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [],
|
||||
});
|
||||
|
||||
// Test the connection
|
||||
await appstate.dataState.dispatchAction(appstate.verifyExternalRegistryAction, {
|
||||
registryId: registry.id,
|
||||
});
|
||||
|
||||
// Close loading modal
|
||||
await loadingModal.destroy();
|
||||
|
||||
// Get updated registry
|
||||
const updatedRegistry = this.data.externalRegistries?.find(r => r.id === registry.id);
|
||||
|
||||
// Show result modal
|
||||
const resultModal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: 'Connection Test Result',
|
||||
content: html`
|
||||
<div style="text-align: center; padding: 20px;">
|
||||
${updatedRegistry?.data.status === 'active' ? html`
|
||||
<div style="color: #4CAF50; font-size: 48px;">✓</div>
|
||||
<p style="margin-top: 20px; color: #4CAF50;">Connection successful!</p>
|
||||
` : html`
|
||||
<div style="color: #f44336; font-size: 48px;">✗</div>
|
||||
<p style="margin-top: 20px; color: #f44336;">Connection failed!</p>
|
||||
${updatedRegistry?.data.lastError ? html`
|
||||
<p style="margin-top: 10px; font-size: 0.9em; color: #999;">
|
||||
Error: ${updatedRegistry.data.lastError}
|
||||
</p>
|
||||
` : ''}
|
||||
`}
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'OK',
|
||||
action: async (modalArg) => {
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
iconName: 'trash',
|
||||
type: ['contextmenu'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
const registry = actionDataArg.item as plugins.interfaces.data.IExternalRegistry;
|
||||
plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Delete Registry: ${registry.data.name}`,
|
||||
content: html`
|
||||
<div style="text-align:center">
|
||||
<p>Do you really want to delete this external registry?</p>
|
||||
<p style="color: #999; font-size: 0.9em; margin-top: 10px;">
|
||||
This will remove all stored credentials and configuration.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
||||
>
|
||||
${registry.data.name} (${registry.data.url})
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Cancel',
|
||||
action: async (modalArg) => {
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
action: async (modalArg) => {
|
||||
await appstate.dataState.dispatchAction(appstate.deleteExternalRegistryAction, {
|
||||
registryId: registry.id,
|
||||
});
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
@@ -1,292 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from '../elements/shared/index.js';
|
||||
|
||||
import { DeesElement, customElement, html, state, css, cssManager } from '@design.estate/dees-element';
|
||||
|
||||
import * as appstate from '../appstate.js';
|
||||
|
||||
@customElement('cloudly-view-images')
|
||||
export class CloudlyViewImages extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = {};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
appstate.dataState
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((dataArg) => {
|
||||
this.data = dataArg;
|
||||
});
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<cloudly-sectionheading>Images</cloudly-sectionheading>
|
||||
<dees-table
|
||||
heading1="Images"
|
||||
heading2="an image is needed for running a service"
|
||||
.data=${this.data.images}
|
||||
.displayFunction=${(image: plugins.interfaces.data.IImage) => {
|
||||
return {
|
||||
id: image.id,
|
||||
name: image.data.name,
|
||||
description: image.data.description,
|
||||
versions: image.data.versions.length,
|
||||
};
|
||||
}}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'create Image',
|
||||
type: ['header', 'footer'],
|
||||
iconName: 'plus',
|
||||
actionFunc: async () => {
|
||||
plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: 'create new Image',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text
|
||||
.label=${'name'}
|
||||
.key=${'data.name'}
|
||||
.value=${''}
|
||||
></dees-input-text>
|
||||
<dees-input-text
|
||||
.label=${'description'}
|
||||
.key=${'data.description'}
|
||||
.value=${''}
|
||||
></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'cancel',
|
||||
action: async (modalArg) => {
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'save',
|
||||
action: async (modalArg) => {
|
||||
const deesForm = modalArg.shadowRoot.querySelector('dees-form');
|
||||
const formData = await deesForm.collectFormData();
|
||||
console.log(`Prepare saving of data:`);
|
||||
console.log(formData);
|
||||
await appstate.dataState.dispatchAction(appstate.createImageAction, {
|
||||
imageName: formData['data.name'] as string,
|
||||
description: formData['data.description'] as string,
|
||||
});
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'edit',
|
||||
type: ['contextmenu', 'inRow', 'doubleClick'],
|
||||
iconName: 'penToSquare',
|
||||
actionFunc: async (
|
||||
dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>
|
||||
) => {
|
||||
const environmentsArray: Array<
|
||||
plugins.interfaces.data.ISecretGroup['data']['environments'][any] & {
|
||||
environment: string;
|
||||
}
|
||||
> = [];
|
||||
for (const environmentName of Object.keys(dataArg.item.data.environments)) {
|
||||
environmentsArray.push({
|
||||
environment: environmentName,
|
||||
...dataArg.item.data.environments[environmentName],
|
||||
});
|
||||
}
|
||||
await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: 'Edit Secret',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text
|
||||
.key=${'id'}
|
||||
.disabled=${true}
|
||||
.label=${'ID'}
|
||||
.value=${dataArg.item.id}
|
||||
></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'data.name'}
|
||||
.disabled=${false}
|
||||
.label=${'name'}
|
||||
.value=${dataArg.item.data.name}
|
||||
></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'data.description'}
|
||||
.disabled=${false}
|
||||
.label=${'description'}
|
||||
.value=${dataArg.item.data.description}
|
||||
></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'data.key'}
|
||||
.disabled=${false}
|
||||
.label=${'key'}
|
||||
.value=${dataArg.item.data.key}
|
||||
></dees-input-text>
|
||||
<dees-table
|
||||
.key=${'environments'}
|
||||
.heading1=${'Environments'}
|
||||
.heading2=${'double-click to edit values'}
|
||||
.data=${environmentsArray.map((itemArg) => {
|
||||
return {
|
||||
environment: itemArg.environment,
|
||||
value: itemArg.value,
|
||||
};
|
||||
})}
|
||||
.editableFields=${['environment', 'value']}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'delete',
|
||||
iconName: 'trash',
|
||||
type: ['inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
actionDataArg.table.data.splice(
|
||||
actionDataArg.table.data.indexOf(actionDataArg.item),
|
||||
1
|
||||
);
|
||||
},
|
||||
},
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Cancel',
|
||||
iconName: null,
|
||||
action: async (modalArg) => {
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Save',
|
||||
iconName: null,
|
||||
action: async (modalArg) => {
|
||||
const data = await modalArg.shadowRoot
|
||||
.querySelector('dees-form')
|
||||
.collectFormData();
|
||||
console.log(data);
|
||||
const updatedSecretGroup: plugins.interfaces.data.ISecretGroup = {
|
||||
id: dataArg.item.id,
|
||||
data: {
|
||||
name: data['data.name'] as string,
|
||||
description: data['data.description'] as string,
|
||||
key: data['data.key'] as string,
|
||||
environments: {},
|
||||
tags: dataArg.item.data.tags,
|
||||
},
|
||||
};
|
||||
const environments: plugins.interfaces.data.ISecretGroup['data']['environments'] =
|
||||
{};
|
||||
for (const itemArg of data['environments'] as any[]) {
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'history',
|
||||
iconName: 'clockRotateLeft',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (
|
||||
dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>
|
||||
) => {
|
||||
const historyArray: Array<{
|
||||
environment: string;
|
||||
value: string;
|
||||
}> = [];
|
||||
for (const environment of Object.keys(dataArg.item.data.environments)) {
|
||||
for (const historyItem of dataArg.item.data.environments[environment].history) {
|
||||
historyArray.push({
|
||||
environment,
|
||||
value: historyItem.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `history for ${dataArg.item.data.key}`,
|
||||
content: html`
|
||||
<dees-table
|
||||
.data=${historyArray}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'delete',
|
||||
iconName: 'trash',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (
|
||||
itemArg: plugins.deesCatalog.ITableActionDataArg<(typeof historyArray)[0]>
|
||||
) => {
|
||||
console.log('delete', itemArg);
|
||||
},
|
||||
},
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'close',
|
||||
action: async (modalArg) => {
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
iconName: 'trash',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (
|
||||
itemArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.IImage>
|
||||
) => {
|
||||
plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Delete Image "${itemArg.item.data.name}"`,
|
||||
content: html`
|
||||
<div style="text-align:center">Do you really want to delete the image?</div>
|
||||
<div
|
||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
||||
>
|
||||
${itemArg.item.id}
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'cancel',
|
||||
action: async (modalArg) => {
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
action: async (modalArg) => {
|
||||
console.log(`Delete ${itemArg.item.id}`);
|
||||
await appstate.dataState.dispatchAction(appstate.deleteImageAction, {
|
||||
imageId: itemArg.item.id,
|
||||
});
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
@@ -1,130 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from '../elements/shared/index.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as appstate from '../appstate.js';
|
||||
|
||||
@customElement('cloudly-view-logs')
|
||||
export class CloudlyViewLogs extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = {
|
||||
secretGroups: [],
|
||||
secretBundles: [],
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subecription = appstate.dataState
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((dataArg) => {
|
||||
this.data = dataArg;
|
||||
});
|
||||
this.rxSubscriptions.push(subecription);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<cloudly-sectionheading>Logs</cloudly-sectionheading>
|
||||
<dees-table
|
||||
.heading1=${'Logs'}
|
||||
.heading2=${'decoded in client'}
|
||||
.data=${this.data.deployments}
|
||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
||||
return {
|
||||
id: itemArg.id,
|
||||
serverAmount: itemArg.data.servers.length,
|
||||
};
|
||||
}}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'add configBundle',
|
||||
iconName: 'plus',
|
||||
type: ['header', 'footer'],
|
||||
actionFunc: async (dataActionArg) => {
|
||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: 'Add ConfigBundle',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'data.secretGroupIds'}
|
||||
.label=${'secretGroupIds'}
|
||||
.value=${''}
|
||||
></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'data.includedTags'}
|
||||
.label=${'includedTags'}
|
||||
.value=${''}
|
||||
></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'create', action: async (modalArg) => {} },
|
||||
{
|
||||
name: 'cancel',
|
||||
action: async (modalArg) => {
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
iconName: 'trash',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
|
||||
content: html`
|
||||
<div style="text-align:center">
|
||||
Do you really want to delete the ConfigBundle?
|
||||
</div>
|
||||
<div
|
||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
||||
>
|
||||
${actionDataArg.item.id}
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'cancel',
|
||||
action: async (modalArg) => {
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
action: async (modalArg) => {
|
||||
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
|
||||
configBundleId: actionDataArg.item.id,
|
||||
});
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
@@ -1,128 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from '../elements/shared/index.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as appstate from '../appstate.js';
|
||||
|
||||
@customElement('cloudly-view-mails')
|
||||
export class CloudlyViewMails extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = {
|
||||
secretGroups: [],
|
||||
secretBundles: [],
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subecription = appstate.dataState
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((dataArg) => {
|
||||
this.data = dataArg;
|
||||
});
|
||||
this.rxSubscriptions.push(subecription);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css``
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<cloudly-sectionheading>Mails</cloudly-sectionheading>
|
||||
<dees-table
|
||||
.heading1=${'Mails'}
|
||||
.heading2=${'decoded in client'}
|
||||
.data=${this.data.deployments}
|
||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
||||
return {
|
||||
id: itemArg.id,
|
||||
serverAmount: itemArg.data.servers.length,
|
||||
};
|
||||
}}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'add configBundle',
|
||||
iconName: 'plus',
|
||||
type: ['header', 'footer'],
|
||||
actionFunc: async (dataActionArg) => {
|
||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: 'Add ConfigBundle',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'data.secretGroupIds'}
|
||||
.label=${'secretGroupIds'}
|
||||
.value=${''}
|
||||
></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'data.includedTags'}
|
||||
.label=${'includedTags'}
|
||||
.value=${''}
|
||||
></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'create', action: async (modalArg) => {} },
|
||||
{
|
||||
name: 'cancel',
|
||||
action: async (modalArg) => {
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
iconName: 'trash',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
|
||||
content: html`
|
||||
<div style="text-align:center">
|
||||
Do you really want to delete the ConfigBundle?
|
||||
</div>
|
||||
<div
|
||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
||||
>
|
||||
${actionDataArg.item.id}
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'cancel',
|
||||
action: async (modalArg) => {
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
action: async (modalArg) => {
|
||||
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
|
||||
configBundleId: actionDataArg.item.id,
|
||||
});
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
@@ -1,156 +0,0 @@
|
||||
import * as shared from '../elements/shared/index.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as appstate from '../appstate.js';
|
||||
|
||||
@customElement('cloudly-view-overview')
|
||||
export class CloudlyViewOverview extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = {
|
||||
secretGroups: [],
|
||||
secretBundles: [],
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subecription = appstate.dataState
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((dataArg) => {
|
||||
this.data = dataArg;
|
||||
});
|
||||
this.rxSubscriptions.push(subecription);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
dees-statsgrid {
|
||||
margin-top: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
// Calculate total nodes across all clusters
|
||||
const totalNodes = this.data.clusters?.reduce((sum, cluster) =>
|
||||
sum + (cluster.data.nodes?.length || 0), 0) || 0;
|
||||
|
||||
// Create tiles for the stats grid
|
||||
const statsTiles = [
|
||||
{
|
||||
id: 'clusters',
|
||||
title: 'Total Clusters',
|
||||
value: this.data.clusters?.length || 0,
|
||||
type: 'number' as const,
|
||||
iconName: 'lucide:Network',
|
||||
description: 'Active clusters'
|
||||
},
|
||||
{
|
||||
id: 'nodes',
|
||||
title: 'Total Nodes',
|
||||
value: totalNodes,
|
||||
type: 'number' as const,
|
||||
iconName: 'lucide:Server',
|
||||
description: 'Connected nodes'
|
||||
},
|
||||
{
|
||||
id: 'services',
|
||||
title: 'Services',
|
||||
value: this.data.services?.length || 0,
|
||||
type: 'number' as const,
|
||||
iconName: 'lucide:Layers',
|
||||
description: 'Deployed services'
|
||||
},
|
||||
{
|
||||
id: 'deployments',
|
||||
title: 'Deployments',
|
||||
value: this.data.deployments?.length || 0,
|
||||
type: 'number' as const,
|
||||
iconName: 'lucide:Rocket',
|
||||
description: 'Active deployments'
|
||||
},
|
||||
{
|
||||
id: 'secretGroups',
|
||||
title: 'Secret Groups',
|
||||
value: this.data.secretGroups?.length || 0,
|
||||
type: 'number' as const,
|
||||
iconName: 'lucide:ShieldCheck',
|
||||
description: 'Configured secret groups'
|
||||
},
|
||||
{
|
||||
id: 'secretBundles',
|
||||
title: 'Secret Bundles',
|
||||
value: this.data.secretBundles?.length || 0,
|
||||
type: 'number' as const,
|
||||
iconName: 'lucide:LockKeyhole',
|
||||
description: 'Available secret bundles'
|
||||
},
|
||||
{
|
||||
id: 'images',
|
||||
title: 'Images',
|
||||
value: this.data.images?.length || 0,
|
||||
type: 'number' as const,
|
||||
iconName: 'lucide:Image',
|
||||
description: 'Container images'
|
||||
},
|
||||
{
|
||||
id: 'dns',
|
||||
title: 'DNS Entries',
|
||||
value: this.data.dnsEntries?.length || 0,
|
||||
type: 'number' as const,
|
||||
iconName: 'lucide:Globe',
|
||||
description: 'Managed DNS records'
|
||||
},
|
||||
{
|
||||
id: 'databases',
|
||||
title: 'Databases',
|
||||
value: this.data.dbs?.length || 0,
|
||||
type: 'number' as const,
|
||||
iconName: 'lucide:Database',
|
||||
description: 'Database instances'
|
||||
},
|
||||
{
|
||||
id: 'backups',
|
||||
title: 'Backups',
|
||||
value: this.data.backups?.length || 0,
|
||||
type: 'number' as const,
|
||||
iconName: 'lucide:Save',
|
||||
description: 'Available backups'
|
||||
},
|
||||
{
|
||||
id: 'mails',
|
||||
title: 'Mail Domains',
|
||||
value: this.data.mails?.length || 0,
|
||||
type: 'number' as const,
|
||||
iconName: 'lucide:Mail',
|
||||
description: 'Mail configurations'
|
||||
},
|
||||
{
|
||||
id: 's3',
|
||||
title: 'S3 Buckets',
|
||||
value: this.data.s3?.length || 0,
|
||||
type: 'number' as const,
|
||||
iconName: 'lucide:Cloud',
|
||||
description: 'Storage buckets'
|
||||
}
|
||||
];
|
||||
|
||||
return html`
|
||||
<cloudly-sectionheading>Overview</cloudly-sectionheading>
|
||||
<dees-statsgrid
|
||||
.tiles=${statsTiles}
|
||||
.minTileWidth=${250}
|
||||
.gap=${16}
|
||||
></dees-statsgrid>
|
||||
`;
|
||||
}
|
||||
}
|
@@ -1,130 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from '../elements/shared/index.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as appstate from '../appstate.js';
|
||||
|
||||
@customElement('cloudly-view-s3')
|
||||
export class CloudlyViewS3 extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = {
|
||||
secretGroups: [],
|
||||
secretBundles: [],
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subecription = appstate.dataState
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((dataArg) => {
|
||||
this.data = dataArg;
|
||||
});
|
||||
this.rxSubscriptions.push(subecription);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<cloudly-sectionheading>S3</cloudly-sectionheading>
|
||||
<dees-table
|
||||
.heading1=${'S3'}
|
||||
.heading2=${'decoded in client'}
|
||||
.data=${this.data.s3}
|
||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
||||
return {
|
||||
id: itemArg.id,
|
||||
serverAmount: itemArg.data.servers.length,
|
||||
};
|
||||
}}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'add configBundle',
|
||||
iconName: 'plus',
|
||||
type: ['header', 'footer'],
|
||||
actionFunc: async (dataActionArg) => {
|
||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: 'Add ConfigBundle',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'data.secretGroupIds'}
|
||||
.label=${'secretGroupIds'}
|
||||
.value=${''}
|
||||
></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'data.includedTags'}
|
||||
.label=${'includedTags'}
|
||||
.value=${''}
|
||||
></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'create', action: async (modalArg) => {} },
|
||||
{
|
||||
name: 'cancel',
|
||||
action: async (modalArg) => {
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
iconName: 'trash',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
|
||||
content: html`
|
||||
<div style="text-align:center">
|
||||
Do you really want to delete the ConfigBundle?
|
||||
</div>
|
||||
<div
|
||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
||||
>
|
||||
${actionDataArg.item.id}
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'cancel',
|
||||
action: async (modalArg) => {
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
action: async (modalArg) => {
|
||||
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
|
||||
configBundleId: actionDataArg.item.id,
|
||||
});
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
@@ -1,167 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from '../elements/shared/index.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as appstate from '../appstate.js';
|
||||
|
||||
@customElement('cloudly-view-secretbundles')
|
||||
export class CloudlyViewSecretBundles extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = {};
|
||||
|
||||
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`
|
||||
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<cloudly-sectionheading>SecretBundles</cloudly-sectionheading>
|
||||
<dees-table
|
||||
.heading1=${'SecretBundles'}
|
||||
.heading2=${'decoded in client'}
|
||||
.data=${this.data.secretBundles || []}
|
||||
.displayFunction=${(itemArg: plugins.interfaces.data.ISecretBundle) => {
|
||||
return {
|
||||
name: itemArg.data.name,
|
||||
secretGroups: (() => {
|
||||
const secretGroupIds = itemArg.data.includedSecretGroupIds;
|
||||
let secretGroupNames: string[] = [];
|
||||
for (const secretGroupId of secretGroupIds) {
|
||||
const secretGroup = this.data.secretGroups.find(
|
||||
(secretGroupArg) => secretGroupArg.id === secretGroupId
|
||||
);
|
||||
if (secretGroup) {
|
||||
secretGroupNames.push(secretGroup.data.name);
|
||||
}
|
||||
}
|
||||
return secretGroupNames.join(', ');
|
||||
})(),
|
||||
tags: html`<dees-chips
|
||||
.selectionMode=${'none'}
|
||||
.selectableChips=${itemArg.data.includedTags}
|
||||
></dees-chips>`,
|
||||
};
|
||||
}}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'add SecretBundle',
|
||||
iconName: 'plus',
|
||||
type: ['header', 'footer'],
|
||||
actionFunc: async (dataActionArg) => {
|
||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: 'Add SecretBundle',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'data.secretGroupIds'}
|
||||
.label=${'secretGroupIds'}
|
||||
.value=${''}
|
||||
></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'data.includedTags'}
|
||||
.label=${'includedTags'}
|
||||
.value=${''}
|
||||
></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'create', action: async (modalArg) => {} },
|
||||
{
|
||||
name: 'cancel',
|
||||
action: async (modalArg) => {
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
iconName: 'trash',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
|
||||
content: html`
|
||||
<div style="text-align:center">
|
||||
Do you really want to delete the ConfigBundle?
|
||||
</div>
|
||||
<div
|
||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
||||
>
|
||||
${actionDataArg.item.id}
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'cancel',
|
||||
action: async (modalArg) => {
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
action: async (modalArg) => {
|
||||
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
|
||||
configBundleId: actionDataArg.item.id,
|
||||
});
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'edit',
|
||||
iconName: 'penToSquare',
|
||||
type: ['doubleClick', 'contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: 'Edit SecretBundle',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .label=${'purpose'}></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'save', action: async (modalArg) => {} },
|
||||
{
|
||||
name: 'cancel',
|
||||
action: async (modalArg) => {
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
@@ -1,362 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from '../elements/shared/index.js';
|
||||
|
||||
import { DeesElement, customElement, html, state, css, cssManager } from '@design.estate/dees-element';
|
||||
|
||||
import * as appstate from '../appstate.js';
|
||||
|
||||
@customElement('cloudly-view-secretsgroups')
|
||||
export class CloudlyViewSecretGroups extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = {};
|
||||
|
||||
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`
|
||||
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<cloudly-sectionheading>SecretGroups</cloudly-sectionheading>
|
||||
<dees-table
|
||||
heading1="SecretGroups"
|
||||
heading2="decoded in client"
|
||||
.data=${this.data.secretGroups || []}
|
||||
.displayFunction=${(secretGroup: plugins.interfaces.data.ISecretGroup) => {
|
||||
return {
|
||||
name: secretGroup.data.name,
|
||||
priority: secretGroup.data.priority,
|
||||
tags: html`<dees-chips
|
||||
.selectionMode=${'none'}
|
||||
.selectableChips=${secretGroup.data.tags}
|
||||
></dees-chips>`,
|
||||
key: secretGroup.data.key,
|
||||
history: (() => {
|
||||
const allHistory = [];
|
||||
for (const environment in secretGroup.data.environments) {
|
||||
allHistory.push(...secretGroup.data.environments[environment].history);
|
||||
}
|
||||
return allHistory.length;
|
||||
})(),
|
||||
};
|
||||
}}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'add SecretGroup',
|
||||
type: ['header', 'footer'],
|
||||
iconName: 'plus',
|
||||
actionFunc: async () => {
|
||||
plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: 'create new SecretGroup',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text
|
||||
.label=${'name'}
|
||||
.key=${'data.name'}
|
||||
.value=${''}
|
||||
></dees-input-text>
|
||||
<dees-input-text
|
||||
.label=${'description'}
|
||||
.key=${'data.description'}
|
||||
.value=${''}
|
||||
></dees-input-text>
|
||||
<dees-input-text
|
||||
.label=${'Secret Key (data.key)'}
|
||||
.key=${'data.key'}
|
||||
.value=${''}
|
||||
></dees-input-text>
|
||||
<dees-table
|
||||
.heading1=${'Environments'}
|
||||
.heading2=${'keys need to be unique'}
|
||||
key="environments"
|
||||
.data=${[
|
||||
{
|
||||
environment: 'production',
|
||||
value: '',
|
||||
},
|
||||
{
|
||||
environment: 'staging',
|
||||
value: '',
|
||||
},
|
||||
]}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'add environment',
|
||||
iconName: 'plus',
|
||||
type: ['footer'],
|
||||
actionFunc: async (dataArg) => {
|
||||
dataArg.table.data.push({
|
||||
environment: 'new environment',
|
||||
value: '',
|
||||
});
|
||||
dataArg.table.requestUpdate('data');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete environment',
|
||||
iconName: 'trash',
|
||||
type: ['inRow'],
|
||||
actionFunc: async (dataArg) => {
|
||||
dataArg.table.data.splice(dataArg.table.data.indexOf(dataArg.item), 1);
|
||||
dataArg.table.requestUpdate('data');
|
||||
},
|
||||
},
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
.editableFields=${['environment', 'value']}
|
||||
></dees-table>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'cancel',
|
||||
action: async (modalArg) => {
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'save',
|
||||
action: async (modalArg) => {
|
||||
const deesForm = modalArg.shadowRoot.querySelector('dees-form');
|
||||
const formData = await deesForm.collectFormData();
|
||||
console.log(`Prepare saving of data:`);
|
||||
console.log(formData);
|
||||
const environments: plugins.interfaces.data.ISecretGroup['data']['environments'] =
|
||||
{};
|
||||
for (const itemArg of formData['environments'] as any[]) {
|
||||
environments[itemArg.environment] = {
|
||||
value: itemArg.value,
|
||||
history: [],
|
||||
lastUpdated: Date.now(),
|
||||
};
|
||||
}
|
||||
await appstate.dataState.dispatchAction(appstate.createSecretGroupAction, {
|
||||
id: null,
|
||||
data: {
|
||||
name: formData['data.name'] as string,
|
||||
description: formData['data.description'] as string,
|
||||
key: formData['data.key'] as string,
|
||||
environments,
|
||||
tags: [],
|
||||
},
|
||||
});
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'edit',
|
||||
type: ['contextmenu', 'inRow', 'doubleClick'],
|
||||
iconName: 'penToSquare',
|
||||
actionFunc: async (
|
||||
dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>
|
||||
) => {
|
||||
const environmentsArray: Array<
|
||||
plugins.interfaces.data.ISecretGroup['data']['environments'][any] & {
|
||||
environment: string;
|
||||
}
|
||||
> = [];
|
||||
for (const environmentName of Object.keys(dataArg.item.data.environments)) {
|
||||
environmentsArray.push({
|
||||
environment: environmentName,
|
||||
...dataArg.item.data.environments[environmentName],
|
||||
});
|
||||
}
|
||||
await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: 'Edit Secret',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text
|
||||
.key=${'id'}
|
||||
.disabled=${true}
|
||||
.label=${'ID'}
|
||||
.value=${dataArg.item.id}
|
||||
></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'data.name'}
|
||||
.disabled=${false}
|
||||
.label=${'name'}
|
||||
.value=${dataArg.item.data.name}
|
||||
></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'data.description'}
|
||||
.disabled=${false}
|
||||
.label=${'description'}
|
||||
.value=${dataArg.item.data.description}
|
||||
></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'data.key'}
|
||||
.disabled=${false}
|
||||
.label=${'key'}
|
||||
.value=${dataArg.item.data.key}
|
||||
></dees-input-text>
|
||||
<dees-table
|
||||
.key=${'environments'}
|
||||
.heading1=${'Environments'}
|
||||
.heading2=${'double-click to edit values'}
|
||||
.data=${environmentsArray.map((itemArg) => {
|
||||
return {
|
||||
environment: itemArg.environment,
|
||||
value: itemArg.value,
|
||||
};
|
||||
})}
|
||||
.editableFields=${['environment', 'value']}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'delete',
|
||||
iconName: 'trash',
|
||||
type: ['inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
actionDataArg.table.data.splice(
|
||||
actionDataArg.table.data.indexOf(actionDataArg.item),
|
||||
1
|
||||
);
|
||||
},
|
||||
},
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Cancel',
|
||||
iconName: null,
|
||||
action: async (modalArg) => {
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Save',
|
||||
iconName: null,
|
||||
action: async (modalArg) => {
|
||||
const data = await modalArg.shadowRoot
|
||||
.querySelector('dees-form')
|
||||
.collectFormData();
|
||||
console.log(data);
|
||||
const updatedSecretGroup: plugins.interfaces.data.ISecretGroup = {
|
||||
id: dataArg.item.id,
|
||||
data: {
|
||||
name: data['data.name'] as string,
|
||||
description: data['data.description'] as string,
|
||||
key: data['data.key'] as string,
|
||||
environments: {},
|
||||
tags: dataArg.item.data.tags,
|
||||
},
|
||||
};
|
||||
const environments: plugins.interfaces.data.ISecretGroup['data']['environments'] =
|
||||
{};
|
||||
for (const itemArg of data['environments'] as any[]) {
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'history',
|
||||
iconName: 'clockRotateLeft',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (
|
||||
dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>
|
||||
) => {
|
||||
const historyArray: Array<{
|
||||
environment: string;
|
||||
value: string;
|
||||
}> = [];
|
||||
for (const environment of Object.keys(dataArg.item.data.environments)) {
|
||||
for (const historyItem of dataArg.item.data.environments[environment].history) {
|
||||
historyArray.push({
|
||||
environment,
|
||||
value: historyItem.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `history for ${dataArg.item.data.key}`,
|
||||
content: html`
|
||||
<dees-table
|
||||
.data=${historyArray}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'delete',
|
||||
iconName: 'trash',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (
|
||||
itemArg: plugins.deesCatalog.ITableActionDataArg<(typeof historyArray)[0]>
|
||||
) => {
|
||||
console.log('delete', itemArg);
|
||||
},
|
||||
},
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'close',
|
||||
action: async (modalArg) => {
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
iconName: 'trash',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (
|
||||
itemArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>
|
||||
) => {
|
||||
plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Delete ${itemArg.item.data.key}`,
|
||||
content: html`
|
||||
<div style="text-align:center">Do you really want to delete the secret?</div>
|
||||
<div
|
||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
||||
>
|
||||
${itemArg.item.data.key}
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'cancel',
|
||||
action: async (modalArg) => {
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
action: async (modalArg) => {
|
||||
console.log(`Delete ${itemArg.item.id}`);
|
||||
await appstate.dataState.dispatchAction(appstate.deleteSecretGroupAction, {
|
||||
secretGroupId: itemArg.item.id,
|
||||
});
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
@@ -1,461 +0,0 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from '../elements/shared/index.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
property,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as appstate from '../appstate.js';
|
||||
|
||||
@customElement('cloudly-view-settings')
|
||||
export class CloudlyViewSettings extends DeesElement {
|
||||
@state()
|
||||
private settings: plugins.interfaces.data.ICloudlySettingsMasked = {};
|
||||
|
||||
@state()
|
||||
private isLoading = false;
|
||||
|
||||
@state()
|
||||
private testResults: {[key: string]: {success: boolean; message: string}} = {};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.loadSettings();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
.settings-container {
|
||||
padding: 24px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.provider-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.test-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.test-status dees-button {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
}
|
||||
|
||||
.actions-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
dees-panel {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-grid.single {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
private async loadSettings() {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
// Use shared API client
|
||||
const response = await appstate.apiClient.settings.getSettings();
|
||||
this.settings = response.settings;
|
||||
} catch (error) {
|
||||
console.error('Failed to load settings:', error);
|
||||
plugins.deesCatalog.DeesToast.createAndShow({
|
||||
message: `Failed to load settings: ${error.message}`,
|
||||
type: 'error',
|
||||
});
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async saveSettings(formData: any) {
|
||||
console.log('saveSettings called with formData:', formData);
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const updates: Partial<plugins.interfaces.data.ICloudlySettings> = {};
|
||||
|
||||
// Process form data
|
||||
for (const [key, value] of Object.entries(formData)) {
|
||||
console.log(`Processing ${key}:`, value);
|
||||
if (value !== undefined && value !== '****' && !value?.toString().endsWith('****')) {
|
||||
// Only update if value changed (not masked)
|
||||
updates[key as keyof plugins.interfaces.data.ICloudlySettings] = value as string;
|
||||
}
|
||||
}
|
||||
console.log('Updates to send:', updates);
|
||||
|
||||
const response = await appstate.apiClient.settings.updateSettings(updates);
|
||||
|
||||
if (response.success) {
|
||||
plugins.deesCatalog.DeesToast.createAndShow({
|
||||
message: 'Settings saved successfully',
|
||||
type: 'success',
|
||||
});
|
||||
await this.loadSettings(); // Reload to get masked values
|
||||
} else {
|
||||
throw new Error(response.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save settings:', error);
|
||||
plugins.deesCatalog.DeesToast.createAndShow({
|
||||
message: `Failed to save settings: ${error.message}`,
|
||||
type: 'error',
|
||||
});
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async testConnection(provider: string) {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const response = await appstate.apiClient.settings.testProviderConnection(provider);
|
||||
|
||||
this.testResults = {
|
||||
...this.testResults,
|
||||
[provider]: {
|
||||
success: response.connectionValid,
|
||||
message: response.message
|
||||
}
|
||||
};
|
||||
|
||||
// Show toast notification
|
||||
plugins.deesCatalog.DeesToast.createAndShow({
|
||||
message: response.message,
|
||||
type: response.connectionValid ? 'success' : 'error',
|
||||
});
|
||||
} catch (error) {
|
||||
this.testResults = {
|
||||
...this.testResults,
|
||||
[provider]: {
|
||||
success: false,
|
||||
message: `Test failed: ${error.message}`
|
||||
}
|
||||
};
|
||||
plugins.deesCatalog.DeesToast.createAndShow({
|
||||
message: `Connection test failed: ${error.message}`,
|
||||
type: 'error',
|
||||
});
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private renderProviderStatus(provider: string) {
|
||||
const result = this.testResults[provider];
|
||||
if (!result) return '';
|
||||
|
||||
return html`
|
||||
<dees-badge
|
||||
.type=${result.success ? 'success' : 'error'}
|
||||
.text=${result.success ? 'Connected' : 'Failed'}
|
||||
></dees-badge>
|
||||
`;
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.isLoading && Object.keys(this.settings).length === 0) {
|
||||
return html`
|
||||
<div class="loading-container">
|
||||
<dees-spinner></dees-spinner>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<cloudly-sectionheading>Settings</cloudly-sectionheading>
|
||||
<div class="settings-container">
|
||||
<dees-form @formData=${(e: CustomEvent) => {
|
||||
console.log('formData event received:', e);
|
||||
console.log('Event detail:', e.detail);
|
||||
console.log('Event detail.data:', e.detail.data);
|
||||
this.saveSettings(e.detail.data);
|
||||
}}>
|
||||
|
||||
<!-- Hetzner Cloud -->
|
||||
<dees-panel
|
||||
.title=${'Hetzner Cloud'}
|
||||
.subtitle=${'Configure Hetzner Cloud API access'}
|
||||
.variant=${'outline'}
|
||||
>
|
||||
<div class="test-status">
|
||||
${this.renderProviderStatus('hetzner')}
|
||||
<dees-button
|
||||
.text=${'Test Connection'}
|
||||
.type=${'secondary'}
|
||||
@click=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.testConnection('hetzner');
|
||||
}}
|
||||
></dees-button>
|
||||
</div>
|
||||
<div class="form-grid single">
|
||||
<dees-input-text
|
||||
.key=${'hetznerToken'}
|
||||
.label=${'API Token'}
|
||||
.value=${this.settings.hetznerToken || ''}
|
||||
.isPasswordBool=${true}
|
||||
.description=${'Your Hetzner Cloud API token for managing infrastructure'}
|
||||
.required=${false}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<!-- Cloudflare -->
|
||||
<dees-panel
|
||||
.title=${'Cloudflare'}
|
||||
.subtitle=${'Configure Cloudflare API access'}
|
||||
.variant=${'outline'}
|
||||
>
|
||||
<div class="test-status">
|
||||
${this.renderProviderStatus('cloudflare')}
|
||||
<dees-button
|
||||
.text=${'Test Connection'}
|
||||
.type=${'secondary'}
|
||||
@click=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.testConnection('cloudflare');
|
||||
}}
|
||||
></dees-button>
|
||||
</div>
|
||||
<div class="form-grid single">
|
||||
<dees-input-text
|
||||
.key=${'cloudflareToken'}
|
||||
.label=${'API Token'}
|
||||
.value=${this.settings.cloudflareToken || ''}
|
||||
.isPasswordBool=${true}
|
||||
.description=${'Cloudflare API token with DNS and Zone permissions'}
|
||||
.required=${false}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<!-- AWS -->
|
||||
<dees-panel
|
||||
.title=${'Amazon Web Services'}
|
||||
.subtitle=${'Configure AWS credentials'}
|
||||
.variant=${'outline'}
|
||||
>
|
||||
<div class="test-status">
|
||||
${this.renderProviderStatus('aws')}
|
||||
<dees-button
|
||||
.text=${'Test Connection'}
|
||||
.type=${'secondary'}
|
||||
@click=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.testConnection('aws');
|
||||
}}
|
||||
></dees-button>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<dees-input-text
|
||||
.key=${'awsAccessKey'}
|
||||
.label=${'Access Key ID'}
|
||||
.value=${this.settings.awsAccessKey || ''}
|
||||
.isPasswordBool=${true}
|
||||
.description=${'AWS IAM access key identifier'}
|
||||
.required=${false}
|
||||
></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'awsSecretKey'}
|
||||
.label=${'Secret Access Key'}
|
||||
.value=${this.settings.awsSecretKey || ''}
|
||||
.isPasswordBool=${true}
|
||||
.description=${'AWS IAM secret access key'}
|
||||
.required=${false}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
<div class="form-grid single">
|
||||
<dees-input-dropdown
|
||||
.key=${'awsRegion'}
|
||||
.label=${'Default Region'}
|
||||
.selectedOption=${this.settings.awsRegion || 'us-east-1'}
|
||||
.options=${[
|
||||
{ key: 'us-east-1', option: 'US East (N. Virginia)', payload: null },
|
||||
{ key: 'us-west-2', option: 'US West (Oregon)', payload: null },
|
||||
{ key: 'eu-west-1', option: 'EU (Ireland)', payload: null },
|
||||
{ key: 'eu-central-1', option: 'EU (Frankfurt)', payload: null },
|
||||
{ key: 'ap-southeast-1', option: 'Asia Pacific (Singapore)', payload: null },
|
||||
{ key: 'ap-northeast-1', option: 'Asia Pacific (Tokyo)', payload: null },
|
||||
]}
|
||||
.description=${'Default AWS region for resource provisioning'}
|
||||
></dees-input-dropdown>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<!-- DigitalOcean -->
|
||||
<dees-panel
|
||||
.title=${'DigitalOcean'}
|
||||
.subtitle=${'Configure DigitalOcean API access'}
|
||||
.variant=${'outline'}
|
||||
>
|
||||
<div class="test-status">
|
||||
${this.renderProviderStatus('digitalocean')}
|
||||
<dees-button
|
||||
.text=${'Test Connection'}
|
||||
.type=${'secondary'}
|
||||
@click=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.testConnection('digitalocean');
|
||||
}}
|
||||
></dees-button>
|
||||
</div>
|
||||
<div class="form-grid single">
|
||||
<dees-input-text
|
||||
.key=${'digitalOceanToken'}
|
||||
.label=${'Personal Access Token'}
|
||||
.value=${this.settings.digitalOceanToken || ''}
|
||||
.isPasswordBool=${true}
|
||||
.description=${'DigitalOcean personal access token with read/write scope'}
|
||||
.required=${false}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<!-- Azure -->
|
||||
<dees-panel
|
||||
.title=${'Microsoft Azure'}
|
||||
.subtitle=${'Configure Azure service principal'}
|
||||
.variant=${'outline'}
|
||||
>
|
||||
<div class="test-status">
|
||||
${this.renderProviderStatus('azure')}
|
||||
<dees-button
|
||||
.text=${'Test Connection'}
|
||||
.type=${'secondary'}
|
||||
@click=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.testConnection('azure');
|
||||
}}
|
||||
></dees-button>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<dees-input-text
|
||||
.key=${'azureClientId'}
|
||||
.label=${'Application (Client) ID'}
|
||||
.value=${this.settings.azureClientId || ''}
|
||||
.isPasswordBool=${true}
|
||||
.description=${'Azure AD application client ID'}
|
||||
.required=${false}
|
||||
></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'azureClientSecret'}
|
||||
.label=${'Client Secret'}
|
||||
.value=${this.settings.azureClientSecret || ''}
|
||||
.isPasswordBool=${true}
|
||||
.description=${'Azure AD application client secret'}
|
||||
.required=${false}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<dees-input-text
|
||||
.key=${'azureTenantId'}
|
||||
.label=${'Directory (Tenant) ID'}
|
||||
.value=${this.settings.azureTenantId || ''}
|
||||
.description=${'Azure AD tenant identifier'}
|
||||
.required=${false}
|
||||
></dees-input-text>
|
||||
<dees-input-text
|
||||
.key=${'azureSubscriptionId'}
|
||||
.label=${'Subscription ID'}
|
||||
.value=${this.settings.azureSubscriptionId || ''}
|
||||
.description=${'Azure subscription for resource management'}
|
||||
.required=${false}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<!-- Google Cloud -->
|
||||
<dees-panel
|
||||
.title=${'Google Cloud Platform'}
|
||||
.subtitle=${'Configure GCP service account'}
|
||||
.variant=${'outline'}
|
||||
>
|
||||
<div class="test-status">
|
||||
${this.renderProviderStatus('google')}
|
||||
<dees-button
|
||||
.text=${'Test Connection'}
|
||||
.type=${'secondary'}
|
||||
@click=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.testConnection('google');
|
||||
}}
|
||||
></dees-button>
|
||||
</div>
|
||||
<div class="form-grid single">
|
||||
<dees-input-textarea
|
||||
.key=${'googleCloudKeyJson'}
|
||||
.label=${'Service Account Key (JSON)'}
|
||||
.value=${this.settings.googleCloudKeyJson || ''}
|
||||
.isPasswordBool=${true}
|
||||
.description=${'Complete JSON key file for service account authentication'}
|
||||
.required=${false}
|
||||
></dees-input-textarea>
|
||||
</div>
|
||||
<div class="form-grid single">
|
||||
<dees-input-text
|
||||
.key=${'googleCloudProjectId'}
|
||||
.label=${'Project ID'}
|
||||
.value=${this.settings.googleCloudProjectId || ''}
|
||||
.description=${'Google Cloud project identifier'}
|
||||
.required=${false}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<div class="actions-container">
|
||||
<dees-form-submit
|
||||
.text=${'Save All Settings'}
|
||||
.disabled=${this.isLoading}
|
||||
></dees-form-submit>
|
||||
</div>
|
||||
</dees-form>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
@@ -1,914 +0,0 @@
|
||||
import * as shared from '../elements/shared/index.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
property,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as appstate from '../appstate.js';
|
||||
|
||||
@customElement('cloudly-view-tasks')
|
||||
export class CloudlyViewTasks extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = {};
|
||||
|
||||
@state()
|
||||
private selectedExecution: plugins.interfaces.data.ITaskExecution | null = null;
|
||||
|
||||
@state()
|
||||
private loading = false;
|
||||
|
||||
@state()
|
||||
private filterStatus: string = 'all';
|
||||
|
||||
@state()
|
||||
private searchQuery: string = '';
|
||||
|
||||
@state()
|
||||
private categoryFilter: string = 'all';
|
||||
|
||||
@state()
|
||||
private autoRefresh: boolean = true;
|
||||
|
||||
private _refreshHandle: any = null;
|
||||
@state()
|
||||
private canceling: Record<string, boolean> = {};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subscription = appstate.dataState
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((dataArg) => {
|
||||
this.data = dataArg;
|
||||
});
|
||||
this.rxSubscriptions.push(subscription);
|
||||
|
||||
// Load initial data (non-blocking)
|
||||
this.loadInitialData();
|
||||
|
||||
// Start periodic refresh (lightweight; executions only by default)
|
||||
this.startAutoRefresh();
|
||||
}
|
||||
|
||||
private async loadInitialData() {
|
||||
try {
|
||||
await appstate.dataState.dispatchAction(appstate.taskActions.getTasks, {});
|
||||
await appstate.dataState.dispatchAction(appstate.taskActions.getTaskExecutions, {});
|
||||
} catch (error) {
|
||||
console.error('Failed to load initial task data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private startAutoRefresh() {
|
||||
this.stopAutoRefresh();
|
||||
if (!this.autoRefresh) return;
|
||||
this._refreshHandle = setInterval(async () => {
|
||||
try {
|
||||
await this.loadExecutionsWithFilter();
|
||||
} catch (err) {
|
||||
// ignore transient errors during refresh
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
private stopAutoRefresh() {
|
||||
if (this._refreshHandle) {
|
||||
clearInterval(this._refreshHandle);
|
||||
this._refreshHandle = null;
|
||||
}
|
||||
}
|
||||
|
||||
public async disconnectedCallback(): Promise<void> {
|
||||
await (super.disconnectedCallback?.());
|
||||
this.stopAutoRefresh();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin: 4px 0 16px 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toolbar .spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
background: #111;
|
||||
color: #ddd;
|
||||
border: 1px solid #333;
|
||||
border-radius: 6px;
|
||||
padding: 8px 10px;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.chipbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chip {
|
||||
padding: 6px 10px;
|
||||
background: #2a2a2a;
|
||||
color: #bbb;
|
||||
border: 1px solid #444;
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.chip.active {
|
||||
background: #2196f3;
|
||||
border-color: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.task-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.task-card {
|
||||
background: #131313;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
}
|
||||
|
||||
.task-card:hover { border-color: #3a3a3a; }
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.header-left { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
||||
.header-right { display: flex; align-items: center; gap: 8px; }
|
||||
.task-icon { color: #cfcfcf; }
|
||||
.task-name { font-size: 1.05em; font-weight: 650; color: #fff; letter-spacing: 0.1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.task-subtitle { color: #8c8c8c; font-size: 0.9em; }
|
||||
|
||||
.task-description {
|
||||
color: #b5b5b5;
|
||||
font-size: 0.95em;
|
||||
margin-bottom: 12px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.task-meta {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.category-badge, .status-badge {
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.category-maintenance {
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.category-deployment {
|
||||
background: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.category-backup {
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.category-monitoring {
|
||||
background: #9c27b0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.category-cleanup {
|
||||
background: #795548;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.category-system {
|
||||
background: #607d8b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.category-security {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-running {
|
||||
background: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-cancelled {
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.trigger-button {
|
||||
padding: 6px 12px;
|
||||
background: #2196f3;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.trigger-button:hover {
|
||||
background: #1976d2;
|
||||
}
|
||||
|
||||
.trigger-button:disabled {
|
||||
background: #666;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.schedule-info {
|
||||
color: #666;
|
||||
font-size: 0.85em;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.last-run {
|
||||
color: #888;
|
||||
font-size: 0.85em;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.execution-logs {
|
||||
background: #0a0a0a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 8px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.log-info {
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
.log-warning {
|
||||
color: #ff9800;
|
||||
background: rgba(255, 152, 0, 0.1);
|
||||
}
|
||||
|
||||
.log-error {
|
||||
color: #f44336;
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
}
|
||||
|
||||
.log-success {
|
||||
color: #4caf50;
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.filter-button {
|
||||
padding: 6px 12px;
|
||||
background: #333;
|
||||
color: #ccc;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.filter-button.active {
|
||||
background: #2196f3;
|
||||
color: white;
|
||||
border-color: #2196f3;
|
||||
}
|
||||
|
||||
.filter-button:hover:not(.active) {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
.metrics {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #333;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
color: #666;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
color: #fff;
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-actions, .task-header .right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
background: #0f0f0f;
|
||||
border: 1px solid #2c2c2c;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.metric-item .label {
|
||||
color: #8d8d8d;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.metric-item .value {
|
||||
color: #eaeaea;
|
||||
font-weight: 600;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.lastline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #a0a0a0;
|
||||
font-size: 0.9em;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
||||
.dot.info { background: #2196f3; }
|
||||
.dot.success { background: #4caf50; }
|
||||
.dot.warning { background: #ff9800; }
|
||||
.dot.error { background: #f44336; }
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.link-button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #8ab4ff;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
.link-button:hover { text-decoration: underline; }
|
||||
|
||||
.secondary-button {
|
||||
padding: 6px 12px;
|
||||
background: #2b2b2b;
|
||||
color: #ccc;
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
transition: background 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.secondary-button:hover {
|
||||
background: #363636;
|
||||
border-color: #555;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
|
||||
private async triggerTask(taskName: string) {
|
||||
try {
|
||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Run Task: ${taskName}`,
|
||||
content: html`
|
||||
<div>
|
||||
<p>Do you want to trigger this task now?</p>
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Run now',
|
||||
action: async (modalArg: any) => {
|
||||
await appstate.dataState.dispatchAction(
|
||||
appstate.taskActions.triggerTask, { taskName }
|
||||
);
|
||||
plugins.deesCatalog.DeesToast.createAndShow({
|
||||
message: `Task ${taskName} triggered`,
|
||||
type: 'success',
|
||||
});
|
||||
await modalArg.destroy();
|
||||
// Refresh executions to reflect the new run quickly
|
||||
await this.loadExecutionsWithFilter();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Cancel',
|
||||
action: async (modalArg: any) => modalArg.destroy()
|
||||
}
|
||||
]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to trigger task:', error);
|
||||
plugins.deesCatalog.DeesToast.createAndShow({
|
||||
message: `Failed to trigger: ${error.message}`,
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async cancelTaskFor(taskName: string) {
|
||||
try {
|
||||
const executions = (this.data.taskExecutions || [])
|
||||
.filter(e => e.data.taskName === taskName && e.data.status === 'running')
|
||||
.sort((a, b) => (b.data.startedAt || 0) - (a.data.startedAt || 0));
|
||||
const running = executions[0];
|
||||
if (!running) return;
|
||||
|
||||
this.canceling = { ...this.canceling, [running.id]: true };
|
||||
try {
|
||||
await appstate.dataState.dispatchAction(
|
||||
appstate.taskActions.cancelTask, { executionId: running.id }
|
||||
);
|
||||
plugins.deesCatalog.DeesToast.createAndShow({
|
||||
message: `Cancelled ${taskName}`,
|
||||
type: 'success',
|
||||
});
|
||||
} finally {
|
||||
this.canceling = { ...this.canceling, [running.id]: false };
|
||||
await this.loadExecutionsWithFilter();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to cancel task:', err);
|
||||
plugins.deesCatalog.DeesToast.createAndShow({
|
||||
message: `Cancel failed: ${err.message}`,
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async loadExecutionsWithFilter() {
|
||||
try {
|
||||
const filter: any = {};
|
||||
if (this.filterStatus !== 'all') {
|
||||
filter.status = this.filterStatus;
|
||||
}
|
||||
|
||||
await appstate.dataState.dispatchAction(
|
||||
appstate.taskActions.getTaskExecutions, { filter }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to load executions:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private formatDate(timestamp: number): string {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
}
|
||||
|
||||
private formatDuration(ms: number): string {
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
||||
if (ms < 3600000) return `${(ms / 60000).toFixed(1)}m`;
|
||||
return `${(ms / 3600000).toFixed(1)}h`;
|
||||
}
|
||||
|
||||
private formatRelativeTime(ts?: number): string {
|
||||
if (!ts) return '-';
|
||||
const diff = Date.now() - ts;
|
||||
const abs = Math.abs(diff);
|
||||
if (abs < 60_000) return `${Math.round(abs / 1000)}s ago`;
|
||||
if (abs < 3_600_000) return `${Math.round(abs / 60_000)}m ago`;
|
||||
if (abs < 86_400_000) return `${Math.round(abs / 3_600_000)}h ago`;
|
||||
return `${Math.round(abs / 86_400_000)}d ago`;
|
||||
}
|
||||
|
||||
private getCategoryIcon(category: string): string {
|
||||
switch (category) {
|
||||
case 'maintenance':
|
||||
return 'lucide:Wrench';
|
||||
case 'deployment':
|
||||
return 'lucide:Rocket';
|
||||
case 'backup':
|
||||
return 'lucide:Archive';
|
||||
case 'monitoring':
|
||||
return 'lucide:Activity';
|
||||
case 'cleanup':
|
||||
return 'lucide:Trash2';
|
||||
case 'system':
|
||||
return 'lucide:Settings';
|
||||
case 'security':
|
||||
return 'lucide:Shield';
|
||||
default:
|
||||
return 'lucide:Play';
|
||||
}
|
||||
}
|
||||
|
||||
private getCategoryHue(category: string): number {
|
||||
switch (category) {
|
||||
case 'maintenance': return 28; // orange
|
||||
case 'deployment': return 208; // blue
|
||||
case 'backup': return 122; // green
|
||||
case 'monitoring': return 280; // purple
|
||||
case 'cleanup': return 20; // brownish
|
||||
case 'system': return 200; // steel
|
||||
case 'security': return 0; // red
|
||||
default: return 210; // default blue
|
||||
}
|
||||
}
|
||||
|
||||
private getStatusColor(status?: string): string {
|
||||
switch (status) {
|
||||
case 'running': return '#2196f3';
|
||||
case 'completed': return '#4caf50';
|
||||
case 'failed': return '#f44336';
|
||||
case 'cancelled': return '#ff9800';
|
||||
default: return '#3a3a3a';
|
||||
}
|
||||
}
|
||||
|
||||
private formatCronFriendly(cron?: string): string {
|
||||
if (!cron) return '';
|
||||
const parts = cron.trim().split(/\s+/);
|
||||
if (parts.length !== 5) return cron; // fallback
|
||||
const [min, hour, dom, mon, dow] = parts;
|
||||
if (min === '*/1' && hour === '*' && dom === '*' && mon === '*' && dow === '*') return 'every minute';
|
||||
if (min.startsWith('*/') && hour === '*' && dom === '*' && mon === '*' && dow === '*') return `every ${min.replace('*/','')} min`;
|
||||
if (min === '0' && hour.startsWith('*/') && dom === '*' && mon === '*' && dow === '*') return `every ${hour.replace('*/','')} hours`;
|
||||
if (min === '0' && hour === '*' && dom === '*' && mon === '*' && dow === '*') return 'hourly';
|
||||
if (min === '0' && hour === '0' && dom === '*' && mon === '*' && dow === '*') return 'daily';
|
||||
if (min === '0' && hour === '0' && dom === '1' && mon === '*' && dow === '*') return 'monthly';
|
||||
return cron;
|
||||
}
|
||||
|
||||
private renderTaskCard(task: any) {
|
||||
const executions = this.data.taskExecutions || [];
|
||||
const lastExecution = executions
|
||||
.filter(e => e.data.taskName === task.name)
|
||||
.sort((a, b) => (b.data.startedAt || 0) - (a.data.startedAt || 0))[0];
|
||||
|
||||
const isRunning = lastExecution?.data.status === 'running';
|
||||
const executionsForTask = executions.filter(e => e.data.taskName === task.name);
|
||||
const now = Date.now();
|
||||
const last24hCount = executionsForTask.filter(e => (e.data.startedAt || 0) > now - 86_400_000).length;
|
||||
const completed = executionsForTask.filter(e => e.data.status === 'completed');
|
||||
const successRate = executionsForTask.length ? Math.round((completed.length * 100) / executionsForTask.length) : 0;
|
||||
const avgDuration = completed.length ? Math.round(completed.reduce((acc, e) => acc + (e.data.duration || 0), 0) / completed.length) : undefined;
|
||||
const lastLog = lastExecution?.data.logs && lastExecution.data.logs.length > 0 ? lastExecution.data.logs[lastExecution.data.logs.length - 1] : null;
|
||||
|
||||
const status = lastExecution?.data.status as ('running'|'completed'|'failed'|'cancelled'|undefined);
|
||||
const hue = this.getCategoryHue(task.category);
|
||||
const subtitle = [
|
||||
task.category,
|
||||
task.schedule ? `⏱ ${this.formatCronFriendly(task.schedule)}` : null,
|
||||
isRunning
|
||||
? (lastExecution?.data.startedAt ? `Started ${this.formatRelativeTime(lastExecution.data.startedAt)}` : 'Running')
|
||||
: (task.lastRun ? `Last ${this.formatRelativeTime(task.lastRun)}` : 'Never run')
|
||||
].filter(Boolean).join(' • ');
|
||||
|
||||
return html`
|
||||
<div class="task-card">
|
||||
<div class="card-header">
|
||||
<div class="header-left">
|
||||
<dees-icon class="task-icon" .iconName=${this.getCategoryIcon(task.category)}></dees-icon>
|
||||
<div>
|
||||
<div class="task-name" title=${task.name}>${task.name}</div>
|
||||
<div class="task-subtitle" title=${task.schedule || ''}>${subtitle}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
${lastExecution ? html`<span class="status-badge status-${lastExecution.data.status}">${lastExecution.data.status}</span>` : html`<span class="status-badge" style="background:#2e2e2e;color:#ddd;border:1px solid #3a3a3a;">idle</span>`}
|
||||
${isRunning ? html`
|
||||
<dees-spinner style="--size: 18px"></dees-spinner>
|
||||
<dees-button
|
||||
.text=${this.canceling[lastExecution!.id] ? 'Cancelling…' : 'Cancel'}
|
||||
.type=${'secondary'}
|
||||
.disabled=${!!this.canceling[lastExecution!.id]}
|
||||
@click=${() => this.cancelTaskFor(task.name)}
|
||||
></dees-button>
|
||||
` : html`
|
||||
<dees-button .text=${'Run'} .type=${'primary'} .disabled=${!task.enabled} @click=${() => this.triggerTask(task.name)}></dees-button>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-description" title=${task.description || ''}>${task.description}</div>
|
||||
|
||||
${lastExecution ? html`
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-item">
|
||||
<div class="label">Last Status</div>
|
||||
<div class="value">
|
||||
<span class="status-badge status-${lastExecution.data.status}">${lastExecution.data.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="label">Avg Duration</div>
|
||||
<div class="value">${avgDuration ? this.formatDuration(avgDuration) : '-'}</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="label">24h Runs · Success</div>
|
||||
<div class="value">${last24hCount} · ${successRate}%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lastline">
|
||||
${lastLog ? html`<span class="dot ${lastLog.severity}"></span> ${lastLog.message}` : 'No recent logs'}
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<button class="link-button" @click=${() => this.openExecutionDetails(lastExecution)}>Details</button>
|
||||
${lastExecution.data.logs?.length ? html`<button class="link-button" @click=${() => this.openLogsModal(lastExecution)}>Logs</button>` : ''}
|
||||
</div>
|
||||
` : html`
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-item">
|
||||
<div class="label">Last Status</div>
|
||||
<div class="value">—</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="label">Avg Duration</div>
|
||||
<div class="value">—</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="label">24h Runs · Success</div>
|
||||
<div class="value">0 · 0%</div>
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async openExecutionDetails(execution: plugins.interfaces.data.ITaskExecution) {
|
||||
this.selectedExecution = execution;
|
||||
// Scroll into view of the details section
|
||||
requestAnimationFrame(() => {
|
||||
this.shadowRoot?.querySelector('cloudly-sectionheading + .execution-details')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
});
|
||||
}
|
||||
|
||||
private async openLogsModal(execution: plugins.interfaces.data.ITaskExecution) {
|
||||
await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Logs: ${execution.data.taskName}`,
|
||||
content: html`
|
||||
<div class="execution-logs">
|
||||
${(execution.data.logs || []).map(log => html`
|
||||
<div class="log-entry log-${log.severity}">
|
||||
<span>${this.formatDate(log.timestamp)}</span> - ${log.message}
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Copy All',
|
||||
action: async (modalArg: any) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText((execution.data.logs || [])
|
||||
.map(l => `${new Date(l.timestamp).toISOString()} [${l.severity}] ${l.message}`).join('\n'));
|
||||
plugins.deesCatalog.DeesToast.createAndShow({ message: 'Logs copied', type: 'success' });
|
||||
} catch (e) {
|
||||
plugins.deesCatalog.DeesToast.createAndShow({ message: 'Copy failed', type: 'error' });
|
||||
}
|
||||
}
|
||||
},
|
||||
{ name: 'Close', action: async (modalArg: any) => modalArg.destroy() }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
private renderExecutionDetails(execution: plugins.interfaces.data.ITaskExecution) {
|
||||
return html`
|
||||
<div class="execution-details">
|
||||
<h3>Execution Details: ${execution.data.taskName}</h3>
|
||||
|
||||
<div class="metrics">
|
||||
<div class="metric">
|
||||
<span class="metric-label">Started</span>
|
||||
<span class="metric-value">${this.formatDate(execution.data.startedAt)}</span>
|
||||
</div>
|
||||
${execution.data.completedAt ? html`
|
||||
<div class="metric">
|
||||
<span class="metric-label">Completed</span>
|
||||
<span class="metric-value">${this.formatDate(execution.data.completedAt)}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${execution.data.duration ? html`
|
||||
<div class="metric">
|
||||
<span class="metric-label">Duration</span>
|
||||
<span class="metric-value">${this.formatDuration(execution.data.duration)}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="metric">
|
||||
<span class="metric-label">Triggered By</span>
|
||||
<span class="metric-value">${execution.data.triggeredBy}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${execution.data.logs && execution.data.logs.length > 0 ? html`
|
||||
<h4>Logs</h4>
|
||||
<div class="execution-logs">
|
||||
${execution.data.logs.map(log => html`
|
||||
<div class="log-entry log-${log.severity}">
|
||||
<span>${this.formatDate(log.timestamp)}</span> -
|
||||
${log.message}
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${execution.data.metrics ? html`
|
||||
<h4>Metrics</h4>
|
||||
<div class="metrics">
|
||||
${Object.entries(execution.data.metrics).map(([key, value]) => html`
|
||||
<div class="metric">
|
||||
<span class="metric-label">${key}</span>
|
||||
<span class="metric-value">${typeof value === 'object' ? JSON.stringify(value) : value}</span>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${execution.data.error ? html`
|
||||
<h4>Error</h4>
|
||||
<div class="execution-logs">
|
||||
<div class="log-entry log-error">
|
||||
${execution.data.error.message}
|
||||
${execution.data.error.stack ? html`<pre>${execution.data.error.stack}</pre>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const tasks = (this.data.tasks || []) as any[];
|
||||
const categories = Array.from(new Set(tasks.map(t => t.category))).sort();
|
||||
const filteredTasks = tasks
|
||||
.filter(t => this.categoryFilter === 'all' || t.category === this.categoryFilter)
|
||||
.filter(t => !this.searchQuery || t.name.toLowerCase().includes(this.searchQuery.toLowerCase()) || (t.description || '').toLowerCase().includes(this.searchQuery.toLowerCase()));
|
||||
|
||||
return html`
|
||||
<cloudly-sectionheading>Tasks</cloudly-sectionheading>
|
||||
|
||||
<dees-panel
|
||||
.title=${'Task Library'}
|
||||
.subtitle=${'Run maintenance, monitoring and system tasks'}
|
||||
.variant=${'outline'}
|
||||
>
|
||||
<div class="toolbar">
|
||||
<div class="chipbar">
|
||||
<div class="chip ${this.categoryFilter === 'all' ? 'active' : ''}"
|
||||
@click=${() => { this.categoryFilter = 'all'; }}>
|
||||
All
|
||||
</div>
|
||||
${categories.map(cat => html`
|
||||
<div class="chip ${this.categoryFilter === cat ? 'active' : ''}"
|
||||
@click=${() => { this.categoryFilter = cat; }}>
|
||||
${cat}
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<input class="search-input" placeholder="Search tasks" .value=${this.searchQuery}
|
||||
@input=${(e: any) => { this.searchQuery = e.target.value; }} />
|
||||
<button class="secondary-button" @click=${async () => {
|
||||
await this.loadExecutionsWithFilter();
|
||||
}}>Refresh</button>
|
||||
<button class="secondary-button" @click=${() => {
|
||||
this.autoRefresh = !this.autoRefresh;
|
||||
this.autoRefresh ? this.startAutoRefresh() : this.stopAutoRefresh();
|
||||
}}>
|
||||
${this.autoRefresh ? 'Auto-Refresh: On' : 'Auto-Refresh: Off'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="task-grid">
|
||||
${filteredTasks.map(task => this.renderTaskCard(task))}
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<cloudly-sectionheading>Execution History</cloudly-sectionheading>
|
||||
|
||||
<dees-panel
|
||||
.title=${'Recent Executions'}
|
||||
.subtitle=${'History of task runs and their outcomes'}
|
||||
.variant=${'outline'}
|
||||
>
|
||||
<dees-table
|
||||
.heading1=${'Task Executions'}
|
||||
.heading2=${'History of task runs and their outcomes'}
|
||||
.data=${this.data.taskExecutions || []}
|
||||
.displayFunction=${(itemArg: plugins.interfaces.data.ITaskExecution) => {
|
||||
return {
|
||||
Task: itemArg.data.taskName,
|
||||
Status: html`<span class="status-badge status-${itemArg.data.status}">${itemArg.data.status}</span>`,
|
||||
'Started At': this.formatDate(itemArg.data.startedAt),
|
||||
Duration: itemArg.data.duration ? this.formatDuration(itemArg.data.duration) : '-',
|
||||
'Triggered By': itemArg.data.triggeredBy,
|
||||
Logs: itemArg.data.logs?.length || 0,
|
||||
};
|
||||
}}
|
||||
.actionFunction=${async (itemArg: plugins.interfaces.data.ITaskExecution) => {
|
||||
const actions: any[] = [
|
||||
{
|
||||
name: 'View Details',
|
||||
iconName: 'lucide:Eye',
|
||||
type: ['inRow'],
|
||||
actionFunc: async () => {
|
||||
this.selectedExecution = itemArg;
|
||||
},
|
||||
}
|
||||
];
|
||||
if (itemArg.data.status === 'running') {
|
||||
actions.push({
|
||||
name: 'Cancel',
|
||||
iconName: 'lucide:SquareX',
|
||||
type: ['inRow'],
|
||||
actionFunc: async () => {
|
||||
await appstate.dataState.dispatchAction(appstate.taskActions.cancelTask, { executionId: itemArg.id });
|
||||
await this.loadExecutionsWithFilter();
|
||||
},
|
||||
});
|
||||
}
|
||||
return actions;
|
||||
}}
|
||||
></dees-table>
|
||||
</dees-panel>
|
||||
|
||||
${this.selectedExecution ? html`
|
||||
<cloudly-sectionheading>Execution Details</cloudly-sectionheading>
|
||||
${this.renderExecutionDetails(this.selectedExecution)}
|
||||
` : ''}
|
||||
`;
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
export * from './shared/index.js';
|
||||
export * from './cloudly-dashboard.js';
|
||||
export * from './cloudly-view-secretgroups.js';
|
||||
export * from './cloudly-view-secretbundles.js';
|
||||
export * from './views/secretgroups/index.js';
|
||||
export * from './views/secretbundles/index.js';
|
||||
|
52
ts_web/elements/views/backups/index.ts
Normal file
52
ts_web/elements/views/backups/index.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
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-backups')
|
||||
export class CloudlyViewBackups extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subecription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
|
||||
this.rxSubscriptions.push(subecription);
|
||||
}
|
||||
|
||||
public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css`` ];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<cloudly-sectionheading>Backups</cloudly-sectionheading>
|
||||
<dees-table
|
||||
.heading1=${'Backups'}
|
||||
.heading2=${'decoded in client'}
|
||||
.data=${this.data.backups}
|
||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => { return { id: itemArg.id, serverAmount: itemArg.data.servers.length }; }}
|
||||
.dataActions=${[
|
||||
{ name: 'add configBundle', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
|
||||
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add ConfigBundle', content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
||||
<dees-input-text .key=${'data.secretGroupIds'} .label=${'secretGroupIds'} .value=${''}></dees-input-text>
|
||||
<dees-input-text .key=${'data.includedTags'} .label=${'includedTags'} .value=${''}></dees-input-text>
|
||||
</dees-form>
|
||||
`, menuOptions: [ { name: 'create', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] });
|
||||
} },
|
||||
{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
|
||||
plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ConfigBundle ${actionDataArg.item.id}`, content: html`
|
||||
<div style="text-align:center">Do you really want to delete the ConfigBundle?</div>
|
||||
<div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${actionDataArg.item.id}</div>
|
||||
`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } } ] });
|
||||
} },
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global { interface HTMLElementTagNameMap { 'cloudly-view-backups': CloudlyViewBackups; } }
|
||||
|
111
ts_web/elements/views/clusters/index.ts
Normal file
111
ts_web/elements/views/clusters/index.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
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-clusters')
|
||||
export class CloudlyViewClusters extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = {} as any;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subecription = appstate.dataState
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((dataArg) => {
|
||||
this.data = dataArg;
|
||||
});
|
||||
this.rxSubscriptions.push(subecription);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css``,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<cloudly-sectionheading>Clusters</cloudly-sectionheading>
|
||||
<dees-table
|
||||
.heading1=${'Clusters'}
|
||||
.heading2=${'decoded in client'}
|
||||
.data=${this.data.clusters}
|
||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
||||
return {
|
||||
id: itemArg.id,
|
||||
serverAmount: itemArg.data.servers.length,
|
||||
};
|
||||
}}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'add cluster',
|
||||
iconName: 'plus',
|
||||
type: ['header', 'footer'],
|
||||
actionFunc: async () => {
|
||||
await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: 'Add Cluster',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'clusterName'} .label=${'cluster name'} .description=${'a descriptive name for the cluster'} .value=${''}></dees-input-text>
|
||||
<dees-input-dropdown .key=${'setupMode'} .label=${'Setup Mode'} .description=${'How the cluster infrastructure should be managed'}
|
||||
.options=${[
|
||||
{option: 'manual', key: 'manual', description: 'Manual Setup - Add your own servers manually'},
|
||||
{option: 'hetzner', key: 'hetzner', description: 'Hetzner Cloud - Auto-provision servers on Hetzner'},
|
||||
{option: 'aws', key: 'aws', description: 'AWS - Auto-provision on Amazon Web Services (coming soon)', disabled: true},
|
||||
{option: 'digitalocean', key: 'digitalocean', description: 'DigitalOcean - Auto-provision on DigitalOcean (coming soon)', disabled: true}
|
||||
]}
|
||||
.selectedOption=${'manual'}>
|
||||
</dees-input-dropdown>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'create', action: async (modalArg: any) => {
|
||||
const data = (await modalArg.shadowRoot.querySelector('dees-form').collectFormData()) as any;
|
||||
await appstate.dataState.dispatchAction(appstate.addClusterAction, data);
|
||||
await modalArg.destroy();
|
||||
}},
|
||||
{ name: 'cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
iconName: 'trash',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg: any) => {
|
||||
plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
|
||||
content: html`
|
||||
<div style="text-align:center">Do you really want to delete the ConfigBundle?</div>
|
||||
<div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${actionDataArg.item.id}</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } },
|
||||
{ name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } },
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'cloudly-view-clusters': CloudlyViewClusters;
|
||||
}
|
||||
}
|
||||
|
52
ts_web/elements/views/dbs/index.ts
Normal file
52
ts_web/elements/views/dbs/index.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
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-dbs')
|
||||
export class CloudlyViewDbs extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subecription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
|
||||
this.rxSubscriptions.push(subecription);
|
||||
}
|
||||
|
||||
public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css`` ];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<cloudly-sectionheading>DBs</cloudly-sectionheading>
|
||||
<dees-table
|
||||
.heading1=${'DBs'}
|
||||
.heading2=${'decoded in client'}
|
||||
.data=${this.data.dbs}
|
||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => { return { id: itemArg.id, serverAmount: itemArg.data.servers.length }; }}
|
||||
.dataActions=${[
|
||||
{ name: 'add configBundle', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
|
||||
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add ConfigBundle', content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
||||
<dees-input-text .key=${'data.secretGroupIds'} .label=${'secretGroupIds'} .value=${''}></dees-input-text>
|
||||
<dees-input-text .key=${'data.includedTags'} .label=${'includedTags'} .value=${''}></dees-input-text>
|
||||
</dees-form>
|
||||
`, menuOptions: [ { name: 'create', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] });
|
||||
} },
|
||||
{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
|
||||
plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ConfigBundle ${actionDataArg.item.id}`, content: html`
|
||||
<div style="text-align:center">Do you really want to delete the ConfigBundle?</div>
|
||||
<div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${actionDataArg.item.id}</div>
|
||||
`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } } ] });
|
||||
} },
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global { interface HTMLElementTagNameMap { 'cloudly-view-dbs': CloudlyViewDbs; } }
|
||||
|
222
ts_web/elements/views/deployments/index.ts
Normal file
222
ts_web/elements/views/deployments/index.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
|
143
ts_web/elements/views/dns/index.ts
Normal file
143
ts_web/elements/views/dns/index.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
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-dns')
|
||||
export class CloudlyViewDns extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = { secretGroups: [], secretBundles: [], dnsEntries: [], domains: [] } as any;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subscription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
|
||||
this.rxSubscriptions.push(subscription);
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
await appstate.dataState.dispatchAction(appstate.getAllDataAction, {});
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
.dns-type-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; color: white; }
|
||||
.type-A { background: #4CAF50; }
|
||||
.type-AAAA { background: #45a049; }
|
||||
.type-CNAME { background: #2196F3; }
|
||||
.type-MX { background: #FF9800; }
|
||||
.type-TXT { background: #9C27B0; }
|
||||
.type-NS { background: #795548; }
|
||||
.type-SOA { background: #607D8B; }
|
||||
.type-SRV { background: #E91E63; }
|
||||
.type-CAA { background: #00BCD4; }
|
||||
.type-PTR { background: #673AB7; }
|
||||
.status-active { color: #4CAF50; }
|
||||
.status-inactive { color: #f44336; }
|
||||
`,
|
||||
];
|
||||
|
||||
private getRecordTypeBadge(type: string) { return html`<span class="dns-type-badge type-${type}">${type}</span>`; }
|
||||
private getStatusBadge(active: boolean) { return html`<span class="${active ? 'status-active' : 'status-inactive'}">${active ? '✓ Active' : '✗ Inactive'}</span>`; }
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<cloudly-sectionheading>DNS Management</cloudly-sectionheading>
|
||||
<dees-table
|
||||
.heading1=${'DNS Entries'}
|
||||
.heading2=${'Manage DNS records for your domains'}
|
||||
.data=${this.data.dnsEntries || []}
|
||||
.displayFunction=${(itemArg: plugins.interfaces.data.IDnsEntry) => {
|
||||
return {
|
||||
Type: this.getRecordTypeBadge(itemArg.data.type),
|
||||
Name: itemArg.data.name === '@' ? '<root>' : itemArg.data.name,
|
||||
Value: itemArg.data.value,
|
||||
TTL: `${itemArg.data.ttl}s`,
|
||||
Priority: itemArg.data.priority || '-',
|
||||
Zone: itemArg.data.zone,
|
||||
Status: this.getStatusBadge(itemArg.data.active),
|
||||
Description: itemArg.data.description || '-',
|
||||
};
|
||||
}}
|
||||
.dataActions=${[
|
||||
{ name: 'Add DNS Entry', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
|
||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: 'Add DNS Entry',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-dropdown .key=${'type'} .label=${'Record Type'} .options=${[
|
||||
{key: 'A', option: 'A - IPv4 Address'}, {key: 'AAAA', option: 'AAAA - IPv6 Address'}, {key: 'CNAME', option: 'CNAME - Canonical Name'}, {key: 'MX', option: 'MX - Mail Exchange'}, {key: 'TXT', option: 'TXT - Text Record'}, {key: 'NS', option: 'NS - Name Server'}, {key: 'SOA', option: 'SOA - Start of Authority'}, {key: 'SRV', option: 'SRV - Service'}, {key: 'CAA', option: 'CAA - Certification Authority'}, {key: 'PTR', option: 'PTR - Pointer'}, ]} .value=${'A'} .required=${true}></dees-input-dropdown>
|
||||
<dees-input-dropdown .key=${'domainId'} .label=${'Domain'} .options=${this.data.domains?.map(domain => ({ key: domain.id, option: domain.data.name })) || []} .required=${true}></dees-input-dropdown>
|
||||
<dees-input-text .key=${'name'} .label=${'Name'} .placeholder=${'@ for root, www, mail, etc.'} .value=${'@'} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'value'} .label=${'Value'} .placeholder=${'IP address, domain, or text value'} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'ttl'} .label=${'TTL (seconds)'} .value=${'3600'} .type=${'number'} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'priority'} .label=${'Priority (MX/SRV only)'} .type=${'number'} .placeholder=${'10'}></dees-input-text>
|
||||
<dees-input-text .key=${'weight'} .label=${'Weight (SRV only)'} .type=${'number'} .placeholder=${'0'}></dees-input-text>
|
||||
<dees-input-text .key=${'port'} .label=${'Port (SRV only)'} .type=${'number'} .placeholder=${'443'}></dees-input-text>
|
||||
<dees-input-checkbox .key=${'active'} .label=${'Active'} .value=${true}></dees-input-checkbox>
|
||||
<dees-input-text .key=${'description'} .label=${'Description (optional)'} .placeholder=${'What is this record for?'}></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'Create DNS Entry', action: async (modalArg: any) => {
|
||||
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||
const formData = await form.gatherData();
|
||||
await appstate.dataState.dispatchAction(appstate.createDnsEntryAction, { dnsEntryData: { type: formData.type, domainId: formData.domainId, zone: '', name: formData.name || '@', value: formData.value, ttl: parseInt(formData.ttl) || 3600, priority: formData.priority ? parseInt(formData.priority) : undefined, weight: formData.weight ? parseInt(formData.weight) : undefined, port: formData.port ? parseInt(formData.port) : undefined, active: formData.active, description: formData.description || undefined, }, });
|
||||
await modalArg.destroy();
|
||||
} },
|
||||
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||
],
|
||||
});
|
||||
} },
|
||||
{ name: 'Edit', iconName: 'edit', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
|
||||
const dnsEntry = actionDataArg.item as plugins.interfaces.data.IDnsEntry;
|
||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Edit DNS Entry`,
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-dropdown .key=${'type'} .label=${'Record Type'} .options=${[
|
||||
{key: 'A', option: 'A - IPv4 Address'}, {key: 'AAAA', option: 'AAAA - IPv6 Address'}, {key: 'CNAME', option: 'CNAME - Canonical Name'}, {key: 'MX', option: 'MX - Mail Exchange'}, {key: 'TXT', option: 'TXT - Text Record'}, {key: 'NS', option: 'NS - Name Server'}, {key: 'SOA', option: 'SOA - Start of Authority'}, {key: 'SRV', option: 'SRV - Service'}, {key: 'CAA', option: 'CAA - Certification Authority'}, {key: 'PTR', option: 'PTR - Pointer'}, ]} .value=${dnsEntry.data.type} .required=${true}></dees-input-dropdown>
|
||||
<dees-input-dropdown .key=${'domainId'} .label=${'Domain'} .options=${this.data.domains?.map(domain => ({ key: domain.id, option: domain.data.name })) || []} .value=${dnsEntry.data.domainId || ''} .required=${true}></dees-input-dropdown>
|
||||
<dees-input-text .key=${'name'} .label=${'Name'} .value=${dnsEntry.data.name} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'value'} .label=${'Value'} .value=${dnsEntry.data.value} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'ttl'} .label=${'TTL (seconds)'} .value=${dnsEntry.data.ttl} .type=${'number'} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'priority'} .label=${'Priority (MX/SRV only)'} .value=${dnsEntry.data.priority || ''} .type=${'number'}></dees-input-text>
|
||||
<dees-input-text .key=${'weight'} .label=${'Weight (SRV only)'} .value=${dnsEntry.data.weight || ''} .type=${'number'}></dees-input-text>
|
||||
<dees-input-text .key=${'port'} .label=${'Port (SRV only)'} .value=${dnsEntry.data.port || ''} .type=${'number'}></dees-input-text>
|
||||
<dees-input-checkbox .key=${'active'} .label=${'Active'} .value=${dnsEntry.data.active}></dees-input-checkbox>
|
||||
<dees-input-text .key=${'description'} .label=${'Description (optional)'} .value=${dnsEntry.data.description || ''}></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'Update DNS Entry', action: async (modalArg: any) => {
|
||||
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||
const formData = await form.gatherData();
|
||||
await appstate.dataState.dispatchAction(appstate.updateDnsEntryAction, { dnsEntryId: dnsEntry.id, dnsEntryData: { ...dnsEntry.data, type: formData.type, domainId: formData.domainId, zone: '', name: formData.name || '@', value: formData.value, ttl: parseInt(formData.ttl) || 3600, priority: formData.priority ? parseInt(formData.priority) : undefined, weight: formData.weight ? parseInt(formData.weight) : undefined, port: formData.port ? parseInt(formData.port) : undefined, active: formData.active, description: formData.description || undefined, }, });
|
||||
await modalArg.destroy();
|
||||
} },
|
||||
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||
],
|
||||
});
|
||||
} },
|
||||
{ name: 'Duplicate', iconName: 'copy', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { const dnsEntry = actionDataArg.item as plugins.interfaces.data.IDnsEntry; await appstate.dataState.dispatchAction(appstate.createDnsEntryAction, { dnsEntryData: { ...dnsEntry.data, description: `Copy of ${dnsEntry.data.description || dnsEntry.data.name}`, }, }); } },
|
||||
{ name: 'Toggle Active', iconName: 'power', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { const dnsEntry = actionDataArg.item as plugins.interfaces.data.IDnsEntry; await appstate.dataState.dispatchAction(appstate.updateDnsEntryAction, { dnsEntryId: dnsEntry.id, dnsEntryData: { ...dnsEntry.data, active: !dnsEntry.data.active, }, }); } },
|
||||
{ name: 'Delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { const dnsEntry = actionDataArg.item as plugins.interfaces.data.IDnsEntry; plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete DNS Entry`, content: html`<div style="text-align:center">Are you sure you want to delete this DNS entry?</div><div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;"><div style="color: #fff; font-weight: bold;">${dnsEntry.data.type} - ${dnsEntry.data.name}.${dnsEntry.data.zone}</div><div style="color: #aaa; font-size: 0.9em; margin-top: 4px;">${dnsEntry.data.value}</div>${dnsEntry.data.description ? html`<div style=\"color: #888; font-size: 0.85em; margin-top: 8px;\">${dnsEntry.data.description}</div>` : ''}</div>`, menuOptions: [ { name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'Delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteDnsEntryAction, { dnsEntryId: dnsEntry.id, }); await modalArg.destroy(); } }, ], }); } },
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global { interface HTMLElementTagNameMap { 'cloudly-view-dns': CloudlyViewDns; } }
|
||||
|
176
ts_web/elements/views/domains/index.ts
Normal file
176
ts_web/elements/views/domains/index.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
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-domains')
|
||||
export class CloudlyViewDomains extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = { secretGroups: [], secretBundles: [], domains: [], dnsEntries: [] } 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 { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; color: white; }
|
||||
.status-active { background: #4CAF50; }
|
||||
.status-pending { background: #FF9800; }
|
||||
.status-expired { background: #f44336; }
|
||||
.status-suspended { background: #9E9E9E; }
|
||||
.status-transferred { background: #607D8B; }
|
||||
.verification-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; }
|
||||
.verification-verified { background: #4CAF50; color: white; }
|
||||
.verification-pending { background: #FF9800; color: white; }
|
||||
.verification-failed { background: #f44336; color: white; }
|
||||
.verification-not_required { background: #E0E0E0; color: #333; }
|
||||
.ssl-badge { display: inline-block; padding: 2px 6px; border-radius: 3px; font-size: 0.8em; }
|
||||
.ssl-active { color: #4CAF50; }
|
||||
.ssl-pending { color: #FF9800; }
|
||||
.ssl-expired { color: #f44336; }
|
||||
.ssl-none { color: #9E9E9E; }
|
||||
.nameserver-list { font-size: 0.85em; color: #666; }
|
||||
.expiry-warning { color: #FF9800; font-weight: 500; }
|
||||
.expiry-critical { color: #f44336; font-weight: bold; }
|
||||
`,
|
||||
];
|
||||
|
||||
private getStatusBadge(status: string) { return html`<span class="status-badge status-${status}">${status.toUpperCase()}</span>`; }
|
||||
private getVerificationBadge(status: string) { const displayText = status === 'not_required' ? 'Not Required' : status.replace('_', ' ').toUpperCase(); return html`<span class="verification-badge verification-${status}">${displayText}</span>`; }
|
||||
private getSslBadge(sslStatus?: string) { if (!sslStatus) return html`<span class="ssl-badge ssl-none">—</span>`; const icon = sslStatus === 'active' ? '🔒' : sslStatus === 'expired' ? '⚠️' : '🔓'; return html`<span class="ssl-badge ssl-${sslStatus}">${icon} ${sslStatus.toUpperCase()}</span>`; }
|
||||
private formatDate(timestamp?: number) { if (!timestamp) return '—'; const date = new Date(timestamp); return date.toLocaleDateString(); }
|
||||
private getDaysUntilExpiry(expiresAt?: number) { if (!expiresAt) return null; const days = Math.floor((expiresAt - Date.now()) / (1000 * 60 * 60 * 24)); return days; }
|
||||
private getExpiryDisplay(expiresAt?: number) { const days = this.getDaysUntilExpiry(expiresAt); if (days === null) return '—'; if (days < 0) { return html`<span class="expiry-critical">Expired ${Math.abs(days)} days ago</span>`; } else if (days <= 30) { return html`<span class="expiry-warning">Expires in ${days} days</span>`; } else { return `${days} days`; } }
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<cloudly-sectionheading>Domain Management</cloudly-sectionheading>
|
||||
<dees-table
|
||||
.heading1=${'Domains'}
|
||||
.heading2=${'Manage your domains and DNS zones'}
|
||||
.data=${this.data.domains || []}
|
||||
.displayFunction=${(itemArg: plugins.interfaces.data.IDomain) => {
|
||||
const dnsCount = this.data.dnsEntries?.filter(dns => dns.data.zone === itemArg.data.name).length || 0;
|
||||
return {
|
||||
Domain: html`<div><div style="font-weight: 500;">${itemArg.data.name}</div>${itemArg.data.description ? html`<div style="font-size: 0.85em; color: #666; margin-top: 2px;">${itemArg.data.description}</div>` : ''}</div>`,
|
||||
Status: this.getStatusBadge(itemArg.data.status),
|
||||
Verification: this.getVerificationBadge(itemArg.data.verificationStatus),
|
||||
SSL: this.getSslBadge(itemArg.data.sslStatus),
|
||||
'DNS Records': dnsCount,
|
||||
Registrar: itemArg.data.registrar?.name || '—',
|
||||
Expires: this.getExpiryDisplay(itemArg.data.expiresAt),
|
||||
'Auto-Renew': itemArg.data.autoRenew ? '✓' : '✗',
|
||||
Nameservers: html`<div class="nameserver-list">${itemArg.data.nameservers?.join(', ') || '—'}</div>`,
|
||||
};
|
||||
}}
|
||||
.dataActions=${[
|
||||
{ name: 'Add Domain', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
|
||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: 'Add Domain',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'name'} .label=${'Domain Name'} .placeholder=${'example.com'} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'description'} .label=${'Description'} .placeholder=${'Main company domain'}></dees-input-text>
|
||||
<dees-input-dropdown .key=${'status'} .label=${'Status'} .options=${[{key: 'active', option: 'Active'}, {key: 'pending', option: 'Pending'}, {key: 'expired', option: 'Expired'}, {key: 'suspended', option: 'Suspended'}]} .value=${'pending'} .required=${true}></dees-input-dropdown>
|
||||
<dees-input-text .key=${'nameservers'} .label=${'Nameservers (comma separated)'} .placeholder=${'ns1.example.com, ns2.example.com'}></dees-input-text>
|
||||
<dees-input-text .key=${'registrarName'} .label=${'Registrar Name'} .placeholder=${'GoDaddy, Namecheap, etc.'}></dees-input-text>
|
||||
<dees-input-text .key=${'registrarUrl'} .label=${'Registrar URL'} .placeholder=${'https://registrar.com'}></dees-input-text>
|
||||
<dees-input-text .key=${'expiresAt'} .label=${'Expiration Date'} .type=${'date'}></dees-input-text>
|
||||
<dees-input-checkbox .key=${'autoRenew'} .label=${'Auto-Renew Enabled'} .value=${true}></dees-input-checkbox>
|
||||
<dees-input-checkbox .key=${'dnssecEnabled'} .label=${'DNSSEC Enabled'} .value=${false}></dees-input-checkbox>
|
||||
<dees-input-checkbox .key=${'isPrimary'} .label=${'Primary Domain'} .value=${false}></dees-input-checkbox>
|
||||
<dees-input-text .key=${'tags'} .label=${'Tags (comma separated)'} .placeholder=${'production, critical'}></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'Create Domain', action: async (modalArg: any) => {
|
||||
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||
const formData = await form.gatherData();
|
||||
const nameservers = formData.nameservers ? formData.nameservers.split(',').map((ns: string) => ns.trim()).filter((ns: string) => ns) : [];
|
||||
const tags = formData.tags ? formData.tags.split(',').map((tag: string) => tag.trim()).filter((tag: string) => tag) : [];
|
||||
await appstate.dataState.dispatchAction(appstate.createDomainAction, { domainData: { name: formData.name, description: formData.description || undefined, status: formData.status, verificationStatus: 'pending', nameservers, registrar: formData.registrarName ? { name: formData.registrarName, url: formData.registrarUrl || undefined, } : undefined, expiresAt: formData.expiresAt ? new Date(formData.expiresAt).getTime() : undefined, autoRenew: formData.autoRenew, dnssecEnabled: formData.dnssecEnabled, isPrimary: formData.isPrimary, tags: tags.length > 0 ? tags : undefined, }, });
|
||||
await modalArg.destroy();
|
||||
}},
|
||||
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||
],
|
||||
});
|
||||
} },
|
||||
{ name: 'Edit', iconName: 'edit', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
|
||||
const domain = actionDataArg.item as plugins.interfaces.data.IDomain;
|
||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Edit Domain: ${domain.data.name}`,
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'name'} .label=${'Domain Name'} .value=${domain.data.name} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'description'} .label=${'Description'} .value=${domain.data.description || ''}></dees-input-text>
|
||||
<dees-input-dropdown .key=${'status'} .label=${'Status'} .options=${[{key: 'active', option: 'Active'}, {key: 'pending', option: 'Pending'}, {key: 'expired', option: 'Expired'}, {key: 'suspended', option: 'Suspended'}, {key: 'transferred', option: 'Transferred'}]} .value=${domain.data.status} .required=${true}></dees-input-dropdown>
|
||||
<dees-input-checkbox .key=${'autoRenew'} .label=${'Auto-Renew Enabled'} .value=${domain.data.autoRenew}></dees-input-checkbox>
|
||||
<dees-input-checkbox .key=${'dnssecEnabled'} .label=${'DNSSEC Enabled'} .value=${domain.data.dnssecEnabled}></dees-input-checkbox>
|
||||
<dees-input-text .key=${'tags'} .label=${'Tags (comma separated)'} .value=${(domain.data.tags || []).join(', ')}></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'Save Changes', action: async (modalArg: any) => {
|
||||
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||
const formData = await form.gatherData();
|
||||
const tags = formData.tags ? formData.tags.split(',').map((tag: string) => tag.trim()).filter((tag: string) => tag) : [];
|
||||
await appstate.dataState.dispatchAction(appstate.updateDomainAction, { domainId: domain.id, updates: { name: formData.name, description: formData.description || undefined, status: formData.status, autoRenew: formData.autoRenew, dnssecEnabled: formData.dnssecEnabled, tags }, });
|
||||
await modalArg.destroy();
|
||||
}},
|
||||
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||
],
|
||||
});
|
||||
} },
|
||||
{ name: 'Verify Ownership', iconName: 'check-circle', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
|
||||
const domain = actionDataArg.item as plugins.interfaces.data.IDomain;
|
||||
await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Verify Domain: ${domain.data.name}`,
|
||||
content: html`
|
||||
<div style="text-align:center; padding: 20px;">
|
||||
<p>Choose a verification method for <strong>${domain.data.name}</strong></p>
|
||||
<dees-form>
|
||||
<dees-input-dropdown .key=${'method'} .label=${'Verification Method'} .options=${[{key: 'dns', option: 'DNS TXT Record'}, {key: 'http', option: 'HTTP File Upload'}, {key: 'email', option: 'Email Verification'}, {key: 'manual', option: 'Manual Verification'}]} .value=${'dns'} .required=${true}></dees-input-dropdown>
|
||||
</dees-form>
|
||||
${domain.data.verificationToken ? html`<div style="margin-top: 20px; padding: 15px; background: #333; border-radius: 8px;"><div style="color: #aaa; font-size: 0.9em;">Verification Token:</div><code style="color: #4CAF50; word-break: break-all;">${domain.data.verificationToken}</code></div>` : ''}
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'Start Verification', action: async (modalArg: any) => { const form = modalArg.shadowRoot.querySelector('dees-form') as any; const formData = await form.gatherData(); await appstate.dataState.dispatchAction(appstate.verifyDomainAction, { domainId: domain.id, verificationMethod: formData.method, }); await modalArg.destroy(); } },
|
||||
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||
],
|
||||
});
|
||||
} },
|
||||
{ name: 'View DNS Records', iconName: 'list', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { const domain = actionDataArg.item as plugins.interfaces.data.IDomain; console.log('View DNS records for domain:', domain.data.name); } },
|
||||
{ name: 'Delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
|
||||
const domain = actionDataArg.item as plugins.interfaces.data.IDomain;
|
||||
const dnsCount = this.data.dnsEntries?.filter(dns => dns.data.zone === domain.data.name).length || 0;
|
||||
plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Delete Domain`,
|
||||
content: html`
|
||||
<div style="text-align:center">Are you sure you want to delete this domain?</div>
|
||||
<div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;">
|
||||
<div style="color: #fff; font-weight: bold; font-size: 1.1em;">${domain.data.name}</div>
|
||||
${domain.data.description ? html`<div style="color: #aaa; margin-top: 4px;">${domain.data.description}</div>` : ''}
|
||||
${dnsCount > 0 ? html`<div style="color: #f44336; margin-top: 12px; padding: 8px; background: #1a1a1a; border-radius: 4px;">⚠️ This domain has ${dnsCount} DNS record${dnsCount > 1 ? 's' : ''} that will also be deleted</div>` : ''}
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [ { name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'Delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteDomainAction, { domainId: domain.id, }); await modalArg.destroy(); } }, ],
|
||||
});
|
||||
} },
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap { 'cloudly-view-domains': CloudlyViewDomains; }
|
||||
}
|
||||
|
118
ts_web/elements/views/externalregistries/index.ts
Normal file
118
ts_web/elements/views/externalregistries/index.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
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-externalregistries')
|
||||
export class CloudlyViewExternalRegistries extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = { secretGroups: [], secretBundles: [], externalRegistries: [] } as any;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subscription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
|
||||
this.rxSubscriptions.push(subscription);
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
await appstate.dataState.dispatchAction(appstate.getAllDataAction, {});
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
.status-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; color: white; }
|
||||
.status-active { background: #4CAF50; }
|
||||
.status-inactive { background: #9E9E9E; }
|
||||
.status-error { background: #f44336; }
|
||||
.status-unverified { background: #FF9800; }
|
||||
.type-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; color: white; }
|
||||
.type-docker { background: #2196F3; }
|
||||
.type-npm { background: #CB3837; }
|
||||
.default-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; background: #673AB7; color: white; margin-left: 8px; }
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<cloudly-sectionheading>External Registries</cloudly-sectionheading>
|
||||
<dees-table
|
||||
.heading1=${'External Registries'}
|
||||
.heading2=${'Configure external Docker and NPM registries'}
|
||||
.data=${this.data.externalRegistries || []}
|
||||
.displayFunction=${(registry: plugins.interfaces.data.IExternalRegistry) => {
|
||||
return {
|
||||
Name: html`${registry.data.name}${registry.data.isDefault ? html`<span class="default-badge">DEFAULT</span>` : ''}`,
|
||||
Type: html`<span class="type-badge type-${registry.data.type}">${registry.data.type.toUpperCase()}</span>`,
|
||||
URL: registry.data.url,
|
||||
Auth: registry.data.authType === 'none' ? 'Public' : (registry.data.username || 'Token Auth'),
|
||||
Namespace: registry.data.namespace || '-',
|
||||
Status: html`<span class="status-badge status-${registry.data.status || 'unverified'}">${(registry.data.status || 'unverified').toUpperCase()}</span>`,
|
||||
'Last Verified': registry.data.lastVerified ? new Date(registry.data.lastVerified).toLocaleString() : 'Never',
|
||||
};
|
||||
}}
|
||||
.dataActions=${[
|
||||
{ name: 'Add Registry', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
|
||||
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add External Registry', content: html`
|
||||
<dees-form>
|
||||
<dees-input-dropdown .key=${'type'} .label=${'Registry Type'} .options=${[{key: 'docker', option: 'Docker'}, {key: 'npm', option: 'NPM'}]} .value=${'docker'} .required=${true}></dees-input-dropdown>
|
||||
<dees-input-text .key=${'name'} .label=${'Registry Name'} .placeholder=${'My Docker Hub'} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'url'} .label=${'Registry URL'} .placeholder=${'https://index.docker.io/v2/ or registry.gitlab.com'} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'username'} .label=${'Username (only needed for basic auth)'} .placeholder=${'username or leave empty for token auth'}></dees-input-text>
|
||||
<dees-input-text .key=${'password'} .label=${'Password / Token (NPM _authToken, Docker access token, etc.)'} .placeholder=${'Token or password'} .isPasswordBool=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'namespace'} .label=${'Namespace/Organization (optional)'} .placeholder=${'myorg'}></dees-input-text>
|
||||
<dees-input-text .key=${'description'} .label=${'Description (optional)'} .placeholder=${'Production Docker registry'}></dees-input-text>
|
||||
<dees-input-dropdown .key=${'authType'} .label=${'Authentication Type'} .options=${[{key: 'none', option: 'No Authentication (Public Registry)'}, {key: 'basic', option: 'Basic Auth (Username + Password)'}, {key: 'token', option: 'Token Only (NPM, GitHub, GitLab tokens)'}, {key: 'oauth2', option: 'OAuth2 (Advanced)'}]} .value=${'none'}></dees-input-dropdown>
|
||||
<dees-input-checkbox .key=${'isDefault'} .label=${'Set as default registry for this type'} .value=${false}></dees-input-checkbox>
|
||||
<dees-input-checkbox .key=${'insecure'} .label=${'Allow insecure connections (HTTP/self-signed certs)'} .value=${false}></dees-input-checkbox>
|
||||
</dees-form>
|
||||
`, menuOptions: [ { name: 'Create Registry', action: async (modalArg: any) => { const form = modalArg.shadowRoot.querySelector('dees-form') as any; const formData = await form.gatherData(); await appstate.dataState.dispatchAction(appstate.createExternalRegistryAction, { registryData: { type: formData.type, name: formData.name, url: formData.url, username: formData.username, password: formData.password, namespace: formData.namespace || undefined, description: formData.description || undefined, authType: formData.authType, isDefault: formData.isDefault, insecure: formData.insecure, }, }); await modalArg.destroy(); } }, { name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } } ] });
|
||||
} },
|
||||
{ name: 'Edit', iconName: 'edit', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
|
||||
const registry = actionDataArg.item as plugins.interfaces.data.IExternalRegistry;
|
||||
await plugins.deesCatalog.DeesModal.createAndShow({ heading: `Edit Registry: ${registry.data.name}`, content: html`
|
||||
<dees-form>
|
||||
<dees-input-dropdown .key=${'type'} .label=${'Registry Type'} .options=${[{key: 'docker', option: 'Docker'}, {key: 'npm', option: 'NPM'}]} .value=${registry.data.type} .required=${true}></dees-input-dropdown>
|
||||
<dees-input-text .key=${'name'} .label=${'Registry Name'} .value=${registry.data.name} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'url'} .label=${'Registry URL'} .value=${registry.data.url} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'username'} .label=${'Username (only needed for basic auth)'} .value=${registry.data.username || ''} .placeholder=${'Leave empty for token auth'}></dees-input-text>
|
||||
<dees-input-text .key=${'password'} .label=${'Password / Token (leave empty to keep current)'} .placeholder=${'New token or password'} .isPasswordBool=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'namespace'} .label=${'Namespace/Organization (optional)'} .value=${registry.data.namespace || ''}></dees-input-text>
|
||||
<dees-input-text .key=${'description'} .label=${'Description (optional)'} .value=${registry.data.description || ''}></dees-input-text>
|
||||
<dees-input-dropdown .key=${'authType'} .label=${'Authentication Type'} .options=${[{key: 'none', option: 'No Authentication (Public Registry)'}, {key: 'basic', option: 'Basic Auth (Username + Password)'}, {key: 'token', option: 'Token Only (NPM, GitHub, GitLab tokens)'}, {key: 'oauth2', option: 'OAuth2 (Advanced)'}]} .value=${registry.data.authType || 'none'}></dees-input-dropdown>
|
||||
<dees-input-checkbox .key=${'isDefault'} .label=${'Set as default registry for this type'} .value=${registry.data.isDefault || false}></dees-input-checkbox>
|
||||
<dees-input-checkbox .key=${'insecure'} .label=${'Allow insecure connections (HTTP/self-signed certs)'} .value=${registry.data.insecure || false}></dees-input-checkbox>
|
||||
</dees-form>
|
||||
`, menuOptions: [ { name: 'Update Registry', action: async (modalArg: any) => { const form = modalArg.shadowRoot.querySelector('dees-form') as any; const formData = await form.gatherData(); const updateData: any = { type: formData.type, name: formData.name, url: formData.url, username: formData.username, namespace: formData.namespace || undefined, description: formData.description || undefined, authType: formData.authType, isDefault: formData.isDefault, insecure: formData.insecure, }; if (formData.password) { updateData.password = formData.password; } await appstate.dataState.dispatchAction(appstate.updateExternalRegistryAction, { registryId: registry.id, updates: updateData, }); await modalArg.destroy(); } }, { name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } } ] });
|
||||
} },
|
||||
{ name: 'Test Connection', iconName: 'check-circle', type: ['contextmenu'], actionFunc: async (actionDataArg: any) => {
|
||||
const registry = actionDataArg.item as plugins.interfaces.data.IExternalRegistry;
|
||||
const loadingModal = await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Testing Registry Connection', content: html`<div style="text-align: center; padding: 20px;"><dees-spinner></dees-spinner><p style="margin-top: 20px;">Testing connection to ${registry.data.name}...</p></div>`, menuOptions: [] });
|
||||
await appstate.dataState.dispatchAction(appstate.verifyExternalRegistryAction, { registryId: registry.id, });
|
||||
await loadingModal.destroy();
|
||||
const updatedRegistry = this.data.externalRegistries?.find(r => r.id === registry.id);
|
||||
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Connection Test Result', content: html`<div style="text-align: center; padding: 20px;">${updatedRegistry?.data.status === 'active' ? html`<div style="color: #4CAF50; font-size: 48px;">✓</div><p style="margin-top: 20px; color: #4CAF50;">Connection successful!</p>` : html`<div style="color: #f44336; font-size: 48px;">✗</div><p style="margin-top: 20px; color: #f44336;">Connection failed!</p>${updatedRegistry?.data.lastError ? html`<p style="margin-top: 10px; font-size: 0.9em; color: #999;">Error: ${updatedRegistry.data.lastError}</p>` : ''}`}</div>`, menuOptions: [ { name: 'OK', action: async (modalArg: any) => { await modalArg.destroy(); } } ] });
|
||||
} },
|
||||
{ name: 'Delete', iconName: 'trash', type: ['contextmenu'], actionFunc: async (actionDataArg: any) => {
|
||||
const registry = actionDataArg.item as plugins.interfaces.data.IExternalRegistry;
|
||||
plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete Registry: ${registry.data.name}`, content: html`<div style="text-align:center"><p>Do you really want to delete this external registry?</p><p style="color: #999; font-size: 0.9em; margin-top: 10px;">This will remove all stored credentials and configuration.</p></div><div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${registry.data.name} (${registry.data.url})</div>`, menuOptions: [ { name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'Delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteExternalRegistryAction, { registryId: registry.id, }); await modalArg.destroy(); } } ] });
|
||||
} },
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global { interface HTMLElementTagNameMap { 'cloudly-view-externalregistries': CloudlyViewExternalRegistries; } }
|
||||
|
142
ts_web/elements/views/images/index.ts
Normal file
142
ts_web/elements/views/images/index.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
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-images')
|
||||
export class CloudlyViewImages extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = {} as any;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
appstate.dataState
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((dataArg) => {
|
||||
this.data = dataArg;
|
||||
});
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css``,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<cloudly-sectionheading>Images</cloudly-sectionheading>
|
||||
<dees-table
|
||||
heading1="Images"
|
||||
heading2="an image is needed for running a service"
|
||||
.data=${this.data.images}
|
||||
.displayFunction=${(image: plugins.interfaces.data.IImage) => {
|
||||
return { id: image.id, name: image.data.name, description: image.data.description, versions: image.data.versions.length };
|
||||
}}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'create Image',
|
||||
type: ['header', 'footer'],
|
||||
iconName: 'plus',
|
||||
actionFunc: async () => {
|
||||
plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: 'create new Image',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .label=${'name'} .key=${'data.name'} .value=${''}></dees-input-text>
|
||||
<dees-input-text .label=${'description'} .key=${'data.description'} .value=${''}></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } },
|
||||
{ name: 'save', action: async (modalArg: any) => {
|
||||
const deesForm = modalArg.shadowRoot.querySelector('dees-form');
|
||||
const formData = await deesForm.collectFormData();
|
||||
await appstate.dataState.dispatchAction(appstate.createImageAction, { imageName: formData['data.name'] as string, description: formData['data.description'] as string });
|
||||
await modalArg.destroy();
|
||||
} },
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'edit',
|
||||
type: ['contextmenu', 'inRow', 'doubleClick'],
|
||||
iconName: 'penToSquare',
|
||||
actionFunc: async (dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>) => {
|
||||
const environmentsArray: Array<plugins.interfaces.data.ISecretGroup['data']['environments'][any] & { environment: string; }> = [];
|
||||
for (const environmentName of Object.keys(dataArg.item.data.environments)) {
|
||||
environmentsArray.push({ environment: environmentName, ...dataArg.item.data.environments[environmentName] });
|
||||
}
|
||||
await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: 'Edit Secret',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'id'} .disabled=${true} .label=${'ID'} .value=${dataArg.item.id}></dees-input-text>
|
||||
<dees-input-text .key=${'data.name'} .disabled=${false} .label=${'name'} .value=${dataArg.item.data.name}></dees-input-text>
|
||||
<dees-input-text .key=${'data.description'} .disabled=${false} .label=${'description'} .value=${dataArg.item.data.description}></dees-input-text>
|
||||
<dees-input-text .key=${'data.key'} .disabled=${false} .label=${'key'} .value=${dataArg.item.data.key}></dees-input-text>
|
||||
<dees-table .key=${'environments'} .heading1=${'Environments'} .heading2=${'double-click to edit values'}
|
||||
.data=${environmentsArray.map((itemArg) => ({ environment: itemArg.environment, value: itemArg.value }))}
|
||||
.editableFields=${['environment', 'value']}
|
||||
.dataActions=${[{ name: 'delete', iconName: 'trash', type: ['inRow'], actionFunc: async (actionDataArg: any) => { actionDataArg.table.data.splice(actionDataArg.table.data.indexOf(actionDataArg.item), 1); } }] as plugins.deesCatalog.ITableAction[]}>
|
||||
</dees-table>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'Cancel', iconName: null, action: async (modalArg: any) => { await modalArg.destroy(); } },
|
||||
{ name: 'Save', iconName: null, action: async (modalArg: any) => { const data = await modalArg.shadowRoot.querySelector('dees-form').collectFormData(); console.log(data); } },
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'history',
|
||||
iconName: 'clockRotateLeft',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>) => {
|
||||
const historyArray: Array<{ environment: string; value: string; }> = [];
|
||||
for (const environment of Object.keys(dataArg.item.data.environments)) {
|
||||
for (const historyItem of dataArg.item.data.environments[environment].history) {
|
||||
historyArray.push({ environment, value: historyItem.value });
|
||||
}
|
||||
}
|
||||
await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `history for ${dataArg.item.data.key}`,
|
||||
content: html`<dees-table .data=${historyArray} .dataActions=${[{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (itemArg: plugins.deesCatalog.ITableActionDataArg<(typeof historyArray)[0]>) => { console.log('delete', itemArg); }, }] as plugins.deesCatalog.ITableAction[]}></dees-table>`,
|
||||
menuOptions: [ { name: 'close', action: async (modalArg: any) => { await modalArg.destroy(); } } ],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
iconName: 'trash',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (itemArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.IImage>) => {
|
||||
plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Delete Image "${itemArg.item.data.name}"`,
|
||||
content: html`
|
||||
<div style="text-align:center">Do you really want to delete the image?</div>
|
||||
<div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${itemArg.item.id}</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } },
|
||||
{ name: 'delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteImageAction, { imageId: itemArg.item.id, }); await modalArg.destroy(); } },
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'cloudly-view-images': CloudlyViewImages;
|
||||
}
|
||||
}
|
||||
|
61
ts_web/elements/views/logs/index.ts
Normal file
61
ts_web/elements/views/logs/index.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
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-logs')
|
||||
export class CloudlyViewLogs extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subecription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
|
||||
this.rxSubscriptions.push(subecription);
|
||||
}
|
||||
|
||||
public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css`` ];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<cloudly-sectionheading>Logs</cloudly-sectionheading>
|
||||
<dees-table
|
||||
.heading1=${'Logs'}
|
||||
.heading2=${'decoded in client'}
|
||||
.data=${this.data.deployments}
|
||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
||||
return { id: itemArg.id, serverAmount: itemArg.data.servers.length };
|
||||
}}
|
||||
.dataActions=${[
|
||||
{ name: 'add configBundle', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
|
||||
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add ConfigBundle', content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
||||
<dees-input-text .key=${'data.secretGroupIds'} .label=${'secretGroupIds'} .value=${''}></dees-input-text>
|
||||
<dees-input-text .key=${'data.includedTags'} .label=${'includedTags'} .value=${''}></dees-input-text>
|
||||
</dees-form>
|
||||
`, menuOptions: [ { name: 'create', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] });
|
||||
} },
|
||||
{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
|
||||
plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ConfigBundle ${actionDataArg.item.id}`, content: html`
|
||||
<div style="text-align:center">Do you really want to delete the ConfigBundle?</div>
|
||||
<div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${actionDataArg.item.id}</div>
|
||||
`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } } ] });
|
||||
} },
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global { interface HTMLElementTagNameMap { 'cloudly-view-logs': CloudlyViewLogs; } }
|
||||
|
61
ts_web/elements/views/mails/index.ts
Normal file
61
ts_web/elements/views/mails/index.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
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-mails')
|
||||
export class CloudlyViewMails extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subecription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
|
||||
this.rxSubscriptions.push(subecription);
|
||||
}
|
||||
|
||||
public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css`` ];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<cloudly-sectionheading>Mails</cloudly-sectionheading>
|
||||
<dees-table
|
||||
.heading1=${'Mails'}
|
||||
.heading2=${'decoded in client'}
|
||||
.data=${this.data.deployments}
|
||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
||||
return { id: itemArg.id, serverAmount: itemArg.data.servers.length };
|
||||
}}
|
||||
.dataActions=${[
|
||||
{ name: 'add configBundle', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
|
||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add ConfigBundle', content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
||||
<dees-input-text .key=${'data.secretGroupIds'} .label=${'secretGroupIds'} .value=${''}></dees-input-text>
|
||||
<dees-input-text .key=${'data.includedTags'} .label=${'includedTags'} .value=${''}></dees-input-text>
|
||||
</dees-form>
|
||||
`, menuOptions: [ { name: 'create', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] });
|
||||
} },
|
||||
{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
|
||||
plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ConfigBundle ${actionDataArg.item.id}`, content: html`
|
||||
<div style=\"text-align:center\">Do you really want to delete the ConfigBundle?</div>
|
||||
<div style=\"font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;\">${actionDataArg.item.id}</div>
|
||||
`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } } ] });
|
||||
} },
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global { interface HTMLElementTagNameMap { 'cloudly-view-mails': CloudlyViewMails; } }
|
||||
|
71
ts_web/elements/views/overview/index.ts
Normal file
71
ts_web/elements/views/overview/index.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
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-overview')
|
||||
export class CloudlyViewOverview extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = {
|
||||
secretGroups: [],
|
||||
secretBundles: [],
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subecription = appstate.dataState
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((dataArg) => {
|
||||
this.data = dataArg;
|
||||
});
|
||||
this.rxSubscriptions.push(subecription);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
dees-statsgrid { margin-top: 24px; }
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
const totalNodes = this.data.clusters?.reduce((sum, cluster) =>
|
||||
sum + (cluster.data.nodes?.length || 0), 0) || 0;
|
||||
|
||||
const statsTiles = [
|
||||
{ id: 'clusters', title: 'Total Clusters', value: this.data.clusters?.length || 0, type: 'number' as const, iconName: 'lucide:Network', description: 'Active clusters' },
|
||||
{ id: 'nodes', title: 'Total Nodes', value: totalNodes, type: 'number' as const, iconName: 'lucide:Server', description: 'Connected nodes' },
|
||||
{ id: 'services', title: 'Services', value: this.data.services?.length || 0, type: 'number' as const, iconName: 'lucide:Layers', description: 'Deployed services' },
|
||||
{ id: 'deployments', title: 'Deployments', value: this.data.deployments?.length || 0, type: 'number' as const, iconName: 'lucide:Rocket', description: 'Active deployments' },
|
||||
{ id: 'secretGroups', title: 'Secret Groups', value: this.data.secretGroups?.length || 0, type: 'number' as const, iconName: 'lucide:ShieldCheck', description: 'Configured secret groups' },
|
||||
{ id: 'secretBundles', title: 'Secret Bundles', value: this.data.secretBundles?.length || 0, type: 'number' as const, iconName: 'lucide:LockKeyhole', description: 'Available secret bundles' },
|
||||
{ id: 'images', title: 'Images', value: this.data.images?.length || 0, type: 'number' as const, iconName: 'lucide:Image', description: 'Container images' },
|
||||
{ id: 'dns', title: 'DNS Entries', value: this.data.dnsEntries?.length || 0, type: 'number' as const, iconName: 'lucide:Globe', description: 'Managed DNS records' },
|
||||
{ id: 'databases', title: 'Databases', value: this.data.dbs?.length || 0, type: 'number' as const, iconName: 'lucide:Database', description: 'Database instances' },
|
||||
{ id: 'backups', title: 'Backups', value: this.data.backups?.length || 0, type: 'number' as const, iconName: 'lucide:Save', description: 'Available backups' },
|
||||
{ id: 'mails', title: 'Mail Domains', value: this.data.mails?.length || 0, type: 'number' as const, iconName: 'lucide:Mail', description: 'Mail configurations' },
|
||||
{ id: 's3', title: 'S3 Buckets', value: this.data.s3?.length || 0, type: 'number' as const, iconName: 'lucide:Cloud', description: 'Storage buckets' },
|
||||
];
|
||||
|
||||
return html`
|
||||
<cloudly-sectionheading>Overview</cloudly-sectionheading>
|
||||
<dees-statsgrid .tiles=${statsTiles} .minTileWidth=${250} .gap=${16}></dees-statsgrid>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'cloudly-view-overview': CloudlyViewOverview;
|
||||
}
|
||||
}
|
||||
|
52
ts_web/elements/views/s3/index.ts
Normal file
52
ts_web/elements/views/s3/index.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
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-s3')
|
||||
export class CloudlyViewS3 extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subecription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
|
||||
this.rxSubscriptions.push(subecription);
|
||||
}
|
||||
|
||||
public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css`` ];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<cloudly-sectionheading>S3</cloudly-sectionheading>
|
||||
<dees-table
|
||||
.heading1=${'S3'}
|
||||
.heading2=${'decoded in client'}
|
||||
.data=${this.data.s3}
|
||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => { return { id: itemArg.id, serverAmount: itemArg.data.servers.length }; }}
|
||||
.dataActions=${[
|
||||
{ name: 'add configBundle', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
|
||||
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add ConfigBundle', content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
||||
<dees-input-text .key=${'data.secretGroupIds'} .label=${'secretGroupIds'} .value=${''}></dees-input-text>
|
||||
<dees-input-text .key=${'data.includedTags'} .label=${'includedTags'} .value=${''}></dees-input-text>
|
||||
</dees-form>
|
||||
`, menuOptions: [ { name: 'create', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] });
|
||||
} },
|
||||
{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
|
||||
plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ConfigBundle ${actionDataArg.item.id}`, content: html`
|
||||
<div style="text-align:center">Do you really want to delete the ConfigBundle?</div>
|
||||
<div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${actionDataArg.item.id}</div>
|
||||
`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } } ] });
|
||||
} },
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global { interface HTMLElementTagNameMap { 'cloudly-view-s3': CloudlyViewS3; } }
|
||||
|
76
ts_web/elements/views/secretbundles/index.ts
Normal file
76
ts_web/elements/views/secretbundles/index.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
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-secretbundles')
|
||||
export class CloudlyViewSecretBundles 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`` ];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<cloudly-sectionheading>SecretBundles</cloudly-sectionheading>
|
||||
<dees-table
|
||||
.heading1=${'SecretBundles'}
|
||||
.heading2=${'decoded in client'}
|
||||
.data=${this.data.secretBundles || []}
|
||||
.displayFunction=${(itemArg: plugins.interfaces.data.ISecretBundle) => {
|
||||
return {
|
||||
name: itemArg.data.name,
|
||||
secretGroups: (() => {
|
||||
const secretGroupIds = itemArg.data.includedSecretGroupIds;
|
||||
let secretGroupNames: string[] = [];
|
||||
for (const secretGroupId of secretGroupIds) {
|
||||
const secretGroup = this.data.secretGroups.find((secretGroupArg: any) => secretGroupArg.id === secretGroupId);
|
||||
if (secretGroup) { secretGroupNames.push(secretGroup.data.name); }
|
||||
}
|
||||
return secretGroupNames.join(', ');
|
||||
})(),
|
||||
tags: html`<dees-chips .selectionMode=${'none'} .selectableChips=${itemArg.data.includedTags}></dees-chips>`,
|
||||
};
|
||||
}}
|
||||
.dataActions=${[
|
||||
{ name: 'add SecretBundle', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
|
||||
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add SecretBundle', content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
||||
<dees-input-text .key=${'data.secretGroupIds'} .label=${'secretGroupIds'} .value=${''}></dees-input-text>
|
||||
<dees-input-text .key=${'data.includedTags'} .label=${'includedTags'} .value=${''}></dees-input-text>
|
||||
</dees-form>
|
||||
`, menuOptions: [ { name: 'create', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] });
|
||||
} },
|
||||
{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
|
||||
plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ConfigBundle ${actionDataArg.item.id}`, content: html`
|
||||
<div style="text-align:center">Do you really want to delete the ConfigBundle?</div>
|
||||
<div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${actionDataArg.item.id}</div>
|
||||
`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } } ] });
|
||||
} },
|
||||
{ name: 'edit', iconName: 'penToSquare', type: ['doubleClick', 'contextmenu', 'inRow'], actionFunc: async () => {
|
||||
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Edit SecretBundle', content: html`<dees-form><dees-input-text .label=${'purpose'}></dees-input-text></dees-form>`, menuOptions: [ { name: 'save', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] });
|
||||
} },
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global { interface HTMLElementTagNameMap { 'cloudly-view-secretbundles': CloudlyViewSecretBundles; } }
|
||||
|
77
ts_web/elements/views/secretgroups/index.ts
Normal file
77
ts_web/elements/views/secretgroups/index.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
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-secretsgroups')
|
||||
export class CloudlyViewSecretGroups 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`` ];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<cloudly-sectionheading>SecretGroups</cloudly-sectionheading>
|
||||
<dees-table
|
||||
heading1="SecretGroups"
|
||||
heading2="decoded in client"
|
||||
.data=${this.data.secretGroups || []}
|
||||
.displayFunction=${(secretGroup: plugins.interfaces.data.ISecretGroup) => {
|
||||
return {
|
||||
name: secretGroup.data.name,
|
||||
priority: secretGroup.data.priority,
|
||||
tags: html`<dees-chips .selectionMode=${'none'} .selectableChips=${secretGroup.data.tags}></dees-chips>`,
|
||||
key: secretGroup.data.key,
|
||||
history: (() => { const allHistory = []; for (const environment in secretGroup.data.environments) { allHistory.push(...secretGroup.data.environments[environment].history); } return allHistory.length; })(),
|
||||
};
|
||||
}}
|
||||
.dataActions=${[
|
||||
{ name: 'add SecretGroup', type: ['header', 'footer'], iconName: 'plus', actionFunc: async () => {
|
||||
plugins.deesCatalog.DeesModal.createAndShow({ heading: 'create new SecretGroup', content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .label=${'name'} .key=${'data.name'} .value=${''}></dees-input-text>
|
||||
<dees-input-text .label=${'description'} .key=${'data.description'} .value=${''}></dees-input-text>
|
||||
<dees-input-text .label=${'Secret Key (data.key)'} .key=${'data.key'} .value=${''}></dees-input-text>
|
||||
<dees-table heading1=${'Environments'} heading2=${'keys need to be unique'} key="environments" .data=${[{ environment: 'production', value: '' }, { environment: 'staging', value: '' }]} .dataActions=${[{ name: 'add environment', iconName: 'plus', type: ['footer'], actionFunc: async (dataArg: any) => { dataArg.table.data.push({ environment: 'new environment', value: '' }); dataArg.table.requestUpdate('data'); } }, { name: 'delete environment', iconName: 'trash', type: ['inRow'], actionFunc: async (dataArg: any) => { dataArg.table.data.splice(dataArg.table.data.indexOf(dataArg.item), 1); dataArg.table.requestUpdate('data'); } }] as plugins.deesCatalog.ITableAction[]} .editableFields=${['environment', 'value']}>
|
||||
</dees-table>
|
||||
</dees-form>
|
||||
`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'save', action: async (modalArg: any) => { const deesForm = modalArg.shadowRoot.querySelector('dees-form'); const formData = await deesForm.collectFormData(); const environments: plugins.interfaces.data.ISecretGroup['data']['environments'] = {}; for (const itemArg of formData['environments'] as any[]) { environments[itemArg.environment] = { value: itemArg.value, history: [], lastUpdated: Date.now(), }; } await appstate.dataState.dispatchAction(appstate.createSecretGroupAction, { id: null, data: { name: formData['data.name'] as string, description: formData['data.description'] as string, key: formData['data.key'] as string, environments, tags: [], }, }); await modalArg.destroy(); } } ] });
|
||||
} },
|
||||
{ name: 'edit', type: ['contextmenu', 'inRow', 'doubleClick'], iconName: 'penToSquare', actionFunc: async (dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>) => {
|
||||
const environmentsArray: Array<plugins.interfaces.data.ISecretGroup['data']['environments'][any] & { environment: string; }> = [];
|
||||
for (const environmentName of Object.keys(dataArg.item.data.environments)) { environmentsArray.push({ environment: environmentName, ...dataArg.item.data.environments[environmentName], }); }
|
||||
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Edit Secret', content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'id'} .disabled=${true} .label=${'ID'} .value=${dataArg.item.id}></dees-input-text>
|
||||
<dees-input-text .key=${'data.name'} .disabled=${false} .label=${'name'} .value=${dataArg.item.data.name}></dees-input-text>
|
||||
<dees-input-text .key=${'data.description'} .disabled=${false} .label=${'description'} .value=${dataArg.item.data.description}></dees-input-text>
|
||||
<dees-input-text .key=${'data.key'} .disabled=${false} .label=${'key'} .value=${dataArg.item.data.key}></dees-input-text>
|
||||
<dees-table .key=${'environments'} .heading1=${'Environments'} .heading2=${'double-click to edit values'} .data=${environmentsArray.map((itemArg) => ({ environment: itemArg.environment, value: itemArg.value, }))} .editableFields=${['environment', 'value']} .dataActions=${[{ name: 'delete', iconName: 'trash', type: ['inRow'], actionFunc: async (actionDataArg: any) => { actionDataArg.table.data.splice(actionDataArg.table.data.indexOf(actionDataArg.item), 1); } }] as plugins.deesCatalog.ITableAction[]}>
|
||||
</dees-table>
|
||||
</dees-form>
|
||||
`, menuOptions: [ { name: 'Cancel', iconName: null, action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'Save', iconName: null, action: async (modalArg: any) => { const data = await modalArg.shadowRoot.querySelector('dees-form').collectFormData(); console.log(data); } } ] });
|
||||
} },
|
||||
{ name: 'history', iconName: 'clockRotateLeft', type: ['contextmenu', 'inRow'], actionFunc: async (dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>) => {
|
||||
const historyArray: Array<{ environment: string; value: string; }> = []; for (const environment of Object.keys(dataArg.item.data.environments)) { for (const historyItem of dataArg.item.data.environments[environment].history) { historyArray.push({ environment, value: historyItem.value, }); } }
|
||||
await plugins.deesCatalog.DeesModal.createAndShow({ heading: `history for ${dataArg.item.data.key}`, content: html`<dees-table .data=${historyArray} .dataActions=${[{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (itemArg: plugins.deesCatalog.ITableActionDataArg<(typeof historyArray)[0]>) => { console.log('delete', itemArg); }, }] as plugins.deesCatalog.ITableAction[]}></dees-table>`, menuOptions: [ { name: 'close', action: async (modalArg: any) => { await modalArg.destroy(); } } ] });
|
||||
} },
|
||||
{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (itemArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>) => {
|
||||
plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ${itemArg.item.data.key}`, content: html`<div style="text-align:center">Do you really want to delete the secret?</div><div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${itemArg.item.data.key}</div>`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteSecretGroupAction, { secretGroupId: itemArg.item.id, }); await modalArg.destroy(); } } ] });
|
||||
} },
|
||||
] as plugins.deesCatalog.ITableAction[]}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global { interface HTMLElementTagNameMap { 'cloudly-view-secretsgroups': CloudlyViewSecretGroups; } }
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from '../elements/shared/index.js';
|
||||
import * as plugins from '../../../plugins.js';
|
||||
import * as shared from '../../shared/index.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
@@ -10,12 +10,12 @@ import {
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as appstate from '../appstate.js';
|
||||
import * as appstate from '../../../appstate.js';
|
||||
|
||||
@customElement('cloudly-view-services')
|
||||
export class CloudlyViewServices extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = {};
|
||||
private data: appstate.IDataState = {} as any;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -31,45 +31,20 @@ export class CloudlyViewServices extends DeesElement {
|
||||
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;
|
||||
}
|
||||
.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';
|
||||
case 'base': return 'lucide:ServerCog';
|
||||
case 'distributed': return 'lucide:Network';
|
||||
case 'workload': return 'lucide:Container';
|
||||
default: return 'lucide:Box';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,70 +85,28 @@ export class CloudlyViewServices extends DeesElement {
|
||||
name: 'Add Service',
|
||||
iconName: 'plus',
|
||||
type: ['header', 'footer'],
|
||||
actionFunc: async (dataActionArg) => {
|
||||
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-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-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) => {
|
||||
{ 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,
|
||||
@@ -186,24 +119,15 @@ export class CloudlyViewServices extends DeesElement {
|
||||
imageVersion: formData.imageVersion,
|
||||
scaleFactor: parseInt(formData.scaleFactor),
|
||||
balancingStrategy: formData.balancingStrategy,
|
||||
ports: {
|
||||
web: parseInt(formData.webPort),
|
||||
},
|
||||
ports: { web: parseInt(formData.webPort) },
|
||||
environment: {},
|
||||
domains: [],
|
||||
deploymentIds: [],
|
||||
},
|
||||
});
|
||||
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Cancel',
|
||||
action: async (modalArg) => {
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
}},
|
||||
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||
],
|
||||
});
|
||||
},
|
||||
@@ -212,7 +136,7 @@ export class CloudlyViewServices extends DeesElement {
|
||||
name: 'Edit',
|
||||
iconName: 'edit',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
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}`,
|
||||
@@ -220,55 +144,19 @@ export class CloudlyViewServices extends DeesElement {
|
||||
<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-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-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) => {
|
||||
{ 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: {
|
||||
@@ -284,16 +172,9 @@ export class CloudlyViewServices extends DeesElement {
|
||||
balancingStrategy: formData.balancingStrategy,
|
||||
},
|
||||
});
|
||||
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Cancel',
|
||||
action: async (modalArg) => {
|
||||
modalArg.destroy();
|
||||
},
|
||||
},
|
||||
}},
|
||||
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||
],
|
||||
});
|
||||
},
|
||||
@@ -302,9 +183,8 @@ export class CloudlyViewServices extends DeesElement {
|
||||
name: 'Deploy',
|
||||
iconName: 'rocket',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
actionFunc: async (actionDataArg: any) => {
|
||||
const service = actionDataArg.item as plugins.interfaces.data.IService;
|
||||
// TODO: Implement deployment action
|
||||
console.log('Deploy service:', service);
|
||||
},
|
||||
},
|
||||
@@ -312,38 +192,21 @@ export class CloudlyViewServices extends DeesElement {
|
||||
name: 'Delete',
|
||||
iconName: 'trash',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
actionFunc: async (actionDataArg) => {
|
||||
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="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 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) => {
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
action: async (modalArg) => {
|
||||
await appstate.dataState.dispatchAction(appstate.deleteServiceAction, {
|
||||
serviceId: service.id,
|
||||
});
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
{ 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(); } },
|
||||
],
|
||||
});
|
||||
},
|
||||
@@ -353,3 +216,10 @@ export class CloudlyViewServices extends DeesElement {
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'cloudly-view-services': CloudlyViewServices;
|
||||
}
|
||||
}
|
||||
|
206
ts_web/elements/views/settings/index.ts
Normal file
206
ts_web/elements/views/settings/index.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
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-settings')
|
||||
export class CloudlyViewSettings extends DeesElement {
|
||||
@state()
|
||||
private settings: plugins.interfaces.data.ICloudlySettingsMasked = {} as any;
|
||||
|
||||
@state()
|
||||
private isLoading = false;
|
||||
|
||||
@state()
|
||||
private testResults: {[key: string]: {success: boolean; message: string}} = {};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.loadSettings();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
.settings-container { padding: 24px 0; display: flex; flex-direction: column; gap: 16px; }
|
||||
.provider-icon { margin-right: 8px; font-size: 20px; }
|
||||
.test-status { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
||||
.test-status dees-button { margin-left: auto; }
|
||||
.loading-container { display: flex; justify-content: center; padding: 48px; }
|
||||
.actions-container { display: flex; justify-content: center; margin-top: 24px; }
|
||||
dees-panel { margin-bottom: 16px; }
|
||||
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||||
.form-grid.single { grid-template-columns: 1fr; }
|
||||
@media (max-width: 768px) { .form-grid { grid-template-columns: 1fr; } }
|
||||
`,
|
||||
];
|
||||
|
||||
private async loadSettings() {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const response = await appstate.apiClient.settings.getSettings();
|
||||
this.settings = response.settings;
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load settings:', error);
|
||||
plugins.deesCatalog.DeesToast.createAndShow({ message: `Failed to load settings: ${error.message}`, type: 'error' });
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async saveSettings(formData: any) {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const updates: Partial<plugins.interfaces.data.ICloudlySettings> = {};
|
||||
for (const [key, value] of Object.entries(formData)) {
|
||||
if (value !== undefined && value !== '****' && !value?.toString().endsWith('****')) {
|
||||
updates[key as keyof plugins.interfaces.data.ICloudlySettings] = value as string;
|
||||
}
|
||||
}
|
||||
const response = await appstate.apiClient.settings.updateSettings(updates);
|
||||
if (response.success) {
|
||||
plugins.deesCatalog.DeesToast.createAndShow({ message: 'Settings saved successfully', type: 'success' });
|
||||
await this.loadSettings();
|
||||
} else {
|
||||
throw new Error(response.message);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Failed to save settings:', error);
|
||||
plugins.deesCatalog.DeesToast.createAndShow({ message: `Failed to save settings: ${error.message}`, type: 'error' });
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async testConnection(provider: string) {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const response = await appstate.apiClient.settings.testProviderConnection(provider);
|
||||
this.testResults = { ...this.testResults, [provider]: { success: response.connectionValid, message: response.message } };
|
||||
plugins.deesCatalog.DeesToast.createAndShow({ message: response.message, type: response.connectionValid ? 'success' : 'error' });
|
||||
} catch (error: any) {
|
||||
this.testResults = { ...this.testResults, [provider]: { success: false, message: `Test failed: ${error.message}` } };
|
||||
plugins.deesCatalog.DeesToast.createAndShow({ message: `Connection test failed: ${error.message}`, type: 'error' });
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private renderProviderStatus(provider: string) {
|
||||
const result = this.testResults[provider];
|
||||
if (!result) return '' as any;
|
||||
return html`<dees-badge .type=${result.success ? 'success' : 'error'} .text=${result.success ? 'Connected' : 'Failed'}></dees-badge>`;
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.isLoading && Object.keys(this.settings).length === 0) {
|
||||
return html`<div class="loading-container"><dees-spinner></dees-spinner></div>`;
|
||||
}
|
||||
return html`
|
||||
<cloudly-sectionheading>Settings</cloudly-sectionheading>
|
||||
<div class="settings-container">
|
||||
<dees-form @formData=${(e: CustomEvent) => { this.saveSettings((e.detail as any).data); }}>
|
||||
<dees-panel .title=${'Hetzner Cloud'} .subtitle=${'Configure Hetzner Cloud API access'} .variant=${'outline'}>
|
||||
<div class="test-status">
|
||||
${this.renderProviderStatus('hetzner')}
|
||||
<dees-button .text=${'Test Connection'} .type=${'secondary'} @click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); this.testConnection('hetzner'); }}></dees-button>
|
||||
</div>
|
||||
<div class="form-grid single">
|
||||
<dees-input-text .key=${'hetznerToken'} .label=${'API Token'} .value=${this.settings.hetznerToken || ''} .isPasswordBool=${true} .description=${'Your Hetzner Cloud API token for managing infrastructure'} .required=${false}></dees-input-text>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Cloudflare'} .subtitle=${'Configure Cloudflare API access'} .variant=${'outline'}>
|
||||
<div class="test-status">
|
||||
${this.renderProviderStatus('cloudflare')}
|
||||
<dees-button .text=${'Test Connection'} .type=${'secondary'} @click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); this.testConnection('cloudflare'); }}></dees-button>
|
||||
</div>
|
||||
<div class="form-grid single">
|
||||
<dees-input-text .key=${'cloudflareToken'} .label=${'API Token'} .value=${this.settings.cloudflareToken || ''} .isPasswordBool=${true} .description=${'Cloudflare API token with DNS and Zone permissions'} .required=${false}></dees-input-text>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Amazon Web Services'} .subtitle=${'Configure AWS credentials'} .variant=${'outline'}>
|
||||
<div class="test-status">
|
||||
${this.renderProviderStatus('aws')}
|
||||
<dees-button .text=${'Test Connection'} .type=${'secondary'} @click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); this.testConnection('aws'); }}></dees-button>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<dees-input-text .key=${'awsAccessKey'} .label=${'Access Key ID'} .value=${this.settings.awsAccessKey || ''} .isPasswordBool=${true} .description=${'AWS IAM access key identifier'} .required=${false}></dees-input-text>
|
||||
<dees-input-text .key=${'awsSecretKey'} .label=${'Secret Access Key'} .value=${this.settings.awsSecretKey || ''} .isPasswordBool=${true} .description=${'AWS IAM secret access key'} .required=${false}></dees-input-text>
|
||||
</div>
|
||||
<div class="form-grid single">
|
||||
<dees-input-dropdown .key=${'awsRegion'} .label=${'Default Region'} .selectedOption=${this.settings.awsRegion || 'us-east-1'} .options=${[
|
||||
{ key: 'us-east-1', option: 'US East (N. Virginia)', payload: null },
|
||||
{ key: 'us-west-2', option: 'US West (Oregon)', payload: null },
|
||||
{ key: 'eu-west-1', option: 'EU (Ireland)', payload: null },
|
||||
{ key: 'eu-central-1', option: 'EU (Frankfurt)', payload: null },
|
||||
{ key: 'ap-southeast-1', option: 'Asia Pacific (Singapore)', payload: null },
|
||||
{ key: 'ap-northeast-1', option: 'Asia Pacific (Tokyo)', payload: null },
|
||||
]} .description=${'Default AWS region for resource provisioning'}></dees-input-dropdown>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'DigitalOcean'} .subtitle=${'Configure DigitalOcean API access'} .variant=${'outline'}>
|
||||
<div class="test-status">
|
||||
${this.renderProviderStatus('digitalocean')}
|
||||
<dees-button .text=${'Test Connection'} .type=${'secondary'} @click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); this.testConnection('digitalocean'); }}></dees-button>
|
||||
</div>
|
||||
<div class="form-grid single">
|
||||
<dees-input-text .key=${'digitalOceanToken'} .label=${'Personal Access Token'} .value=${this.settings.digitalOceanToken || ''} .isPasswordBool=${true} .description=${'DigitalOcean personal access token with read/write scope'} .required=${false}></dees-input-text>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Microsoft Azure'} .subtitle=${'Configure Azure service principal'} .variant=${'outline'}>
|
||||
<div class="test-status">
|
||||
${this.renderProviderStatus('azure')}
|
||||
<dees-button .text=${'Test Connection'} .type=${'secondary'} @click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); this.testConnection('azure'); }}></dees-button>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<dees-input-text .key=${'azureClientId'} .label=${'Application (Client) ID'} .value=${this.settings.azureClientId || ''} .isPasswordBool=${true} .description=${'Azure AD application client ID'} .required=${false}></dees-input-text>
|
||||
<dees-input-text .key=${'azureClientSecret'} .label=${'Client Secret'} .value=${this.settings.azureClientSecret || ''} .isPasswordBool=${true} .description=${'Azure AD application client secret'} .required=${false}></dees-input-text>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<dees-input-text .key=${'azureTenantId'} .label=${'Directory (Tenant) ID'} .value=${this.settings.azureTenantId || ''} .description=${'Azure AD tenant identifier'} .required=${false}></dees-input-text>
|
||||
<dees-input-text .key=${'azureSubscriptionId'} .label=${'Subscription ID'} .value=${this.settings.azureSubscriptionId || ''} .description=${'Azure subscription for resource management'} .required=${false}></dees-input-text>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Google Cloud Platform'} .subtitle=${'Configure GCP service account'} .variant=${'outline'}>
|
||||
<div class="test-status">
|
||||
${this.renderProviderStatus('google')}
|
||||
<dees-button .text=${'Test Connection'} .type=${'secondary'} @click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); this.testConnection('google'); }}></dees-button>
|
||||
</div>
|
||||
<div class="form-grid single">
|
||||
<dees-input-textarea .key=${'googleCloudKeyJson'} .label=${'Service Account Key (JSON)'} .value=${this.settings.googleCloudKeyJson || ''} .isPasswordBool=${true} .description=${'Complete JSON key file for service account authentication'} .required=${false}></dees-input-textarea>
|
||||
</div>
|
||||
<div class="form-grid single">
|
||||
<dees-input-text .key=${'googleCloudProjectId'} .label=${'Project ID'} .value=${this.settings.googleCloudProjectId || ''} .description=${'Google Cloud project identifier'} .required=${false}></dees-input-text>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<div class="actions-container">
|
||||
<dees-form-submit .text=${'Save All Settings'} .disabled=${this.isLoading}></dees-form-submit>
|
||||
</div>
|
||||
</dees-form>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'cloudly-view-settings': CloudlyViewSettings;
|
||||
}
|
||||
}
|
||||
|
308
ts_web/elements/views/tasks/index.ts
Normal file
308
ts_web/elements/views/tasks/index.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
import * as shared from '../../shared/index.js';
|
||||
import * as plugins from '../../../plugins.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as appstate from '../../../appstate.js';
|
||||
import './parts/cloudly-task-panel.js';
|
||||
import './parts/cloudly-execution-details.js';
|
||||
import { formatCronFriendly, formatDate, formatDuration } from './utils.js';
|
||||
|
||||
@customElement('cloudly-view-tasks')
|
||||
export class CloudlyViewTasks extends DeesElement {
|
||||
@state()
|
||||
private data: appstate.IDataState = {} as any;
|
||||
|
||||
@state()
|
||||
private selectedExecution: plugins.interfaces.data.ITaskExecution | null = null;
|
||||
|
||||
@state()
|
||||
private loading = false;
|
||||
|
||||
@state()
|
||||
private filterStatus: string = 'all';
|
||||
|
||||
@state()
|
||||
private searchQuery: string = '';
|
||||
|
||||
@state()
|
||||
private categoryFilter: string = 'all';
|
||||
|
||||
@state()
|
||||
private autoRefresh: boolean = true;
|
||||
|
||||
private _refreshHandle: any = null;
|
||||
@state()
|
||||
private canceling: Record<string, boolean> = {};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subscription = appstate.dataState
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((dataArg) => {
|
||||
this.data = dataArg;
|
||||
});
|
||||
this.rxSubscriptions.push(subscription);
|
||||
|
||||
// Load initial data (non-blocking)
|
||||
this.loadInitialData();
|
||||
|
||||
// Start periodic refresh (lightweight; executions only by default)
|
||||
this.startAutoRefresh();
|
||||
}
|
||||
|
||||
private async loadInitialData() {
|
||||
try {
|
||||
await appstate.dataState.dispatchAction(appstate.taskActions.getTasks, {});
|
||||
await appstate.dataState.dispatchAction(appstate.taskActions.getTaskExecutions, {});
|
||||
} catch (error) {
|
||||
console.error('Failed to load initial task data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private startAutoRefresh() {
|
||||
this.stopAutoRefresh();
|
||||
if (!this.autoRefresh) return;
|
||||
this._refreshHandle = setInterval(async () => {
|
||||
try {
|
||||
await this.loadExecutionsWithFilter();
|
||||
} catch (err) {
|
||||
// ignore transient errors during refresh
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
private stopAutoRefresh() {
|
||||
if (this._refreshHandle) {
|
||||
clearInterval(this._refreshHandle);
|
||||
this._refreshHandle = null;
|
||||
}
|
||||
}
|
||||
|
||||
public async disconnectedCallback(): Promise<void> {
|
||||
await (super.disconnectedCallback?.());
|
||||
this.stopAutoRefresh();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
.toolbar { display: flex; gap: 12px; align-items: center; margin: 4px 0 16px 0; flex-wrap: wrap; }
|
||||
.toolbar .spacer { flex: 1 1 auto; }
|
||||
.search-input { background: #111; color: #ddd; border: 1px solid #333; border-radius: 6px; padding: 8px 10px; min-width: 220px; }
|
||||
.chipbar { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.chip { padding: 6px 10px; background: #2a2a2a; color: #bbb; border: 1px solid #444; border-radius: 16px; cursor: pointer; transition: all 0.2s; user-select: none; }
|
||||
.chip.active { background: #2196f3; border-color: #2196f3; color: white; }
|
||||
|
||||
.task-list { display: flex; flex-direction: column; gap: 16px; margin-bottom: 32px; }
|
||||
|
||||
.secondary-button { padding: 6px 12px; background: #2b2b2b; color: #ccc; border: 1px solid #444; border-radius: 6px; cursor: pointer; font-size: 0.9em; transition: background 0.2s, border-color 0.2s; }
|
||||
.secondary-button:hover { background: #363636; border-color: #555; }
|
||||
|
||||
/* Shared badge styles used within the table content */
|
||||
.status-badge { padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; }
|
||||
.status-running { background: #2196f3; color: white; }
|
||||
.status-completed { background: #4caf50; color: white; }
|
||||
.status-failed { background: #f44336; color: white; }
|
||||
.status-cancelled { background: #ff9800; color: white; }
|
||||
|
||||
.execution-logs { background: #0a0a0a; border: 1px solid #333; border-radius: 6px; padding: 16px; margin-top: 16px; max-height: 400px; overflow-y: auto; }
|
||||
.log-entry { font-family: monospace; font-size: 0.9em; margin-bottom: 8px; padding: 4px 8px; border-radius: 4px; }
|
||||
.log-info { color: #2196f3; }
|
||||
.log-warning { color: #ff9800; background: rgba(255, 152, 0, 0.1); }
|
||||
.log-error { color: #f44336; background: rgba(244, 67, 54, 0.1); }
|
||||
.log-success { color: #4caf50; background: rgba(76, 175, 80, 0.1); }
|
||||
`,
|
||||
];
|
||||
|
||||
private async triggerTask(taskName: string) {
|
||||
try {
|
||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Run Task: ${taskName}`,
|
||||
content: html`<div><p>Do you want to trigger this task now?</p></div>`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Run now',
|
||||
action: async (modalArg: any) => {
|
||||
await appstate.dataState.dispatchAction(appstate.taskActions.triggerTask, { taskName });
|
||||
plugins.deesCatalog.DeesToast.createAndShow({ message: `Task ${taskName} triggered`, type: 'success' });
|
||||
await modalArg.destroy();
|
||||
await this.loadExecutionsWithFilter();
|
||||
}
|
||||
},
|
||||
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() }
|
||||
]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to trigger task:', error);
|
||||
plugins.deesCatalog.DeesToast.createAndShow({ message: `Failed to trigger: ${error.message}`, type: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
private async cancelTaskFor(taskName: string) {
|
||||
try {
|
||||
const executions = (this.data.taskExecutions || [])
|
||||
.filter((e: any) => e.data.taskName === taskName && e.data.status === 'running')
|
||||
.sort((a: any, b: any) => (b.data.startedAt || 0) - (a.data.startedAt || 0));
|
||||
const running = executions[0];
|
||||
if (!running) return;
|
||||
|
||||
this.canceling = { ...this.canceling, [running.id]: true };
|
||||
try {
|
||||
await appstate.dataState.dispatchAction(appstate.taskActions.cancelTask, { executionId: running.id });
|
||||
plugins.deesCatalog.DeesToast.createAndShow({ message: `Cancelled ${taskName}`, type: 'success' });
|
||||
} finally {
|
||||
this.canceling = { ...this.canceling, [running.id]: false };
|
||||
await this.loadExecutionsWithFilter();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to cancel task:', err);
|
||||
plugins.deesCatalog.DeesToast.createAndShow({ message: `Cancel failed: ${err.message}`, type: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
private async loadExecutionsWithFilter() {
|
||||
try {
|
||||
const filter: any = {};
|
||||
if (this.filterStatus !== 'all') {
|
||||
filter.status = this.filterStatus;
|
||||
}
|
||||
await appstate.dataState.dispatchAction(appstate.taskActions.getTaskExecutions, { filter });
|
||||
} catch (error) {
|
||||
console.error('Failed to load executions:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async openExecutionDetails(execution: plugins.interfaces.data.ITaskExecution) {
|
||||
this.selectedExecution = execution;
|
||||
requestAnimationFrame(() => {
|
||||
this.shadowRoot?.querySelector('cloudly-sectionheading + cloudly-execution-details')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
});
|
||||
}
|
||||
|
||||
private async openLogsModal(execution: plugins.interfaces.data.ITaskExecution) {
|
||||
await plugins.deesCatalog.DeesModal.createAndShow({
|
||||
heading: `Logs: ${execution.data.taskName}`,
|
||||
content: html`
|
||||
<div class="execution-logs">
|
||||
${(execution.data.logs || []).map((log: any) => html`
|
||||
<div class="log-entry log-${log.severity}"><span>${formatDate(log.timestamp)}</span> - ${log.message}</div>
|
||||
`)}
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Copy All',
|
||||
action: async (modalArg: any) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText((execution.data.logs || [])
|
||||
.map((l: any) => `${new Date(l.timestamp).toISOString()} [${l.severity}] ${l.message}`).join('\n'));
|
||||
plugins.deesCatalog.DeesToast.createAndShow({ message: 'Logs copied', type: 'success' });
|
||||
} catch (e) {
|
||||
plugins.deesCatalog.DeesToast.createAndShow({ message: 'Copy failed', type: 'error' });
|
||||
}
|
||||
}
|
||||
},
|
||||
{ name: 'Close', action: async (modalArg: any) => modalArg.destroy() }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const tasks = (this.data.tasks || []) as any[];
|
||||
const categories = Array.from(new Set(tasks.map(t => t.category))).sort();
|
||||
const filteredTasks = tasks
|
||||
.filter(t => this.categoryFilter === 'all' || t.category === this.categoryFilter)
|
||||
.filter(t => !this.searchQuery || t.name.toLowerCase().includes(this.searchQuery.toLowerCase()) || (t.description || '').toLowerCase().includes(this.searchQuery.toLowerCase()));
|
||||
|
||||
return html`
|
||||
<cloudly-sectionheading>Tasks</cloudly-sectionheading>
|
||||
|
||||
<dees-panel .title=${'Task Library'} .subtitle=${'Run maintenance, monitoring and system tasks'} .variant=${'outline'}>
|
||||
<div class="toolbar">
|
||||
<div class="chipbar">
|
||||
<div class="chip ${this.categoryFilter === 'all' ? 'active' : ''}"
|
||||
@click=${() => { this.categoryFilter = 'all'; }}>
|
||||
All
|
||||
</div>
|
||||
${categories.map(cat => html`
|
||||
<div class="chip ${this.categoryFilter === cat ? 'active' : ''}"
|
||||
@click=${() => { this.categoryFilter = cat; }}>
|
||||
${cat}
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<input class="search-input" placeholder="Search tasks" .value=${this.searchQuery}
|
||||
@input=${(e: any) => { this.searchQuery = e.target.value; }} />
|
||||
<button class="secondary-button" @click=${async () => { await this.loadExecutionsWithFilter(); }}>Refresh</button>
|
||||
<button class="secondary-button" @click=${() => { this.autoRefresh = !this.autoRefresh; this.autoRefresh ? this.startAutoRefresh() : this.stopAutoRefresh(); }}>
|
||||
${this.autoRefresh ? 'Auto-Refresh: On' : 'Auto-Refresh: Off'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="task-list">
|
||||
${filteredTasks.map(task => html`
|
||||
<cloudly-task-panel
|
||||
.task=${task}
|
||||
.executions=${this.data.taskExecutions || []}
|
||||
.canceling=${this.canceling}
|
||||
.onRun=${(name: string) => this.triggerTask(name)}
|
||||
.onCancel=${(name: string) => this.cancelTaskFor(name)}
|
||||
.onOpenDetails=${(exec: any) => this.openExecutionDetails(exec)}
|
||||
.onOpenLogs=${(exec: any) => this.openLogsModal(exec)}
|
||||
></cloudly-task-panel>
|
||||
`)}
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<cloudly-sectionheading>Execution History</cloudly-sectionheading>
|
||||
|
||||
<dees-panel .title=${'Recent Executions'} .subtitle=${'History of task runs and their outcomes'} .variant=${'outline'}>
|
||||
<dees-table
|
||||
.heading1=${'Task Executions'}
|
||||
.heading2=${'History of task runs and their outcomes'}
|
||||
.data=${this.data.taskExecutions || []}
|
||||
.displayFunction=${(itemArg: plugins.interfaces.data.ITaskExecution) => {
|
||||
return {
|
||||
Task: itemArg.data.taskName,
|
||||
Status: html`<span class="status-badge status-${itemArg.data.status}">${itemArg.data.status}</span>`,
|
||||
'Started At': formatDate(itemArg.data.startedAt),
|
||||
Duration: itemArg.data.duration ? formatDuration(itemArg.data.duration) : '-',
|
||||
'Triggered By': itemArg.data.triggeredBy,
|
||||
Logs: itemArg.data.logs?.length || 0,
|
||||
} as any;
|
||||
}}
|
||||
.actionFunction=${async (itemArg: plugins.interfaces.data.ITaskExecution) => {
|
||||
const actions: any[] = [
|
||||
{ name: 'View Details', iconName: 'lucide:Eye', type: ['inRow'], actionFunc: async () => { this.selectedExecution = itemArg; } }
|
||||
];
|
||||
if (itemArg.data.status === 'running') {
|
||||
actions.push({ name: 'Cancel', iconName: 'lucide:SquareX', type: ['inRow'], actionFunc: async () => { await appstate.dataState.dispatchAction(appstate.taskActions.cancelTask, { executionId: itemArg.id }); await this.loadExecutionsWithFilter(); } });
|
||||
}
|
||||
return actions;
|
||||
}}
|
||||
></dees-table>
|
||||
</dees-panel>
|
||||
|
||||
${this.selectedExecution ? html`
|
||||
<cloudly-sectionheading>Execution Details</cloudly-sectionheading>
|
||||
<cloudly-execution-details .execution=${this.selectedExecution}></cloudly-execution-details>
|
||||
` : ''}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'cloudly-view-tasks': CloudlyViewTasks;
|
||||
}
|
||||
}
|
@@ -0,0 +1,93 @@
|
||||
import { DeesElement, customElement, html, css, cssManager, property } from '@design.estate/dees-element';
|
||||
import { formatDate, formatDuration } from '../utils.js';
|
||||
|
||||
@customElement('cloudly-execution-details')
|
||||
export class CloudlyExecutionDetails extends DeesElement {
|
||||
@property({ type: Object }) execution: any;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
.execution-details h3, .execution-details h4 { margin: 8px 0; }
|
||||
.metrics { display: flex; gap: 16px; margin-top: 12px; padding-top: 12px; border-top: 1px solid #333; }
|
||||
.metric { display: flex; flex-direction: column; }
|
||||
.metric-label { color: #666; font-size: 0.85em; }
|
||||
.metric-value { color: #fff; font-size: 1.1em; font-weight: 600; }
|
||||
.execution-logs { background: #0a0a0a; border: 1px solid #333; border-radius: 6px; padding: 16px; margin-top: 16px; max-height: 400px; overflow-y: auto; }
|
||||
.log-entry { font-family: monospace; font-size: 0.9em; margin-bottom: 8px; padding: 4px 8px; border-radius: 4px; }
|
||||
.log-info { color: #2196f3; }
|
||||
.log-warning { color: #ff9800; background: rgba(255, 152, 0, 0.1); }
|
||||
.log-error { color: #f44336; background: rgba(244, 67, 54, 0.1); }
|
||||
.log-success { color: #4caf50; background: rgba(76, 175, 80, 0.1); }
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
const execution = this.execution;
|
||||
if (!execution) return html``;
|
||||
return html`
|
||||
<div class="execution-details">
|
||||
<h3>Execution Details: ${execution.data.taskName}</h3>
|
||||
<div class="metrics">
|
||||
<div class="metric">
|
||||
<span class="metric-label">Started</span>
|
||||
<span class="metric-value">${formatDate(execution.data.startedAt)}</span>
|
||||
</div>
|
||||
${execution.data.completedAt ? html`
|
||||
<div class="metric">
|
||||
<span class="metric-label">Completed</span>
|
||||
<span class="metric-value">${formatDate(execution.data.completedAt)}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${execution.data.duration ? html`
|
||||
<div class="metric">
|
||||
<span class="metric-label">Duration</span>
|
||||
<span class="metric-value">${formatDuration(execution.data.duration)}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="metric">
|
||||
<span class="metric-label">Triggered By</span>
|
||||
<span class="metric-value">${execution.data.triggeredBy}</span>
|
||||
</div>
|
||||
</div>
|
||||
${execution.data.logs && execution.data.logs.length > 0 ? html`
|
||||
<h4>Logs</h4>
|
||||
<div class="execution-logs">
|
||||
${execution.data.logs.map((log: any) => html`
|
||||
<div class="log-entry log-${log.severity}">
|
||||
<span>${formatDate(log.timestamp)}</span> - ${log.message}
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
` : ''}
|
||||
${execution.data.metrics ? html`
|
||||
<h4>Metrics</h4>
|
||||
<div class="metrics">
|
||||
${Object.entries(execution.data.metrics).map(([key, value]) => html`
|
||||
<div class="metric">
|
||||
<span class="metric-label">${key}</span>
|
||||
<span class="metric-value">${typeof value === 'object' ? JSON.stringify(value) : value}</span>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
` : ''}
|
||||
${execution.data.error ? html`
|
||||
<h4>Error</h4>
|
||||
<div class="execution-logs">
|
||||
<div class="log-entry log-error">
|
||||
${execution.data.error.message}
|
||||
${execution.data.error.stack ? html`<pre>${execution.data.error.stack}</pre>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'cloudly-execution-details': CloudlyExecutionDetails;
|
||||
}
|
||||
}
|
||||
|
206
ts_web/elements/views/tasks/parts/cloudly-task-panel.ts
Normal file
206
ts_web/elements/views/tasks/parts/cloudly-task-panel.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import { DeesElement, customElement, html, css, cssManager, property } from '@design.estate/dees-element';
|
||||
import { formatCronFriendly, formatDuration, formatRelativeTime, getCategoryHue, getCategoryIcon } from '../utils.js';
|
||||
|
||||
@customElement('cloudly-task-panel')
|
||||
export class CloudlyTaskPanel extends DeesElement {
|
||||
@property({ type: Object }) task: any;
|
||||
@property({ type: Array }) executions: any[] = [];
|
||||
@property({ type: Object }) canceling: Record<string, boolean> = {};
|
||||
|
||||
// Callbacks provided by parent view
|
||||
@property({ attribute: false }) onRun?: (taskName: string) => void;
|
||||
@property({ attribute: false }) onCancel?: (taskName: string) => void;
|
||||
@property({ attribute: false }) onOpenDetails?: (execution: any) => void;
|
||||
@property({ attribute: false }) onOpenLogs?: (execution: any) => void;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
.task-panel {
|
||||
background: #131313;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
}
|
||||
.task-panel:hover { border-color: #3a3a3a; }
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.header-left { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
||||
.header-right { display: flex; align-items: center; gap: 8px; }
|
||||
.task-icon { color: #cfcfcf; font-size: 28px; }
|
||||
.task-name { font-size: 1.05em; font-weight: 650; color: #fff; letter-spacing: 0.1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.task-subtitle { color: #8c8c8c; font-size: 0.9em; }
|
||||
|
||||
.task-description {
|
||||
color: #b5b5b5;
|
||||
font-size: 0.95em;
|
||||
margin-bottom: 12px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
max-width: 760px;
|
||||
}
|
||||
.metric-item {
|
||||
background: #0f0f0f;
|
||||
border: 1px solid #2c2c2c;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
.metric-item .label { color: #8d8d8d; font-size: 0.8em; }
|
||||
.metric-item .value { color: #eaeaea; font-weight: 600; margin-top: 4px; }
|
||||
|
||||
.lastline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #a0a0a0;
|
||||
font-size: 0.9em;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
||||
.dot.info { background: #2196f3; }
|
||||
.dot.success { background: #4caf50; }
|
||||
.dot.warning { background: #ff9800; }
|
||||
.dot.error { background: #f44336; }
|
||||
|
||||
.panel-footer { display: flex; gap: 12px; margin-top: 12px; }
|
||||
|
||||
.link-button { background: transparent; border: none; color: #8ab4ff; cursor: pointer; padding: 0; font-size: 0.95em; }
|
||||
.link-button:hover { text-decoration: underline; }
|
||||
|
||||
.status-badge {
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
}
|
||||
.status-running { background: #2196f3; color: white; }
|
||||
.status-completed { background: #4caf50; color: white; }
|
||||
.status-failed { background: #f44336; color: white; }
|
||||
.status-cancelled { background: #ff9800; color: white; }
|
||||
`,
|
||||
];
|
||||
|
||||
private computeData() {
|
||||
const task = this.task || {};
|
||||
const executions = this.executions || [];
|
||||
const lastExecution = executions
|
||||
.filter((e: any) => e.data.taskName === task.name)
|
||||
.sort((a: any, b: any) => (b.data.startedAt || 0) - (a.data.startedAt || 0))[0];
|
||||
const isRunning = lastExecution?.data.status === 'running';
|
||||
const executionsForTask = executions.filter((e: any) => e.data.taskName === task.name);
|
||||
const now = Date.now();
|
||||
const last24hCount = executionsForTask.filter((e: any) => (e.data.startedAt || 0) > now - 86_400_000).length;
|
||||
const completed = executionsForTask.filter((e: any) => e.data.status === 'completed');
|
||||
const successRate = executionsForTask.length ? Math.round((completed.length * 100) / executionsForTask.length) : 0;
|
||||
const avgDuration = completed.length ? Math.round(completed.reduce((acc: number, e: any) => acc + (e.data.duration || 0), 0) / completed.length) : undefined;
|
||||
const lastLog = lastExecution?.data.logs && lastExecution.data.logs.length > 0 ? lastExecution.data.logs[lastExecution.data.logs.length - 1] : null;
|
||||
const subtitle = [
|
||||
task.category,
|
||||
task.schedule ? `⏱ ${formatCronFriendly(task.schedule)}` : null,
|
||||
isRunning
|
||||
? (lastExecution?.data.startedAt ? `Started ${formatRelativeTime(lastExecution.data.startedAt)}` : 'Running')
|
||||
: (task.lastRun ? `Last ${formatRelativeTime(task.lastRun)}` : 'Never run')
|
||||
].filter(Boolean).join(' • ');
|
||||
return { lastExecution, isRunning, last24hCount, successRate, avgDuration, lastLog, subtitle };
|
||||
}
|
||||
|
||||
public render() {
|
||||
const task = this.task;
|
||||
const { lastExecution, isRunning, last24hCount, successRate, avgDuration, lastLog, subtitle } = this.computeData();
|
||||
|
||||
return html`
|
||||
<div class="task-panel">
|
||||
<div class="panel-header">
|
||||
<div class="header-left">
|
||||
<dees-icon class="task-icon" .icon=${getCategoryIcon(task.category)}></dees-icon>
|
||||
<div>
|
||||
<div class="task-name" title=${task.name}>${task.name}</div>
|
||||
<div class="task-subtitle" title=${task.schedule || ''}>${subtitle}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
${lastExecution ? html`<span class="status-badge status-${lastExecution.data.status}">${lastExecution.data.status}</span>` : html`<span class="status-badge" style="background:#2e2e2e;color:#ddd;border:1px solid #3a3a3a;">idle</span>`}
|
||||
${isRunning ? html`
|
||||
<dees-spinner style="--size: 18px"></dees-spinner>
|
||||
<dees-button
|
||||
.text=${this.canceling[lastExecution!.id] ? 'Cancelling…' : 'Cancel'}
|
||||
.type=${'secondary'}
|
||||
.disabled=${!!this.canceling[lastExecution!.id]}
|
||||
@click=${() => this.onCancel?.(task.name)}
|
||||
></dees-button>
|
||||
` : html`
|
||||
<dees-button .text=${'Run'} .type=${'primary'} .disabled=${!task.enabled} @click=${() => this.onRun?.(task.name)}></dees-button>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-description" title=${task.description || ''}>${task.description}</div>
|
||||
|
||||
${lastExecution ? html`
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-item">
|
||||
<div class="label">Last Status</div>
|
||||
<div class="value">
|
||||
<span class="status-badge status-${lastExecution.data.status}">${lastExecution.data.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="label">Avg Duration</div>
|
||||
<div class="value">${avgDuration ? formatDuration(avgDuration) : '-'}</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="label">24h Runs · Success</div>
|
||||
<div class="value">${last24hCount} · ${successRate}%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lastline">
|
||||
${lastLog ? html`<span class="dot ${lastLog.severity}"></span> ${lastLog.message}` : 'No recent logs'}
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<button class="link-button" @click=${() => this.onOpenDetails?.(lastExecution)}>Details</button>
|
||||
${lastExecution.data.logs?.length ? html`<button class="link-button" @click=${() => this.onOpenLogs?.(lastExecution)}>Logs</button>` : ''}
|
||||
</div>
|
||||
` : html`
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-item">
|
||||
<div class="label">Last Status</div>
|
||||
<div class="value">—</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="label">Avg Duration</div>
|
||||
<div class="value">—</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="label">24h Runs · Success</div>
|
||||
<div class="value">0 · 0%</div>
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'cloudly-task-panel': CloudlyTaskPanel;
|
||||
}
|
||||
}
|
||||
|
68
ts_web/elements/views/tasks/utils.ts
Normal file
68
ts_web/elements/views/tasks/utils.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
export function formatDate(timestamp: number): string {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
}
|
||||
|
||||
export function formatDuration(ms: number): string {
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
||||
if (ms < 3600000) return `${(ms / 60000).toFixed(1)}m`;
|
||||
return `${(ms / 3600000).toFixed(1)}h`;
|
||||
}
|
||||
|
||||
export function formatRelativeTime(ts?: number): string {
|
||||
if (!ts) return '-';
|
||||
const diff = Date.now() - ts;
|
||||
const abs = Math.abs(diff);
|
||||
if (abs < 60_000) return `${Math.round(abs / 1000)}s ago`;
|
||||
if (abs < 3_600_000) return `${Math.round(abs / 60_000)}m ago`;
|
||||
if (abs < 86_400_000) return `${Math.round(abs / 3_600_000)}h ago`;
|
||||
return `${Math.round(abs / 86_400_000)}d ago`;
|
||||
}
|
||||
|
||||
export function getCategoryIcon(category: string): string {
|
||||
switch (category) {
|
||||
case 'maintenance':
|
||||
return 'lucide:Wrench';
|
||||
case 'deployment':
|
||||
return 'lucide:Rocket';
|
||||
case 'backup':
|
||||
return 'lucide:Archive';
|
||||
case 'monitoring':
|
||||
return 'lucide:Activity';
|
||||
case 'cleanup':
|
||||
return 'lucide:Trash2';
|
||||
case 'system':
|
||||
return 'lucide:Settings';
|
||||
case 'security':
|
||||
return 'lucide:Shield';
|
||||
default:
|
||||
return 'lucide:Play';
|
||||
}
|
||||
}
|
||||
|
||||
export function getCategoryHue(category: string): number {
|
||||
switch (category) {
|
||||
case 'maintenance': return 28; // orange
|
||||
case 'deployment': return 208; // blue
|
||||
case 'backup': return 122; // green
|
||||
case 'monitoring': return 280; // purple
|
||||
case 'cleanup': return 20; // brownish
|
||||
case 'system': return 200; // steel
|
||||
case 'security': return 0; // red
|
||||
default: return 210; // default blue
|
||||
}
|
||||
}
|
||||
|
||||
export function formatCronFriendly(cron?: string): string {
|
||||
if (!cron) return '';
|
||||
const parts = cron.trim().split(/\s+/);
|
||||
if (parts.length !== 5) return cron; // fallback
|
||||
const [min, hour, dom, mon, dow] = parts;
|
||||
if (min === '*/1' && hour === '*' && dom === '*' && mon === '*' && dow === '*') return 'every minute';
|
||||
if (min.startsWith('*/') && hour === '*' && dom === '*' && mon === '*' && dow === '*') return `every ${min.replace('*/','')} min`;
|
||||
if (min === '0' && hour.startsWith('*/') && dom === '*' && mon === '*' && dow === '*') return `every ${hour.replace('*/','')} hours`;
|
||||
if (min === '0' && hour === '*' && dom === '*' && mon === '*' && dow === '*') return 'hourly';
|
||||
if (min === '0' && hour === '0' && dom === '*' && mon === '*' && dow === '*') return 'daily';
|
||||
if (min === '0' && hour === '0' && dom === '1' && mon === '*' && dow === '*') return 'monthly';
|
||||
return cron;
|
||||
}
|
Reference in New Issue
Block a user