diff --git a/ts_web/elements/cloudly-dashboard.ts b/ts_web/elements/cloudly-dashboard.ts index c4e3313..9a235ac 100644 --- a/ts_web/elements/cloudly-dashboard.ts +++ b/ts_web/elements/cloudly-dashboard.ts @@ -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 { diff --git a/ts_web/elements/cloudly-view-backups.ts b/ts_web/elements/cloudly-view-backups.ts deleted file mode 100644 index 2b5e459..0000000 --- a/ts_web/elements/cloudly-view-backups.ts +++ /dev/null @@ -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` - Backups - { - 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` - - - - - - `, - 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` -
- Do you really want to delete the ConfigBundle? -
-
- ${actionDataArg.item.id} -
- `, - 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[]} - >
- `; - } -} diff --git a/ts_web/elements/cloudly-view-clusters.ts b/ts_web/elements/cloudly-view-clusters.ts deleted file mode 100644 index 5d9d368..0000000 --- a/ts_web/elements/cloudly-view-clusters.ts +++ /dev/null @@ -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` - Clusters - { - 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` - - - - - `, - 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` -
- Do you really want to delete the ConfigBundle? -
-
- ${actionDataArg.item.id} -
- `, - 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[]} - >
- `; - } -} diff --git a/ts_web/elements/cloudly-view-dbs.ts b/ts_web/elements/cloudly-view-dbs.ts deleted file mode 100644 index 7170651..0000000 --- a/ts_web/elements/cloudly-view-dbs.ts +++ /dev/null @@ -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` - DBs - { - 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` - - - - - - `, - 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` -
- Do you really want to delete the ConfigBundle? -
-
- ${actionDataArg.item.id} -
- `, - 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[]} - >
- `; - } -} diff --git a/ts_web/elements/cloudly-view-deployments.ts b/ts_web/elements/cloudly-view-deployments.ts deleted file mode 100644 index 6cf1cf5..0000000 --- a/ts_web/elements/cloudly-view-deployments.ts +++ /dev/null @@ -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`${status}`; - } - - private getHealthIndicatorHtml(health?: string): any { - if (!health) health = 'unknown'; - const className = `health-indicator health-${health}`; - const icon = health === 'healthy' ? '✓' : health === 'unhealthy' ? '✗' : '?'; - return html`${icon} ${health}`; - } - - private getResourceUsageHtml(deployment: plugins.interfaces.data.IDeployment): any { - if (!deployment.resourceUsage) { - return html`N/A`; - } - const { cpuUsagePercent, memoryUsedMB } = deployment.resourceUsage; - return html` -
-
- - ${cpuUsagePercent?.toFixed(1) || 0}% -
-
- - ${memoryUsedMB || 0} MB -
-
- `; - } - - public render() { - return html` - Deployments - { - 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`${itemArg.containerId.substring(0, 12)}` : - html`N/A`, - 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` -
- -
Please create a service first before creating deployments.
-
- `, - menuOptions: [ - { - name: 'OK', - action: async (modalArg) => { - await modalArg.destroy(); - }, - }, - ], - }); - return; - } - - const modal = await plugins.deesCatalog.DeesModal.createAndShow({ - heading: 'Deploy Service', - content: html` - - ({ key: s.id, value: s.data.name }))} - .required=${true}> - - - - - - - - - `, - 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` -
- Are you sure you want to restart this deployment? -
-
-
- ${this.getServiceName(deployment.serviceId)} -
-
- Node: ${this.getNodeName(deployment.nodeId)} -
-
- `, - 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` -
- Are you sure you want to delete this deployment? -
-
-
- ${this.getServiceName(deployment.serviceId)} -
-
- Node: ${this.getNodeName(deployment.nodeId)} -
-
- This action cannot be undone. -
-
- `, - 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[]} - >
- `; - } -} \ No newline at end of file diff --git a/ts_web/elements/cloudly-view-dns.ts b/ts_web/elements/cloudly-view-dns.ts deleted file mode 100644 index 04a8a56..0000000 --- a/ts_web/elements/cloudly-view-dns.ts +++ /dev/null @@ -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`${type}`; - } - - private getStatusBadge(active: boolean) { - return html` - ${active ? '✓ Active' : '✗ Inactive'} - `; - } - - public render() { - return html` - DNS Management - { - return { - Type: this.getRecordTypeBadge(itemArg.data.type), - Name: itemArg.data.name === '@' ? '' : 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` - - - - ({ - key: domain.id, - option: domain.data.name - })) || []} - .required=${true}> - - - - - - - - - - - - - - - - - - - `, - 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` - - - - ({ - key: domain.id, - option: domain.data.name - })) || []} - .value=${dnsEntry.data.domainId || ''} - .required=${true}> - - - - - - - - - - - - - - - - - - - `, - 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` -
- Are you sure you want to delete this DNS entry? -
-
-
- ${dnsEntry.data.type} - ${dnsEntry.data.name}.${dnsEntry.data.zone} -
-
- ${dnsEntry.data.value} -
- ${dnsEntry.data.description ? html` -
- ${dnsEntry.data.description} -
- ` : ''} -
- `, - 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[]} - >
- `; - } -} \ No newline at end of file diff --git a/ts_web/elements/cloudly-view-domains.ts b/ts_web/elements/cloudly-view-domains.ts deleted file mode 100644 index ee0629e..0000000 --- a/ts_web/elements/cloudly-view-domains.ts +++ /dev/null @@ -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`${status.toUpperCase()}`; - } - - private getVerificationBadge(status: string) { - const displayText = status === 'not_required' ? 'Not Required' : status.replace('_', ' ').toUpperCase(); - return html`${displayText}`; - } - - private getSslBadge(sslStatus?: string) { - if (!sslStatus) return html``; - const icon = sslStatus === 'active' ? '🔒' : sslStatus === 'expired' ? '⚠️' : '🔓'; - return html`${icon} ${sslStatus.toUpperCase()}`; - } - - 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`Expired ${Math.abs(days)} days ago`; - } else if (days <= 30) { - return html`Expires in ${days} days`; - } else { - return `${days} days`; - } - } - - public render() { - return html` - Domain Management - { - const dnsCount = this.data.dnsEntries?.filter(dns => dns.data.zone === itemArg.data.name).length || 0; - return { - Domain: html` -
-
${itemArg.data.name}
- ${itemArg.data.description ? html`
${itemArg.data.description}
` : ''} -
- `, - 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`
${itemArg.data.nameservers?.join(', ') || '—'}
`, - }; - }} - .dataActions=${[ - { - name: 'Add Domain', - iconName: 'plus', - type: ['header', 'footer'], - actionFunc: async (dataActionArg) => { - const modal = await plugins.deesCatalog.DeesModal.createAndShow({ - heading: 'Add Domain', - content: html` - - - - - - - - - - - - - - - - - - - - - - - - - `, - 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` - - - - - - - - - - - - - - - - - - - - - - - - - `, - 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` -
-

Choose a verification method for ${domain.data.name}

- - - - - ${domain.data.verificationToken ? html` -
-
Verification Token:
- ${domain.data.verificationToken} -
- ` : ''} -
- `, - 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` -
- Are you sure you want to delete this domain? -
-
-
- ${domain.data.name} -
- ${domain.data.description ? html` -
- ${domain.data.description} -
- ` : ''} - ${dnsCount > 0 ? html` -
- ⚠️ This domain has ${dnsCount} DNS record${dnsCount > 1 ? 's' : ''} that will also be deleted -
- ` : ''} -
- `, - 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[]} - >
- `; - } -} \ No newline at end of file diff --git a/ts_web/elements/cloudly-view-externalregistries.ts b/ts_web/elements/cloudly-view-externalregistries.ts deleted file mode 100644 index 3203aeb..0000000 --- a/ts_web/elements/cloudly-view-externalregistries.ts +++ /dev/null @@ -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` - External Registries - { - return { - Name: html`${registry.data.name}${registry.data.isDefault ? html`DEFAULT` : ''}`, - Type: html`${registry.data.type.toUpperCase()}`, - URL: registry.data.url, - Auth: registry.data.authType === 'none' ? 'Public' : (registry.data.username || 'Token Auth'), - Namespace: registry.data.namespace || '-', - Status: html`${(registry.data.status || 'unverified').toUpperCase()}`, - '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` - - - - - - - - - - - - - - - - - - - - - - - `, - 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` - - - - - - - - - - - - - - - - - - - - - - - `, - 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` -
- -

