feat(settings): Add runtime settings management, node & baremetal managers, and settings UI
This commit is contained in:
		| @@ -25,6 +25,7 @@ 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'; | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
| @@ -79,6 +80,11 @@ export class CloudlyDashboard extends DeesElement { | ||||
|                 iconName: 'lucide:LayoutDashboard', | ||||
|                 element: CloudlyViewOverview, | ||||
|               }, | ||||
|               { | ||||
|                 name: 'Settings', | ||||
|                 iconName: 'lucide:Settings', | ||||
|                 element: CloudlyViewSettings, | ||||
|               }, | ||||
|               { | ||||
|                 name: 'SecretGroups', | ||||
|                 iconName: 'lucide:ShieldCheck', | ||||
|   | ||||
| @@ -40,9 +40,9 @@ export class CloudlyViewOverview extends DeesElement { | ||||
|   ]; | ||||
|  | ||||
|   public render() { | ||||
|     // Calculate total servers across all clusters | ||||
|     const totalServers = this.data.clusters?.reduce((sum, cluster) =>  | ||||
|       sum + (cluster.data.servers?.length || 0), 0) || 0; | ||||
|     // 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 = [ | ||||
| @@ -55,12 +55,12 @@ export class CloudlyViewOverview extends DeesElement { | ||||
|         description: 'Active clusters' | ||||
|       }, | ||||
|       { | ||||
|         id: 'servers', | ||||
|         title: 'Total Servers', | ||||
|         value: totalServers, | ||||
|         id: 'nodes', | ||||
|         title: 'Total Nodes', | ||||
|         value: totalNodes, | ||||
|         type: 'number' as const, | ||||
|         iconName: 'lucide:Server', | ||||
|         description: 'Connected servers' | ||||
|         description: 'Connected nodes' | ||||
|       }, | ||||
|       { | ||||
|         id: 'services', | ||||
|   | ||||
							
								
								
									
										478
									
								
								ts_web/elements/cloudly-view-settings.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										478
									
								
								ts_web/elements/cloudly-view-settings.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,478 @@ | ||||
| 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 { | ||||
|       const trRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest< | ||||
|         plugins.interfaces.requests.settings.IRequest_GetSettings | ||||
|       >( | ||||
|         '/typedrequest', | ||||
|         'getSettings' | ||||
|       ); | ||||
|       const response = await trRequest.fire({}); | ||||
|       this.settings = response.settings; | ||||
|     } catch (error) { | ||||
|       console.error('Failed to load settings:', error); | ||||
|       plugins.deesCatalog.DeesToast.createAndShow({ | ||||
|         message: `Failed to load settings: ${error.message}`, | ||||
|         type: 'error', | ||||
|       }); | ||||
|     } finally { | ||||
|       this.isLoading = false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async saveSettings(formData: any) { | ||||
|     console.log('saveSettings called with formData:', formData); | ||||
|     this.isLoading = true; | ||||
|     try { | ||||
|       const updates: Partial<plugins.interfaces.data.ICloudlySettings> = {}; | ||||
|        | ||||
|       // Process form data | ||||
|       for (const [key, value] of Object.entries(formData)) { | ||||
|         console.log(`Processing ${key}:`, value); | ||||
|         if (value !== undefined && value !== '****' && !value?.toString().endsWith('****')) { | ||||
|           // Only update if value changed (not masked) | ||||
|           updates[key as keyof plugins.interfaces.data.ICloudlySettings] = value as string; | ||||
|         } | ||||
|       } | ||||
|       console.log('Updates to send:', updates); | ||||
|  | ||||
|       const trRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest< | ||||
|         plugins.interfaces.requests.settings.IRequest_UpdateSettings | ||||
|       >( | ||||
|         '/typedrequest', | ||||
|         'updateSettings' | ||||
|       ); | ||||
|       const response = await trRequest.fire({ 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 trRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest< | ||||
|         plugins.interfaces.requests.settings.IRequest_TestProviderConnection | ||||
|       >( | ||||
|         '/typedrequest', | ||||
|         'testProviderConnection' | ||||
|       ); | ||||
|       const response = await trRequest.fire({ provider: provider as any }); | ||||
|  | ||||
|       this.testResults = { | ||||
|         ...this.testResults, | ||||
|         [provider]: { | ||||
|           success: response.connectionValid, | ||||
|           message: response.message | ||||
|         } | ||||
|       }; | ||||
|  | ||||
|       // Show toast notification | ||||
|       plugins.deesCatalog.DeesToast.createAndShow({ | ||||
|         message: response.message, | ||||
|         type: response.connectionValid ? 'success' : 'error', | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       this.testResults = { | ||||
|         ...this.testResults, | ||||
|         [provider]: { | ||||
|           success: false, | ||||
|           message: `Test failed: ${error.message}` | ||||
|         } | ||||
|       }; | ||||
|       plugins.deesCatalog.DeesToast.createAndShow({ | ||||
|         message: `Connection test failed: ${error.message}`, | ||||
|         type: 'error', | ||||
|       }); | ||||
|     } finally { | ||||
|       this.isLoading = false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private renderProviderStatus(provider: string) { | ||||
|     const result = this.testResults[provider]; | ||||
|     if (!result) return ''; | ||||
|      | ||||
|     return html` | ||||
|       <dees-badge  | ||||
|         .type=${result.success ? 'success' : 'error'} | ||||
|         .text=${result.success ? 'Connected' : 'Failed'} | ||||
|       ></dees-badge> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   public render() { | ||||
|     if (this.isLoading && Object.keys(this.settings).length === 0) { | ||||
|       return html` | ||||
|         <div class="loading-container"> | ||||
|           <dees-spinner></dees-spinner> | ||||
|         </div> | ||||
|       `; | ||||
|     } | ||||
|  | ||||
|     return html` | ||||
|       <cloudly-sectionheading>Settings</cloudly-sectionheading> | ||||
|       <div class="settings-container"> | ||||
|         <dees-form @formData=${(e: CustomEvent) => { | ||||
|           console.log('formData event received:', e); | ||||
|           console.log('Event detail:', e.detail); | ||||
|           console.log('Event detail.data:', e.detail.data); | ||||
|           this.saveSettings(e.detail.data); | ||||
|         }}> | ||||
|              | ||||
|             <!-- Hetzner Cloud --> | ||||
|             <dees-panel  | ||||
|               .title=${'Hetzner Cloud'} | ||||
|               .subtitle=${'Configure Hetzner Cloud API access'} | ||||
|               .variant=${'outline'} | ||||
|             > | ||||
|               <div class="test-status"> | ||||
|                 ${this.renderProviderStatus('hetzner')} | ||||
|                 <dees-button | ||||
|                   .text=${'Test Connection'} | ||||
|                   .type=${'secondary'} | ||||
|                   @click=${(e: Event) => { | ||||
|                     e.preventDefault(); | ||||
|                     e.stopPropagation(); | ||||
|                     this.testConnection('hetzner'); | ||||
|                   }} | ||||
|                 ></dees-button> | ||||
|               </div> | ||||
|               <div class="form-grid single"> | ||||
|                 <dees-input-text | ||||
|                   .key=${'hetznerToken'} | ||||
|                   .label=${'API Token'} | ||||
|                   .value=${this.settings.hetznerToken || ''} | ||||
|                   .isPasswordBool=${true} | ||||
|                   .description=${'Your Hetzner Cloud API token for managing infrastructure'} | ||||
|                   .required=${false} | ||||
|                 ></dees-input-text> | ||||
|               </div> | ||||
|             </dees-panel> | ||||
|  | ||||
|             <!-- Cloudflare --> | ||||
|             <dees-panel  | ||||
|               .title=${'Cloudflare'} | ||||
|               .subtitle=${'Configure Cloudflare API access'} | ||||
|               .variant=${'outline'} | ||||
|             > | ||||
|               <div class="test-status"> | ||||
|                 ${this.renderProviderStatus('cloudflare')} | ||||
|                 <dees-button | ||||
|                   .text=${'Test Connection'} | ||||
|                   .type=${'secondary'} | ||||
|                   @click=${(e: Event) => { | ||||
|                     e.preventDefault(); | ||||
|                     e.stopPropagation(); | ||||
|                     this.testConnection('cloudflare'); | ||||
|                   }} | ||||
|                 ></dees-button> | ||||
|               </div> | ||||
|               <div class="form-grid single"> | ||||
|                 <dees-input-text | ||||
|                   .key=${'cloudflareToken'} | ||||
|                   .label=${'API Token'} | ||||
|                   .value=${this.settings.cloudflareToken || ''} | ||||
|                   .isPasswordBool=${true} | ||||
|                   .description=${'Cloudflare API token with DNS and Zone permissions'} | ||||
|                   .required=${false} | ||||
|                 ></dees-input-text> | ||||
|               </div> | ||||
|             </dees-panel> | ||||
|  | ||||
|             <!-- AWS --> | ||||
|             <dees-panel  | ||||
|               .title=${'Amazon Web Services'} | ||||
|               .subtitle=${'Configure AWS credentials'} | ||||
|               .variant=${'outline'} | ||||
|             > | ||||
|               <div class="test-status"> | ||||
|                 ${this.renderProviderStatus('aws')} | ||||
|                 <dees-button | ||||
|                   .text=${'Test Connection'} | ||||
|                   .type=${'secondary'} | ||||
|                   @click=${(e: Event) => { | ||||
|                     e.preventDefault(); | ||||
|                     e.stopPropagation(); | ||||
|                     this.testConnection('aws'); | ||||
|                   }} | ||||
|                 ></dees-button> | ||||
|               </div> | ||||
|               <div class="form-grid"> | ||||
|                 <dees-input-text | ||||
|                   .key=${'awsAccessKey'} | ||||
|                   .label=${'Access Key ID'} | ||||
|                   .value=${this.settings.awsAccessKey || ''} | ||||
|                   .isPasswordBool=${true} | ||||
|                   .description=${'AWS IAM access key identifier'} | ||||
|                   .required=${false} | ||||
|                 ></dees-input-text> | ||||
|                 <dees-input-text | ||||
|                   .key=${'awsSecretKey'} | ||||
|                   .label=${'Secret Access Key'} | ||||
|                   .value=${this.settings.awsSecretKey || ''} | ||||
|                   .isPasswordBool=${true} | ||||
|                   .description=${'AWS IAM secret access key'} | ||||
|                   .required=${false} | ||||
|                 ></dees-input-text> | ||||
|               </div> | ||||
|               <div class="form-grid single"> | ||||
|                 <dees-input-dropdown | ||||
|                   .key=${'awsRegion'} | ||||
|                   .label=${'Default Region'} | ||||
|                   .selectedOption=${this.settings.awsRegion || 'us-east-1'} | ||||
|                   .options=${[ | ||||
|                     { key: 'us-east-1', option: 'US East (N. Virginia)', payload: null }, | ||||
|                     { key: 'us-west-2', option: 'US West (Oregon)', payload: null }, | ||||
|                     { key: 'eu-west-1', option: 'EU (Ireland)', payload: null }, | ||||
|                     { key: 'eu-central-1', option: 'EU (Frankfurt)', payload: null }, | ||||
|                     { key: 'ap-southeast-1', option: 'Asia Pacific (Singapore)', payload: null }, | ||||
|                     { key: 'ap-northeast-1', option: 'Asia Pacific (Tokyo)', payload: null }, | ||||
|                   ]} | ||||
|                   .description=${'Default AWS region for resource provisioning'} | ||||
|                 ></dees-input-dropdown> | ||||
|               </div> | ||||
|             </dees-panel> | ||||
|  | ||||
|             <!-- DigitalOcean --> | ||||
|             <dees-panel  | ||||
|               .title=${'DigitalOcean'} | ||||
|               .subtitle=${'Configure DigitalOcean API access'} | ||||
|               .variant=${'outline'} | ||||
|             > | ||||
|               <div class="test-status"> | ||||
|                 ${this.renderProviderStatus('digitalocean')} | ||||
|                 <dees-button | ||||
|                   .text=${'Test Connection'} | ||||
|                   .type=${'secondary'} | ||||
|                   @click=${(e: Event) => { | ||||
|                     e.preventDefault(); | ||||
|                     e.stopPropagation(); | ||||
|                     this.testConnection('digitalocean'); | ||||
|                   }} | ||||
|                 ></dees-button> | ||||
|               </div> | ||||
|               <div class="form-grid single"> | ||||
|                 <dees-input-text | ||||
|                   .key=${'digitalOceanToken'} | ||||
|                   .label=${'Personal Access Token'} | ||||
|                   .value=${this.settings.digitalOceanToken || ''} | ||||
|                   .isPasswordBool=${true} | ||||
|                   .description=${'DigitalOcean personal access token with read/write scope'} | ||||
|                   .required=${false} | ||||
|                 ></dees-input-text> | ||||
|               </div> | ||||
|             </dees-panel> | ||||
|  | ||||
|             <!-- Azure --> | ||||
|             <dees-panel  | ||||
|               .title=${'Microsoft Azure'} | ||||
|               .subtitle=${'Configure Azure service principal'} | ||||
|               .variant=${'outline'} | ||||
|             > | ||||
|               <div class="test-status"> | ||||
|                 ${this.renderProviderStatus('azure')} | ||||
|                 <dees-button | ||||
|                   .text=${'Test Connection'} | ||||
|                   .type=${'secondary'} | ||||
|                   @click=${(e: Event) => { | ||||
|                     e.preventDefault(); | ||||
|                     e.stopPropagation(); | ||||
|                     this.testConnection('azure'); | ||||
|                   }} | ||||
|                 ></dees-button> | ||||
|               </div> | ||||
|               <div class="form-grid"> | ||||
|                 <dees-input-text | ||||
|                   .key=${'azureClientId'} | ||||
|                   .label=${'Application (Client) ID'} | ||||
|                   .value=${this.settings.azureClientId || ''} | ||||
|                   .isPasswordBool=${true} | ||||
|                   .description=${'Azure AD application client ID'} | ||||
|                   .required=${false} | ||||
|                 ></dees-input-text> | ||||
|                 <dees-input-text | ||||
|                   .key=${'azureClientSecret'} | ||||
|                   .label=${'Client Secret'} | ||||
|                   .value=${this.settings.azureClientSecret || ''} | ||||
|                   .isPasswordBool=${true} | ||||
|                   .description=${'Azure AD application client secret'} | ||||
|                   .required=${false} | ||||
|                 ></dees-input-text> | ||||
|               </div> | ||||
|               <div class="form-grid"> | ||||
|                 <dees-input-text | ||||
|                   .key=${'azureTenantId'} | ||||
|                   .label=${'Directory (Tenant) ID'} | ||||
|                   .value=${this.settings.azureTenantId || ''} | ||||
|                   .description=${'Azure AD tenant identifier'} | ||||
|                   .required=${false} | ||||
|                 ></dees-input-text> | ||||
|                 <dees-input-text | ||||
|                   .key=${'azureSubscriptionId'} | ||||
|                   .label=${'Subscription ID'} | ||||
|                   .value=${this.settings.azureSubscriptionId || ''} | ||||
|                   .description=${'Azure subscription for resource management'} | ||||
|                   .required=${false} | ||||
|                 ></dees-input-text> | ||||
|               </div> | ||||
|             </dees-panel> | ||||
|  | ||||
|             <!-- Google Cloud --> | ||||
|             <dees-panel  | ||||
|               .title=${'Google Cloud Platform'} | ||||
|               .subtitle=${'Configure GCP service account'} | ||||
|               .variant=${'outline'} | ||||
|             > | ||||
|               <div class="test-status"> | ||||
|                 ${this.renderProviderStatus('google')} | ||||
|                 <dees-button | ||||
|                   .text=${'Test Connection'} | ||||
|                   .type=${'secondary'} | ||||
|                   @click=${(e: Event) => { | ||||
|                     e.preventDefault(); | ||||
|                     e.stopPropagation(); | ||||
|                     this.testConnection('google'); | ||||
|                   }} | ||||
|                 ></dees-button> | ||||
|               </div> | ||||
|               <div class="form-grid single"> | ||||
|                 <dees-input-textarea | ||||
|                   .key=${'googleCloudKeyJson'} | ||||
|                   .label=${'Service Account Key (JSON)'} | ||||
|                   .value=${this.settings.googleCloudKeyJson || ''} | ||||
|                   .isPasswordBool=${true} | ||||
|                   .description=${'Complete JSON key file for service account authentication'} | ||||
|                   .required=${false} | ||||
|                 ></dees-input-textarea> | ||||
|               </div> | ||||
|               <div class="form-grid single"> | ||||
|                 <dees-input-text | ||||
|                   .key=${'googleCloudProjectId'} | ||||
|                   .label=${'Project ID'} | ||||
|                   .value=${this.settings.googleCloudProjectId || ''} | ||||
|                   .description=${'Google Cloud project identifier'} | ||||
|                   .required=${false} | ||||
|                 ></dees-input-text> | ||||
|               </div> | ||||
|             </dees-panel> | ||||
|  | ||||
|             <div class="actions-container"> | ||||
|               <dees-form-submit  | ||||
|                 .text=${'Save All Settings'} | ||||
|                 .disabled=${this.isLoading} | ||||
|               ></dees-form-submit> | ||||
|             </div> | ||||
|           </dees-form> | ||||
|         </div> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user