feat(settings): Add runtime settings management, node & baremetal managers, and settings UI

This commit is contained in:
2025-09-07 17:21:30 +00:00
parent 83abe37d8c
commit 54ef62e7af
36 changed files with 1914 additions and 301 deletions

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/cloudly',
version: '5.1.0',
version: '5.2.0',
description: 'A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.'
}

View File

@@ -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',

View File

@@ -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',

View 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>
`;
}
}