Testing connection to ${registry.data.name}...

-
- `, - 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` -
- ${updatedRegistry?.data.status === 'active' ? html` -
-

Connection successful!

- ` : html` -
-

Connection failed!

- ${updatedRegistry?.data.lastError ? html` -

- Error: ${updatedRegistry.data.lastError} -

- ` : ''} - `} -
- `, - 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` -
-

Do you really want to delete this external registry?

-

- This will remove all stored credentials and configuration. -

-
-
- ${registry.data.name} (${registry.data.url}) -
- `, - 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[]} - >
- `; - } -} \ No newline at end of file diff --git a/ts_web/elements/cloudly-view-images.ts b/ts_web/elements/cloudly-view-images.ts deleted file mode 100644 index df284ef..0000000 --- a/ts_web/elements/cloudly-view-images.ts +++ /dev/null @@ -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` - Images - { - 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` - - - - - `, - 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 - ) => { - 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` - - - - - - { - 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[]} - > - - `, - 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 - ) => { - 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` - - ) => { - console.log('delete', itemArg); - }, - }, - ] as plugins.deesCatalog.ITableAction[]} - > - `, - menuOptions: [ - { - name: 'close', - action: async (modalArg) => { - await modalArg.destroy(); - }, - }, - ], - }); - }, - }, - { - name: 'delete', - iconName: 'trash', - type: ['contextmenu', 'inRow'], - actionFunc: async ( - itemArg: plugins.deesCatalog.ITableActionDataArg - ) => { - plugins.deesCatalog.DeesModal.createAndShow({ - heading: `Delete Image "${itemArg.item.data.name}"`, - content: html` -
Do you really want to delete the image?
-
- ${itemArg.item.id} -
- `, - 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[]} - >
- `; - } -} diff --git a/ts_web/elements/cloudly-view-logs.ts b/ts_web/elements/cloudly-view-logs.ts deleted file mode 100644 index ca3edc3..0000000 --- a/ts_web/elements/cloudly-view-logs.ts +++ /dev/null @@ -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` - Logs - { - 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` - - - - - - `, - 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` -
- Do you really want to delete the ConfigBundle? -
-
- ${actionDataArg.item.id} -
- `, - 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[]} - >
- `; - } -} diff --git a/ts_web/elements/cloudly-view-mails.ts b/ts_web/elements/cloudly-view-mails.ts deleted file mode 100644 index e336145..0000000 --- a/ts_web/elements/cloudly-view-mails.ts +++ /dev/null @@ -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` - Mails - { - 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` - - - - - - `, - 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` -
- Do you really want to delete the ConfigBundle? -
-
- ${actionDataArg.item.id} -
- `, - 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[]} - >
- `; - } -} diff --git a/ts_web/elements/cloudly-view-overview.ts b/ts_web/elements/cloudly-view-overview.ts deleted file mode 100644 index c9007b6..0000000 --- a/ts_web/elements/cloudly-view-overview.ts +++ /dev/null @@ -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` - Overview - - `; - } -} diff --git a/ts_web/elements/cloudly-view-s3.ts b/ts_web/elements/cloudly-view-s3.ts deleted file mode 100644 index 335d3be..0000000 --- a/ts_web/elements/cloudly-view-s3.ts +++ /dev/null @@ -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` - S3 - { - 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` - - - - - - `, - 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` -
- Do you really want to delete the ConfigBundle? -
-
- ${actionDataArg.item.id} -
- `, - 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[]} - >
- `; - } -} diff --git a/ts_web/elements/cloudly-view-secretbundles.ts b/ts_web/elements/cloudly-view-secretbundles.ts deleted file mode 100644 index c9356a3..0000000 --- a/ts_web/elements/cloudly-view-secretbundles.ts +++ /dev/null @@ -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` - SecretBundles - { - 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``, - }; - }} - .dataActions=${[ - { - name: 'add SecretBundle', - iconName: 'plus', - type: ['header', 'footer'], - actionFunc: async (dataActionArg) => { - const modal = await plugins.deesCatalog.DeesModal.createAndShow({ - heading: 'Add SecretBundle', - content: html` - - - - - - `, - 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` -
- Do you really want to delete the ConfigBundle? -
-
- ${actionDataArg.item.id} -
- `, - 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` - - - - `, - menuOptions: [ - { name: 'save', action: async (modalArg) => {} }, - { - name: 'cancel', - action: async (modalArg) => { - modalArg.destroy(); - }, - }, - ], - }); - }, - }, - ] as plugins.deesCatalog.ITableAction[]} - >
- `; - } -} diff --git a/ts_web/elements/cloudly-view-secretgroups.ts b/ts_web/elements/cloudly-view-secretgroups.ts deleted file mode 100644 index 08bd43c..0000000 --- a/ts_web/elements/cloudly-view-secretgroups.ts +++ /dev/null @@ -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` - SecretGroups - { - return { - name: secretGroup.data.name, - priority: secretGroup.data.priority, - tags: html``, - 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` - - - - - { - 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']} - > - - `, - 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 - ) => { - 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` - - - - - - { - 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[]} - > - - `, - 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 - ) => { - 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` - - ) => { - console.log('delete', itemArg); - }, - }, - ] as plugins.deesCatalog.ITableAction[]} - > - `, - menuOptions: [ - { - name: 'close', - action: async (modalArg) => { - await modalArg.destroy(); - }, - }, - ], - }); - }, - }, - { - name: 'delete', - iconName: 'trash', - type: ['contextmenu', 'inRow'], - actionFunc: async ( - itemArg: plugins.deesCatalog.ITableActionDataArg - ) => { - plugins.deesCatalog.DeesModal.createAndShow({ - heading: `Delete ${itemArg.item.data.key}`, - content: html` -
Do you really want to delete the secret?
-
- ${itemArg.item.data.key} -
- `, - 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[]} - >
- `; - } -} diff --git a/ts_web/elements/cloudly-view-settings.ts b/ts_web/elements/cloudly-view-settings.ts deleted file mode 100644 index ce2308f..0000000 --- a/ts_web/elements/cloudly-view-settings.ts +++ /dev/null @@ -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 = {}; - - // 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` - - `; - } - - public render() { - if (this.isLoading && Object.keys(this.settings).length === 0) { - return html` -
- -
- `; - } - - return html` - Settings -
- { - 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); - }}> - - - -
- ${this.renderProviderStatus('hetzner')} - { - e.preventDefault(); - e.stopPropagation(); - this.testConnection('hetzner'); - }} - > -
-
- -
-
- - - -
- ${this.renderProviderStatus('cloudflare')} - { - e.preventDefault(); - e.stopPropagation(); - this.testConnection('cloudflare'); - }} - > -
-
- -
-
- - - -
- ${this.renderProviderStatus('aws')} - { - e.preventDefault(); - e.stopPropagation(); - this.testConnection('aws'); - }} - > -
-
- - -
-
- -
-
- - - -
- ${this.renderProviderStatus('digitalocean')} - { - e.preventDefault(); - e.stopPropagation(); - this.testConnection('digitalocean'); - }} - > -
-
- -
-
- - - -
- ${this.renderProviderStatus('azure')} - { - e.preventDefault(); - e.stopPropagation(); - this.testConnection('azure'); - }} - > -
-
- - -
-
- - -
-
- - - -
- ${this.renderProviderStatus('google')} - { - e.preventDefault(); - e.stopPropagation(); - this.testConnection('google'); - }} - > -
-
- -
-
- -
-
- -
- -
-
-
- `; - } -} diff --git a/ts_web/elements/cloudly-view-tasks.ts b/ts_web/elements/cloudly-view-tasks.ts deleted file mode 100644 index 2da6146..0000000 --- a/ts_web/elements/cloudly-view-tasks.ts +++ /dev/null @@ -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 = {}; - - 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 { - 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` -
-

