Files
cloudly/ts_web/elements/views/settings/index.ts
Juergen Kunz bb313fd9dc feat: Add settings view for cloud provider configurations
- Implemented CloudlyViewSettings component for managing cloud provider settings including Hetzner, Cloudflare, AWS, DigitalOcean, Azure, and Google Cloud.
- Added functionality to load, save, and test connections for each provider.
- Enhanced UI with loading states and success/error notifications.

feat: Create tasks view with execution history

- Developed CloudlyViewTasks component to display and manage tasks and their executions.
- Integrated auto-refresh functionality for task executions.
- Added filtering and searching capabilities for tasks.

feat: Implement execution details and task panel components

- Created CloudlyExecutionDetails component to show detailed information about task executions including logs and metrics.
- Developed CloudlyTaskPanel component to display individual tasks with execution status and actions to run or cancel tasks.

feat: Utility functions for formatting and categorization

- Added utility functions for formatting dates, durations, and cron expressions.
- Implemented functions to retrieve category icons and hues for task categorization.
2025-09-14 17:28:21 +00:00

207 lines
11 KiB
TypeScript

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<plugins.interfaces.data.ICloudlySettings> = {};
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`<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) => { this.saveSettings((e.detail as any).data); }}>
<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>
<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>
<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>
<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>
<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>
<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>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'cloudly-view-settings': CloudlyViewSettings;
}
}