import * as plugins from '../../../plugins.js'; import * as appstate from '../../../appstate.js'; import * as shared from '../../shared/index.js'; import { DeesElement, css, cssManager, customElement, html, state, } from '@design.estate/dees-element'; type TBaseOsImageBuild = any; type TBaseOsImageSourcePreset = | 'balena-generic-amd64' | 'balena-generic-aarch64' | 'balena-raspberrypi4-64'; const sourcePresetArchitectures: Record = { 'balena-generic-amd64': 'amd64', 'balena-generic-aarch64': 'arm64', 'balena-raspberrypi4-64': 'rpi', }; @customElement('cloudly-view-baseos') export class CloudlyViewBaseOs extends DeesElement { @state() private builds: TBaseOsImageBuild[] = []; @state() private isLoading = false; private refreshTimer?: number; public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css` .layout { display: grid; grid-template-columns: 420px 1fr; gap: 16px; padding: 24px 0; } .builds { display: flex; flex-direction: column; gap: 12px; } .build { border: 1px solid #2a2f3a; border-radius: 12px; padding: 16px; background: #10151f; } .build-head { display: flex; justify-content: space-between; align-items: center; gap: 12px; } .meta { color: #9aa4b2; font-size: 13px; margin-top: 8px; } .logs { margin-top: 12px; max-height: 120px; overflow: auto; font-family: monospace; font-size: 12px; color: #bac4d1; white-space: pre-wrap; } @media (max-width: 900px) { .layout { grid-template-columns: 1fr; } } `, ]; public async connectedCallback() { await super.connectedCallback(); await this.loadBuilds(); this.refreshTimer = window.setInterval(() => this.loadBuilds(), 5000); } public async disconnectedCallback() { await super.disconnectedCallback(); if (this.refreshTimer) { window.clearInterval(this.refreshTimer); } } public render() { return html` BaseOS Images
this.createBuild((eventArg.detail as any).data)}>
${this.builds.length === 0 ? html`` : this.builds.map((buildArg) => this.renderBuild(buildArg))}
`; } private renderBuild(buildArg: TBaseOsImageBuild) { const data = buildArg.data; return html`
${data.hostname || buildArg.id}
${data.imageKind || 'ubuntu-iso'} · ${data.architecture} · ${data.cloudlyUrl}
${data.sourceImagePreset ? html`
${data.sourceImagePreset} · balenaOS ${data.balenaOsVersion || 'latest'}
` : ''}
${data.artifact ? `${data.artifact.filename} · ${Math.round(data.artifact.size / 1024 / 1024)} MB · ${data.artifact.sha256.slice(0, 12)}...` : data.errorText || 'Waiting for artifact'}
${data.status === 'ready' ? html` this.downloadBuild(buildArg.id)}>` : ''} ${data.logs?.length ? html`
${data.logs.slice(-8).join('\n')}
` : ''}
`; } private async loadBuilds() { try { const response = await this.fireBaseOsRequest('getBaseOsImageBuilds', {}); this.builds = response.builds || []; } catch (error) { console.error('Failed to load BaseOS image builds:', error); } } private async createBuild(formDataArg: any) { try { const architecture = formDataArg.architecture || 'amd64'; const imageKind = formDataArg.imageKind || 'balena-raw'; const sourceImageUrl = formDataArg.sourceImageUrl?.trim() || undefined; const selectedSourceImagePreset = formDataArg.sourceImagePreset || 'auto'; const sourceImagePreset = this.getSourceImagePreset(selectedSourceImagePreset, imageKind, sourceImageUrl); const balenaOsVersion = imageKind === 'balena-raw' && !sourceImageUrl ? formDataArg.balenaOsVersion?.trim() || 'latest' : undefined; this.validateBuildForm({ architecture, imageKind, selectedSourceImagePreset, sourceImagePreset, sourceImageUrl, wifiSsid: formDataArg.wifiSsid, wifiPassword: formDataArg.wifiPassword, }); this.isLoading = true; const response = await this.fireBaseOsRequest('createBaseOsImageBuild', { build: { architecture, imageKind, cloudlyUrl: formDataArg.cloudlyUrl || window.location.origin, hostname: formDataArg.hostname || undefined, sourceImageUrl, sourceImagePreset, balenaOsVersion, wifi: formDataArg.wifiSsid ? { ssid: formDataArg.wifiSsid, password: formDataArg.wifiPassword || undefined, } : undefined, sshPublicKey: formDataArg.sshPublicKey || undefined, }, }); this.builds = [response.build, ...this.builds]; plugins.deesCatalog.DeesToast.createAndShow({ message: 'BaseOS image build queued', type: 'success' }); } catch (error: any) { plugins.deesCatalog.DeesToast.createAndShow({ message: `Failed to create image build: ${error.message}`, type: 'error' }); } finally { this.isLoading = false; } } private getSourceImagePreset( sourceImagePresetArg: string | undefined, imageKindArg: string, sourceImageUrlArg?: string, ) { if (imageKindArg !== 'balena-raw' || sourceImageUrlArg || sourceImagePresetArg === 'auto') { return undefined; } if (sourceImagePresetArg === 'custom-url') { return undefined; } return sourceImagePresetArg as TBaseOsImageSourcePreset | undefined; } private validateBuildForm(optionsArg: { architecture: string; imageKind: string; selectedSourceImagePreset?: string; sourceImagePreset?: TBaseOsImageSourcePreset; sourceImageUrl?: string; wifiSsid?: string; wifiPassword?: string; }) { if (optionsArg.architecture === 'rpi' && optionsArg.imageKind === 'ubuntu-iso') { throw new Error('Raspberry Pi BaseOS images require the balenaOS raw image type.'); } if ( optionsArg.imageKind === 'balena-raw' && optionsArg.selectedSourceImagePreset === 'custom-url' && !optionsArg.sourceImageUrl ) { throw new Error('A custom source image URL is required when the custom source preset is selected.'); } if (optionsArg.imageKind === 'balena-raw' && optionsArg.sourceImagePreset) { const expectedArchitecture = sourcePresetArchitectures[optionsArg.sourceImagePreset]; if (expectedArchitecture !== optionsArg.architecture) { throw new Error(`${optionsArg.sourceImagePreset} is only valid for ${expectedArchitecture} images.`); } } if (optionsArg.wifiPassword && !optionsArg.wifiSsid) { throw new Error('A WiFi SSID is required when a WiFi password is set.'); } } private async downloadBuild(buildIdArg: string) { const response = await this.fireBaseOsRequest('createBaseOsImageDownloadUrl', { buildId: buildIdArg, }); window.location.href = response.url; } private async fireBaseOsRequest(methodArg: string, payloadArg: Record) { appstate.apiClient.identity = appstate.loginStatePart.getState()?.identity || null as any; if (!appstate.apiClient.typedsocketClient) { await appstate.apiClient.start(); } const request = appstate.apiClient.typedsocketClient.createTypedRequest(methodArg); return await request.fire({ identity: appstate.apiClient.identity, ...payloadArg, }); } } declare global { interface HTMLElementTagNameMap { 'cloudly-view-baseos': CloudlyViewBaseOs; } }