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.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`
-
-
-
-
-
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
-
-
-
-
-
- ${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
+
+
+
+
+
+ ${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.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`
+
+
+
+
+
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;
+}