Do you want to trigger this task now?

-
- `, - 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` -
-
-
- -
-
${task.name}
-
${subtitle}
-
-
-
- ${lastExecution ? html`${lastExecution.data.status}` : html`idle`} - ${isRunning ? html` - - this.cancelTaskFor(task.name)} - > - ` : html` - this.triggerTask(task.name)}> - `} -
-
- -
${task.description}
- - ${lastExecution ? html` -
-
-
Last Status
-
- ${lastExecution.data.status} -
-
-
-
Avg Duration
-
${avgDuration ? this.formatDuration(avgDuration) : '-'}
-
-
-
24h Runs · Success
-
${last24hCount} · ${successRate}%
-
-
-
- ${lastLog ? html` ${lastLog.message}` : 'No recent logs'} -
- - ` : html` -
-
-
Last Status
-
-
-
-
Avg Duration
-
-
-
-
24h Runs · Success
-
0 · 0%
-
-
- `} -
- `; - } - - 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` -
- ${(execution.data.logs || []).map(log => html` -
- ${this.formatDate(log.timestamp)} - ${log.message} -
- `)} -
- `, - 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` -
-

Execution Details: ${execution.data.taskName}

- -
-
- Started - ${this.formatDate(execution.data.startedAt)} -
- ${execution.data.completedAt ? html` -
- Completed - ${this.formatDate(execution.data.completedAt)} -
- ` : ''} - ${execution.data.duration ? html` -
- Duration - ${this.formatDuration(execution.data.duration)} -
- ` : ''} -
- Triggered By - ${execution.data.triggeredBy} -
-
- - ${execution.data.logs && execution.data.logs.length > 0 ? html` -

Logs

-
- ${execution.data.logs.map(log => html` -
- ${this.formatDate(log.timestamp)} - - ${log.message} -
- `)} -
- ` : ''} - - ${execution.data.metrics ? html` -

Metrics

-
- ${Object.entries(execution.data.metrics).map(([key, value]) => html` -
- ${key} - ${typeof value === 'object' ? JSON.stringify(value) : value} -
- `)} -
- ` : ''} - - ${execution.data.error ? html` -

Error

-
-
- ${execution.data.error.message} - ${execution.data.error.stack ? html`
${execution.data.error.stack}
` : ''} -
-
- ` : ''} -
- `; - } - - 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` - Tasks - - -
-
-
{ this.categoryFilter = 'all'; }}> - All -
- ${categories.map(cat => html` -
{ this.categoryFilter = cat; }}> - ${cat} -
- `)} -
-
- { this.searchQuery = e.target.value; }} /> - - -
- -
- ${filteredTasks.map(task => this.renderTaskCard(task))} -
-
- - Execution History - - - { - return { - Task: itemArg.data.taskName, - Status: html`${itemArg.data.status}`, - '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; - }} - > - - - ${this.selectedExecution ? html` - Execution Details - ${this.renderExecutionDetails(this.selectedExecution)} - ` : ''} - `; - } -} diff --git a/ts_web/elements/index.ts b/ts_web/elements/index.ts index 6b21cbb..71aeb9e 100644 --- a/ts_web/elements/index.ts +++ b/ts_web/elements/index.ts @@ -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'; \ No newline at end of file +export * from './views/secretgroups/index.js'; +export * from './views/secretbundles/index.js'; diff --git a/ts_web/elements/views/backups/index.ts b/ts_web/elements/views/backups/index.ts new file mode 100644 index 0000000..fed5293 --- /dev/null +++ b/ts_web/elements/views/backups/index.ts @@ -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` + Backups + { 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` + + + + + + `, 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` +
Do you really want to delete the ConfigBundle?
+
${actionDataArg.item.id}
+ `, 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[]} + >
+ `; + } +} + +declare global { interface HTMLElementTagNameMap { 'cloudly-view-backups': CloudlyViewBackups; } } + diff --git a/ts_web/elements/views/clusters/index.ts b/ts_web/elements/views/clusters/index.ts new file mode 100644 index 0000000..0d82566 --- /dev/null +++ b/ts_web/elements/views/clusters/index.ts @@ -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` + Clusters + { + 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` + + + + + + `, + 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` +
Do you really want to delete the ConfigBundle?
+
${actionDataArg.item.id}
+ `, + 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[]} + >
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'cloudly-view-clusters': CloudlyViewClusters; + } +} + diff --git a/ts_web/elements/views/dbs/index.ts b/ts_web/elements/views/dbs/index.ts new file mode 100644 index 0000000..04708a6 --- /dev/null +++ b/ts_web/elements/views/dbs/index.ts @@ -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` + DBs + { 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` + + + + + + `, 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` +
Do you really want to delete the ConfigBundle?
+
${actionDataArg.item.id}
+ `, 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[]} + >
+ `; + } +} + +declare global { interface HTMLElementTagNameMap { 'cloudly-view-dbs': CloudlyViewDbs; } } + diff --git a/ts_web/elements/views/deployments/index.ts b/ts_web/elements/views/deployments/index.ts new file mode 100644 index 0000000..25b8e6e --- /dev/null +++ b/ts_web/elements/views/deployments/index.ts @@ -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`${status}`; + } + + private getHealthIndicatorHtml(health?: string): any { + if (!health) health = 'unknown'; + const className = `health-indicator health-${health}`; + const icon = health === 'healthy' ? '✓' : health === 'unhealthy' ? '✗' : '?'; + return html`${icon} ${health}`; + } + + private getResourceUsageHtml(deployment: plugins.interfaces.data.IDeployment): any { + if (!deployment.resourceUsage) { + return html`N/A`; + } + const { cpuUsagePercent, memoryUsedMB } = deployment.resourceUsage; + return html` +
+
+ + ${cpuUsagePercent?.toFixed(1) || 0}% +
+
+ + ${memoryUsedMB || 0} MB +
+
+ `; + } + + public render() { + return html` + Deployments + { + 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`${itemArg.containerId.substring(0, 12)}` : html`N/A`, + 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`
Please create a service first before creating deployments.
`, + menuOptions: [ { name: 'OK', action: async (modalArg: any) => { await modalArg.destroy(); } } ], + }); + return; + } + await plugins.deesCatalog.DeesModal.createAndShow({ + heading: 'Deploy Service', + content: html` + + ({ key: s.id, value: s.data.name }))} .required=${true}> + + + + + `, + 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` +
Are you sure you want to restart this deployment?
+
+
${this.getServiceName(deployment.serviceId)}
+
Node: ${this.getNodeName(deployment.nodeId)}
+
+ `, + 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` +
Are you sure you want to delete this deployment?
+
+
${this.getServiceName(deployment.serviceId)}
+
Node: ${this.getNodeName(deployment.nodeId)}
+
This action cannot be undone.
+
+ `, + 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[]} + >
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'cloudly-view-deployments': CloudlyViewDeployments; + } +} + diff --git a/ts_web/elements/views/dns/index.ts b/ts_web/elements/views/dns/index.ts new file mode 100644 index 0000000..a11da3b --- /dev/null +++ b/ts_web/elements/views/dns/index.ts @@ -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`${type}`; } + private getStatusBadge(active: boolean) { return html`${active ? '✓ Active' : '✗ Inactive'}`; } + + public render() { + return html` + DNS Management + { + return { + Type: this.getRecordTypeBadge(itemArg.data.type), + Name: itemArg.data.name === '@' ? '' : 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` + + + ({ key: domain.id, option: domain.data.name })) || []} .required=${true}> + + + + + + + + + + `, + 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` + + + ({ key: domain.id, option: domain.data.name })) || []} .value=${dnsEntry.data.domainId || ''} .required=${true}> + + + + + + + + + + `, + 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`
Are you sure you want to delete this DNS entry?
${dnsEntry.data.type} - ${dnsEntry.data.name}.${dnsEntry.data.zone}
${dnsEntry.data.value}
${dnsEntry.data.description ? html`
${dnsEntry.data.description}
` : ''}
`, 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[]} + >
+ `; + } +} + +declare global { interface HTMLElementTagNameMap { 'cloudly-view-dns': CloudlyViewDns; } } + diff --git a/ts_web/elements/views/domains/index.ts b/ts_web/elements/views/domains/index.ts new file mode 100644 index 0000000..547b961 --- /dev/null +++ b/ts_web/elements/views/domains/index.ts @@ -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`${status.toUpperCase()}`; } + private getVerificationBadge(status: string) { const displayText = status === 'not_required' ? 'Not Required' : status.replace('_', ' ').toUpperCase(); return html`${displayText}`; } + private getSslBadge(sslStatus?: string) { if (!sslStatus) return html``; const icon = sslStatus === 'active' ? '🔒' : sslStatus === 'expired' ? '⚠️' : '🔓'; return html`${icon} ${sslStatus.toUpperCase()}`; } + 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`Expired ${Math.abs(days)} days ago`; } else if (days <= 30) { return html`Expires in ${days} days`; } else { return `${days} days`; } } + + public render() { + return html` + Domain Management + { + const dnsCount = this.data.dnsEntries?.filter(dns => dns.data.zone === itemArg.data.name).length || 0; + return { + Domain: html`
${itemArg.data.name}
${itemArg.data.description ? html`
${itemArg.data.description}
` : ''}
`, + 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`
${itemArg.data.nameservers?.join(', ') || '—'}
`, + }; + }} + .dataActions=${[ + { name: 'Add Domain', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => { + const modal = await plugins.deesCatalog.DeesModal.createAndShow({ + heading: 'Add Domain', + content: html` + + + + + + + + + + + + + + `, + 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` + + + + + + + + + `, + 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` +
+

Choose a verification method for ${domain.data.name}

+ + + + ${domain.data.verificationToken ? html`
Verification Token:
${domain.data.verificationToken}
` : ''} +
+ `, + 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` +
Are you sure you want to delete this domain?
+
+
${domain.data.name}
+ ${domain.data.description ? html`
${domain.data.description}
` : ''} + ${dnsCount > 0 ? html`
⚠️ This domain has ${dnsCount} DNS record${dnsCount > 1 ? 's' : ''} that will also be deleted
` : ''} +
+ `, + 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[]} + >
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { 'cloudly-view-domains': CloudlyViewDomains; } +} + diff --git a/ts_web/elements/views/externalregistries/index.ts b/ts_web/elements/views/externalregistries/index.ts new file mode 100644 index 0000000..0706332 --- /dev/null +++ b/ts_web/elements/views/externalregistries/index.ts @@ -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` + External Registries + { + return { + Name: html`${registry.data.name}${registry.data.isDefault ? html`DEFAULT` : ''}`, + Type: html`${registry.data.type.toUpperCase()}`, + URL: registry.data.url, + Auth: registry.data.authType === 'none' ? 'Public' : (registry.data.username || 'Token Auth'), + Namespace: registry.data.namespace || '-', + Status: html`${(registry.data.status || 'unverified').toUpperCase()}`, + '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` + + + + + + + + + + + + + `, 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` + + + + + + + + + + + + + `, 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`

Testing connection to ${registry.data.name}...

`, 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`
${updatedRegistry?.data.status === 'active' ? html`

Connection successful!

` : html`

Connection failed!

${updatedRegistry?.data.lastError ? html`

Error: ${updatedRegistry.data.lastError}

` : ''}`}
`, 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`

Do you really want to delete this external registry?

This will remove all stored credentials and configuration.

${registry.data.name} (${registry.data.url})
`, 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[]} + >
+ `; + } +} + +declare global { interface HTMLElementTagNameMap { 'cloudly-view-externalregistries': CloudlyViewExternalRegistries; } } + diff --git a/ts_web/elements/views/images/index.ts b/ts_web/elements/views/images/index.ts new file mode 100644 index 0000000..38099f8 --- /dev/null +++ b/ts_web/elements/views/images/index.ts @@ -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` + Images + { + 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` + + + + + `, + 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) => { + const environmentsArray: Array = []; + 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` + + + + + + ({ 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[]}> + + + `, + 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) => { + 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`) => { console.log('delete', itemArg); }, }] as plugins.deesCatalog.ITableAction[]}>`, + menuOptions: [ { name: 'close', action: async (modalArg: any) => { await modalArg.destroy(); } } ], + }); + }, + }, + { + name: 'delete', + iconName: 'trash', + type: ['contextmenu', 'inRow'], + actionFunc: async (itemArg: plugins.deesCatalog.ITableActionDataArg) => { + plugins.deesCatalog.DeesModal.createAndShow({ + heading: `Delete Image "${itemArg.item.data.name}"`, + content: html` +
Do you really want to delete the image?
+
${itemArg.item.id}
+ `, + 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[]} + >
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'cloudly-view-images': CloudlyViewImages; + } +} + diff --git a/ts_web/elements/views/logs/index.ts b/ts_web/elements/views/logs/index.ts new file mode 100644 index 0000000..4c661c8 --- /dev/null +++ b/ts_web/elements/views/logs/index.ts @@ -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` + Logs + { + 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` + + + + + + `, 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` +
Do you really want to delete the ConfigBundle?
+
${actionDataArg.item.id}
+ `, 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[]} + >
+ `; + } +} + +declare global { interface HTMLElementTagNameMap { 'cloudly-view-logs': CloudlyViewLogs; } } + diff --git a/ts_web/elements/views/mails/index.ts b/ts_web/elements/views/mails/index.ts new file mode 100644 index 0000000..a6a0af3 --- /dev/null +++ b/ts_web/elements/views/mails/index.ts @@ -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` + Mails + { + 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` + + + + + + `, 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` +
Do you really want to delete the ConfigBundle?
+
${actionDataArg.item.id}
+ `, 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[]} + >
+ `; + } +} + +declare global { interface HTMLElementTagNameMap { 'cloudly-view-mails': CloudlyViewMails; } } + diff --git a/ts_web/elements/views/overview/index.ts b/ts_web/elements/views/overview/index.ts new file mode 100644 index 0000000..351546c --- /dev/null +++ b/ts_web/elements/views/overview/index.ts @@ -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` + Overview + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'cloudly-view-overview': CloudlyViewOverview; + } +} + diff --git a/ts_web/elements/views/s3/index.ts b/ts_web/elements/views/s3/index.ts new file mode 100644 index 0000000..59d2a3b --- /dev/null +++ b/ts_web/elements/views/s3/index.ts @@ -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` + S3 + { 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` + + + + + + `, 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` +
Do you really want to delete the ConfigBundle?
+
${actionDataArg.item.id}
+ `, 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[]} + >
+ `; + } +} + +declare global { interface HTMLElementTagNameMap { 'cloudly-view-s3': CloudlyViewS3; } } + diff --git a/ts_web/elements/views/secretbundles/index.ts b/ts_web/elements/views/secretbundles/index.ts new file mode 100644 index 0000000..cf22700 --- /dev/null +++ b/ts_web/elements/views/secretbundles/index.ts @@ -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` + SecretBundles + { + 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``, + }; + }} + .dataActions=${[ + { name: 'add SecretBundle', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => { + await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add SecretBundle', content: html` + + + + + + `, 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` +
Do you really want to delete the ConfigBundle?
+
${actionDataArg.item.id}
+ `, 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``, menuOptions: [ { name: 'save', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] }); + } }, + ] as plugins.deesCatalog.ITableAction[]} + >
+ `; + } +} + +declare global { interface HTMLElementTagNameMap { 'cloudly-view-secretbundles': CloudlyViewSecretBundles; } } + diff --git a/ts_web/elements/views/secretgroups/index.ts b/ts_web/elements/views/secretgroups/index.ts new file mode 100644 index 0000000..8379dd2 --- /dev/null +++ b/ts_web/elements/views/secretgroups/index.ts @@ -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` + SecretGroups + { + return { + name: secretGroup.data.name, + priority: secretGroup.data.priority, + tags: html``, + 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` + + + + + { 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']}> + + + `, 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) => { + const environmentsArray: Array = []; + 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` + + + + + + ({ 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[]}> + + + `, 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) => { + 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`) => { console.log('delete', itemArg); }, }] as plugins.deesCatalog.ITableAction[]}>`, menuOptions: [ { name: 'close', action: async (modalArg: any) => { await modalArg.destroy(); } } ] }); + } }, + { name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (itemArg: plugins.deesCatalog.ITableActionDataArg) => { + plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ${itemArg.item.data.key}`, content: html`
Do you really want to delete the secret?
${itemArg.item.data.key}
`, 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[]} + >
+ `; + } +} + +declare global { interface HTMLElementTagNameMap { 'cloudly-view-secretsgroups': CloudlyViewSecretGroups; } } + diff --git a/ts_web/elements/cloudly-view-services.ts b/ts_web/elements/views/services/index.ts similarity index 50% rename from ts_web/elements/cloudly-view-services.ts rename to ts_web/elements/views/services/index.ts index 97ac321..338beab 100644 --- a/ts_web/elements/cloudly-view-services.ts +++ b/ts_web/elements/views/services/index.ts @@ -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` - - - - - - - - + + + + - - - - - - + + + `, 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 { - - - - - - - - + + + + - - - - + + `, 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` -
- Are you sure you want to delete this service? -
+
Are you sure you want to delete this service?
${service.data.name}
${service.data.description}
-
- This will also delete ${service.data.deploymentIds?.length || 0} deployment(s) -
+
This will also delete ${service.data.deploymentIds?.length || 0} deployment(s)
`, 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(); } }, ], }); }, @@ -352,4 +215,11 @@ export class CloudlyViewServices extends DeesElement { > `; } -} \ No newline at end of file +} + +declare global { + interface HTMLElementTagNameMap { + 'cloudly-view-services': CloudlyViewServices; + } +} + diff --git a/ts_web/elements/views/settings/index.ts b/ts_web/elements/views/settings/index.ts new file mode 100644 index 0000000..29a4ec1 --- /dev/null +++ b/ts_web/elements/views/settings/index.ts @@ -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 = {}; + 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``; + } + + public render() { + if (this.isLoading && Object.keys(this.settings).length === 0) { + return html`
`; + } + return html` + Settings +
+ { this.saveSettings((e.detail as any).data); }}> + +
+ ${this.renderProviderStatus('hetzner')} + { e.preventDefault(); e.stopPropagation(); this.testConnection('hetzner'); }}> +
+
+ +
+
+ + +
+ ${this.renderProviderStatus('cloudflare')} + { e.preventDefault(); e.stopPropagation(); this.testConnection('cloudflare'); }}> +
+
+ +
+
+ + +
+ ${this.renderProviderStatus('aws')} + { e.preventDefault(); e.stopPropagation(); this.testConnection('aws'); }}> +
+
+ + +
+
+ +
+
+ + +
+ ${this.renderProviderStatus('digitalocean')} + { e.preventDefault(); e.stopPropagation(); this.testConnection('digitalocean'); }}> +
+
+ +
+
+ + +
+ ${this.renderProviderStatus('azure')} + { e.preventDefault(); e.stopPropagation(); this.testConnection('azure'); }}> +
+
+ + +
+
+ + +
+
+ + +
+ ${this.renderProviderStatus('google')} + { e.preventDefault(); e.stopPropagation(); this.testConnection('google'); }}> +
+
+ +
+
+ +
+
+ +
+ +
+
+
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'cloudly-view-settings': CloudlyViewSettings; + } +} + diff --git a/ts_web/elements/views/tasks/index.ts b/ts_web/elements/views/tasks/index.ts new file mode 100644 index 0000000..16ad803 --- /dev/null +++ b/ts_web/elements/views/tasks/index.ts @@ -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 = {}; + + 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 { + 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`

Do you want to trigger this task now?

`, + 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` +
+ ${(execution.data.logs || []).map((log: any) => html` +
${formatDate(log.timestamp)} - ${log.message}
+ `)} +
+ `, + 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` + Tasks + + +
+
+
{ this.categoryFilter = 'all'; }}> + All +
+ ${categories.map(cat => html` +
{ this.categoryFilter = cat; }}> + ${cat} +
+ `)} +
+
+ { this.searchQuery = e.target.value; }} /> + + +
+ +
+ ${filteredTasks.map(task => html` + this.triggerTask(name)} + .onCancel=${(name: string) => this.cancelTaskFor(name)} + .onOpenDetails=${(exec: any) => this.openExecutionDetails(exec)} + .onOpenLogs=${(exec: any) => this.openLogsModal(exec)} + > + `)} +
+
+ + Execution History + + + { + return { + Task: itemArg.data.taskName, + Status: html`${itemArg.data.status}`, + '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; + }} + > + + + ${this.selectedExecution ? html` + Execution Details + + ` : ''} + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'cloudly-view-tasks': CloudlyViewTasks; + } +} diff --git a/ts_web/elements/views/tasks/parts/cloudly-execution-details.ts b/ts_web/elements/views/tasks/parts/cloudly-execution-details.ts new file mode 100644 index 0000000..68a5185 --- /dev/null +++ b/ts_web/elements/views/tasks/parts/cloudly-execution-details.ts @@ -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` +
+

Execution Details: ${execution.data.taskName}

+
+
+ Started + ${formatDate(execution.data.startedAt)} +
+ ${execution.data.completedAt ? html` +
+ Completed + ${formatDate(execution.data.completedAt)} +
+ ` : ''} + ${execution.data.duration ? html` +
+ Duration + ${formatDuration(execution.data.duration)} +
+ ` : ''} +
+ Triggered By + ${execution.data.triggeredBy} +
+
+ ${execution.data.logs && execution.data.logs.length > 0 ? html` +

Logs

+
+ ${execution.data.logs.map((log: any) => html` +
+ ${formatDate(log.timestamp)} - ${log.message} +
+ `)} +
+ ` : ''} + ${execution.data.metrics ? html` +

Metrics

+
+ ${Object.entries(execution.data.metrics).map(([key, value]) => html` +
+ ${key} + ${typeof value === 'object' ? JSON.stringify(value) : value} +
+ `)} +
+ ` : ''} + ${execution.data.error ? html` +

Error

+
+
+ ${execution.data.error.message} + ${execution.data.error.stack ? html`
${execution.data.error.stack}
` : ''} +
+
+ ` : ''} +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'cloudly-execution-details': CloudlyExecutionDetails; + } +} + diff --git a/ts_web/elements/views/tasks/parts/cloudly-task-panel.ts b/ts_web/elements/views/tasks/parts/cloudly-task-panel.ts new file mode 100644 index 0000000..368a04a --- /dev/null +++ b/ts_web/elements/views/tasks/parts/cloudly-task-panel.ts @@ -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 = {}; + + // 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` +
+
+
+ +
+
${task.name}
+
${subtitle}
+
+
+
+ ${lastExecution ? html`${lastExecution.data.status}` : html`idle`} + ${isRunning ? html` + + this.onCancel?.(task.name)} + > + ` : html` + this.onRun?.(task.name)}> + `} +
+
+ +
${task.description}
+ + ${lastExecution ? html` +
+
+
Last Status
+
+ ${lastExecution.data.status} +
+
+
+
Avg Duration
+
${avgDuration ? formatDuration(avgDuration) : '-'}
+
+
+
24h Runs · Success
+
${last24hCount} · ${successRate}%
+
+
+
+ ${lastLog ? html` ${lastLog.message}` : 'No recent logs'} +
+ + ` : html` +
+
+
Last Status
+
+
+
+
Avg Duration
+
+
+
+
24h Runs · Success
+
0 · 0%
+
+
+ `} +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'cloudly-task-panel': CloudlyTaskPanel; + } +} + diff --git a/ts_web/elements/views/tasks/utils.ts b/ts_web/elements/views/tasks/utils.ts new file mode 100644 index 0000000..52ef647 --- /dev/null +++ b/ts_web/elements/views/tasks/utils.ts @@ -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; +}