feat(settings): Add runtime settings management, node & baremetal managers, and settings UI
This commit is contained in:
@@ -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.'
|
||||
}
|
||||
|
@@ -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