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}} = {}; @state() private accessor hostedRuntime: appstate.IHostedRuntimeState = { isHosted: false, loading: false, upgradeState: null, }; constructor() { super(); const hostedRuntimeSubscription = appstate.hostedRuntimeStatePart .select((stateArg) => stateArg) .subscribe((stateArg) => { this.hostedRuntime = stateArg; }); this.rxSubscriptions.push(hostedRuntimeSubscription); const loginSubscription = appstate.loginStatePart .select((stateArg) => stateArg.identity) .subscribe((identityArg) => { if (identityArg) { void this.refreshHostedRuntimeStatus(); } }); this.rxSubscriptions.push(loginSubscription); 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; } .runtime-panel { display: grid; gap: 16px; } .runtime-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; } .runtime-card { border: 1px solid var(--ci-shade-2, #27272a); border-radius: 8px; padding: 12px; background: var(--ci-shade-1, #09090b); } .runtime-label { color: var(--ci-shade-4, #71717a); font-size: 12px; margin-bottom: 6px; } .runtime-value { color: var(--ci-shade-7, #e4e4e7); font-size: 14px; font-weight: 600; overflow-wrap: anywhere; } .runtime-message { color: var(--ci-shade-5, #a1a1aa); font-size: 13px; line-height: 1.5; } .runtime-message.error { color: #ef4444; } .runtime-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } @media (max-width: 768px) { .form-grid { grid-template-columns: 1fr; } } @media (max-width: 768px) { .runtime-grid { grid-template-columns: 1fr; } } `, ]; public async connectedCallback() { super.connectedCallback(); await this.refreshHostedRuntimeStatus(); } 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 = {}; 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``; } private async refreshHostedRuntimeStatus() { if (!appstate.loginStatePart.getState()?.identity) { return; } await appstate.hostedRuntimeStatePart.dispatchAction(appstate.fetchHostedRuntimeUpgradeStatusAction, null); } private getHostedRuntimeBadgeType() { const status = this.hostedRuntime.upgradeState?.status; if (!this.hostedRuntime.isHosted) return 'info'; if (status === 'failed') return 'error'; if (status === 'upToDate' || status === 'success') return 'success'; return 'info'; } private getHostedRuntimeStatusText() { if (!this.hostedRuntime.isHosted) return 'Not hosted'; const status = this.hostedRuntime.upgradeState?.status || 'unknown'; switch (status) { case 'upToDate': return 'Up to date'; case 'available': return 'Update available'; case 'running': return 'Upgrade running'; case 'success': return 'Upgrade complete'; case 'failed': return 'Upgrade failed'; default: return 'Unknown'; } } private getHostedRuntimeMessage() { if (!this.hostedRuntime.isHosted) { return this.hostedRuntime.unavailableReason || 'This Cloudly instance is not running as a managed hosted app.'; } if (this.hostedRuntime.unavailableReason) { return this.hostedRuntime.unavailableReason; } const upgradeState = this.hostedRuntime.upgradeState; if (upgradeState?.status === 'available') { return `Parent host can upgrade Cloudly from ${upgradeState.currentVersion || 'current'} to ${upgradeState.targetVersion || upgradeState.latestVersion}.`; } if (upgradeState?.status === 'running') { return 'The parent host is upgrading this Cloudly service. Status refreshes automatically.'; } if (upgradeState?.status === 'failed') { return upgradeState.error || 'The last parent-hosted upgrade failed.'; } return 'Parent hosted runtime status is available. No upgrade action is currently required.'; } private async startHostedRuntimeUpgrade() { const upgradeState = this.hostedRuntime.upgradeState; const targetVersion = upgradeState?.targetVersion || upgradeState?.latestVersion; if (!targetVersion) { plugins.deesCatalog.DeesToast.createAndShow({ message: 'No hosted runtime upgrade target is available.', type: 'error' }); return; } let upgradeStarting = false; await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Upgrade Hosted Cloudly', content: html`
The parent host will upgrade this Cloudly app from ${upgradeState?.currentVersion || 'current'} to ${targetVersion} using its hosted app lifecycle controls.
`, menuOptions: [ { name: 'Start Upgrade', action: async (modalArg: any) => { if (upgradeStarting) return; upgradeStarting = true; try { await appstate.hostedRuntimeStatePart.dispatchAction(appstate.startHostedRuntimeParentUpgradeAction, { targetVersion }); plugins.deesCatalog.DeesToast.createAndShow({ message: 'Hosted runtime upgrade started.', type: 'success' }); await modalArg.destroy(); } catch (error) { upgradeStarting = false; plugins.deesCatalog.DeesToast.createAndShow({ message: `Upgrade failed: ${(error as Error).message}`, type: 'error' }); } }, }, { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() }, ], }); } private renderHostedRuntimePanel() { const upgradeState = this.hostedRuntime.upgradeState; const canStartUpgrade = this.hostedRuntime.isHosted && upgradeState?.status === 'available' && !this.hostedRuntime.loading; return html`
Runtime
${this.hostedRuntime.isHosted ? 'Managed hosted app' : 'Standalone'}
Upgrade Status
Version
${upgradeState?.currentVersion || '-'}${upgradeState?.latestVersion ? ` / ${upgradeState.latestVersion}` : ''}
${this.getHostedRuntimeMessage()}
${upgradeState?.warnings?.length ? html`
${upgradeState.warnings.join(' | ')}
` : ''}
this.refreshHostedRuntimeStatus()}> this.startHostedRuntimeUpgrade()}>
`; } public render() { if (this.isLoading && Object.keys(this.settings).length === 0) { return html`
`; } return html` Settings
${this.renderHostedRuntimePanel()} { this.saveSettings((e.detail as any).data); }}>
${this.renderProviderStatus('hetzner')} { e.preventDefault(); e.stopPropagation(); this.testConnection('hetzner'); }}>
${this.renderProviderStatus('cloudflare')} { e.preventDefault(); e.stopPropagation(); this.testConnection('cloudflare'); }}>
${this.renderProviderStatus('aws')} { e.preventDefault(); e.stopPropagation(); this.testConnection('aws'); }}>
${this.renderProviderStatus('digitalocean')} { e.preventDefault(); e.stopPropagation(); this.testConnection('digitalocean'); }}>
${this.renderProviderStatus('azure')} { e.preventDefault(); e.stopPropagation(); this.testConnection('azure'); }}>
${this.renderProviderStatus('google')} { e.preventDefault(); e.stopPropagation(); this.testConnection('google'); }}>
`; } } declare global { interface HTMLElementTagNameMap { 'cloudly-view-settings': CloudlyViewSettings; } }