f40ef6b7c0
Align Cloudly with the current typedserver, smartconfig, smartstate, and Docker tooling releases so builds and Docker output stay compatible with the upgraded stack.
306 lines
11 KiB
TypeScript
306 lines
11 KiB
TypeScript
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<TBaseOsImageSourcePreset, string> = {
|
|
'balena-generic-amd64': 'amd64',
|
|
'balena-generic-aarch64': 'arm64',
|
|
'balena-raspberrypi4-64': 'rpi',
|
|
};
|
|
|
|
@customElement('cloudly-view-baseos')
|
|
export class CloudlyViewBaseOs extends DeesElement {
|
|
@state() private accessor builds: TBaseOsImageBuild[] = [];
|
|
@state() private accessor 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`
|
|
<cloudly-sectionheading>BaseOS Images</cloudly-sectionheading>
|
|
<div class="layout">
|
|
<dees-panel .title=${'Create Image'} .subtitle=${'Build a Cloudly-bound BaseOS artifact'} .variant=${'outline'}>
|
|
<dees-form @formData=${(eventArg: CustomEvent) => this.createBuild((eventArg.detail as any).data)}>
|
|
<dees-input-dropdown
|
|
.key=${'imageKind'}
|
|
.label=${'Image Type'}
|
|
.selectedOption=${'balena-raw'}
|
|
.options=${[
|
|
{ key: 'balena-raw', option: 'balenaOS raw image', payload: null },
|
|
{ key: 'ubuntu-iso', option: 'Ubuntu bootstrap ISO', payload: null },
|
|
]}
|
|
></dees-input-dropdown>
|
|
<dees-input-dropdown
|
|
.key=${'architecture'}
|
|
.label=${'Architecture'}
|
|
.selectedOption=${'amd64'}
|
|
.options=${[
|
|
{ key: 'amd64', option: 'amd64', payload: null },
|
|
{ key: 'arm64', option: 'arm64', payload: null },
|
|
{ key: 'rpi', option: 'Raspberry Pi', payload: null },
|
|
]}
|
|
></dees-input-dropdown>
|
|
<dees-input-text .key=${'cloudlyUrl'} .label=${'Cloudly URL'} .value=${window.location.origin} .required=${true}></dees-input-text>
|
|
<dees-input-text .key=${'hostname'} .label=${'Hostname'} .value=${'baseos-node'} .required=${false}></dees-input-text>
|
|
<dees-input-dropdown
|
|
.key=${'sourceImagePreset'}
|
|
.label=${'balenaOS Source Preset'}
|
|
.selectedOption=${'auto'}
|
|
.options=${[
|
|
{ key: 'auto', option: 'Auto by architecture', payload: null },
|
|
{ key: 'balena-generic-amd64', option: 'Generic x86_64 (GPT)', payload: null },
|
|
{ key: 'balena-generic-aarch64', option: 'Generic AARCH64', payload: null },
|
|
{ key: 'balena-raspberrypi4-64', option: 'Raspberry Pi 4 64-bit', payload: null },
|
|
{ key: 'custom-url', option: 'Custom source URL', payload: null },
|
|
]}
|
|
.description=${'Used for balenaOS raw images when no custom source URL is provided.'}
|
|
></dees-input-dropdown>
|
|
<dees-input-text .key=${'balenaOsVersion'} .label=${'balenaOS Version'} .value=${'latest'} .description=${'Use latest, or an explicit balenaOS raw_version such as 2026.1.0.'} .required=${false}></dees-input-text>
|
|
<dees-input-text .key=${'wifiSsid'} .label=${'WiFi SSID'} .required=${false}></dees-input-text>
|
|
<dees-input-text .key=${'wifiPassword'} .label=${'WiFi Password'} .isPasswordBool=${true} .required=${false}></dees-input-text>
|
|
<dees-input-textarea .key=${'sshPublicKey'} .label=${'SSH Public Key'} .required=${false}></dees-input-textarea>
|
|
<dees-input-text .key=${'sourceImageUrl'} .label=${'Custom Source Image URL'} .description=${'Optional override for balenaOS raw images (.img, .img.xz, .zip, or balena download URL with fileType=.zip).'} .required=${false}></dees-input-text>
|
|
<dees-form-submit .text=${this.isLoading ? 'Creating...' : 'Create BaseOS Image'} .disabled=${this.isLoading}></dees-form-submit>
|
|
</dees-form>
|
|
</dees-panel>
|
|
<div class="builds">
|
|
${this.builds.length === 0
|
|
? html`<dees-panel .title=${'No image builds yet'} .subtitle=${'Create an image to start a corebuild job.'}></dees-panel>`
|
|
: this.builds.map((buildArg) => this.renderBuild(buildArg))}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderBuild(buildArg: TBaseOsImageBuild) {
|
|
const data = buildArg.data;
|
|
return html`
|
|
<div class="build">
|
|
<div class="build-head">
|
|
<div>
|
|
<strong>${data.hostname || buildArg.id}</strong>
|
|
<div class="meta">${data.imageKind || 'ubuntu-iso'} · ${data.architecture} · ${data.cloudlyUrl}</div>
|
|
${data.sourceImagePreset ? html`<div class="meta">${data.sourceImagePreset} · balenaOS ${data.balenaOsVersion || 'latest'}</div>` : ''}
|
|
</div>
|
|
<dees-badge .text=${data.status} .type=${data.status === 'ready' ? 'success' : data.status === 'failed' ? 'error' : 'info'}></dees-badge>
|
|
</div>
|
|
<div class="meta">
|
|
${data.artifact ? `${data.artifact.filename} · ${Math.round(data.artifact.size / 1024 / 1024)} MB · ${data.artifact.sha256.slice(0, 12)}...` : data.errorText || 'Waiting for artifact'}
|
|
</div>
|
|
${data.status === 'ready'
|
|
? html`<dees-button .text=${'Download'} .type=${'primary'} @click=${() => this.downloadBuild(buildArg.id)}></dees-button>`
|
|
: ''}
|
|
${data.logs?.length ? html`<div class="logs">${data.logs.slice(-8).join('\n')}</div>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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<string, unknown>) {
|
|
appstate.apiClient.identity = appstate.loginStatePart.getState()?.identity || null as any;
|
|
if (!appstate.apiClient.typedsocketClient) {
|
|
await appstate.apiClient.start();
|
|
}
|
|
const request = appstate.apiClient.typedsocketClient.createTypedRequest<any>(methodArg);
|
|
return await request.fire({
|
|
identity: appstate.apiClient.identity,
|
|
...payloadArg,
|
|
});
|
|
}
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
'cloudly-view-baseos': CloudlyViewBaseOs;
|
|
}
|
|
}
|