f40ef6b7c0
Align Cloudly with the current typedserver, smartconfig, smartstate, and Docker tooling releases so builds and Docker output stay compatible with the upgraded stack.
230 lines
14 KiB
TypeScript
230 lines
14 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 accessor settings: plugins.interfaces.data.ICloudlySettingsMasked = {} as any;
|
|
|
|
@state()
|
|
private accessor isLoading = false;
|
|
|
|
@state()
|
|
private accessor 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().startsWith('****')) {
|
|
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=${'External dcrouter Gateway'} .subtitle=${'Route WorkApps through a dcrouter edge authority'} .variant=${'outline'}>
|
|
<div class="form-grid">
|
|
<dees-input-text .key=${'dcrouterGatewayUrl'} .label=${'Gateway URL'} .value=${this.settings.dcrouterGatewayUrl || ''} .description=${'Base URL of the dcrouter OpsServer, for example https://edge.example.com'} .required=${false}></dees-input-text>
|
|
<dees-input-text .key=${'dcrouterGatewayApiToken'} .label=${'API Token'} .value=${this.settings.dcrouterGatewayApiToken || ''} .isPasswordBool=${true} .description=${'dcrouter API token with workhosters and certificates scopes'} .required=${false}></dees-input-text>
|
|
</div>
|
|
<div class="form-grid">
|
|
<dees-input-text .key=${'dcrouterWorkHosterId'} .label=${'WorkHoster ID'} .value=${this.settings.dcrouterWorkHosterId || ''} .description=${'Optional stable owner ID; defaults to the cluster ID'} .required=${false}></dees-input-text>
|
|
<dees-input-text .key=${'dcrouterTargetHost'} .label=${'Target Host'} .value=${this.settings.dcrouterTargetHost || ''} .description=${'Host or IP dcrouter forwards workload traffic to'} .required=${false}></dees-input-text>
|
|
</div>
|
|
<div class="form-grid single">
|
|
<dees-input-text .key=${'dcrouterTargetPort'} .label=${'Target Port'} .value=${this.settings.dcrouterTargetPort || '80'} .description=${'Internal HTTP port dcrouter should forward to'} .required=${false}></dees-input-text>
|
|
</div>
|
|
</dees-panel>
|
|
|
|
<dees-panel .title=${'CoreBuild Worker'} .subtitle=${'Build BaseOS images on a capable worker node'} .variant=${'outline'}>
|
|
<div class="form-grid">
|
|
<dees-input-text .key=${'corebuildWorkerUrl'} .label=${'Worker URL'} .value=${this.settings.corebuildWorkerUrl || ''} .description=${'Base URL of the corebuild worker, for example http://10.0.0.20:3060'} .required=${false}></dees-input-text>
|
|
<dees-input-text .key=${'corebuildWorkerToken'} .label=${'Worker Token'} .value=${this.settings.corebuildWorkerToken || ''} .isPasswordBool=${true} .description=${'Shared token accepted by the corebuild worker'} .required=${false}></dees-input-text>
|
|
</div>
|
|
<div class="form-grid single">
|
|
<dees-input-textarea .key=${'corebuildWorkersJson'} .label=${'Worker Registry JSON'} .value=${this.settings.corebuildWorkersJson || ''} .isPasswordBool=${true} .description=${'Optional JSON array of workers: [{"id":"builder-1","url":"http://10.0.0.20:3060","token":"secret"}]'} .required=${false}></dees-input-textarea>
|
|
</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;
|
|
}
|
|
}
|