2026-05-07 17:44:31 +00:00
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 ;
@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 `
<cloudly-sectionheading>BaseOS Images</cloudly-sectionheading>
<div class="layout">
2026-05-07 19:49:56 +00:00
<dees-panel .title= ${ 'Create Image' } .subtitle= ${ 'Build a Cloudly-bound BaseOS artifact' } .variant= ${ 'outline' } >
2026-05-07 17:44:31 +00:00
<dees-form @formData= ${ ( eventArg : CustomEvent ) = > this . createBuild ( ( eventArg . detail as any ) . data ) } >
2026-05-07 19:49:56 +00:00
<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>
2026-05-07 17:44:31 +00:00
<dees-input-dropdown
.key= ${ 'architecture' }
.label= ${ 'Architecture' }
.selectedOption= ${ 'amd64' }
.options= ${ [
2026-05-07 19:49:56 +00:00
{ key : 'amd64' , option : 'amd64' , payload : null } ,
{ key: 'arm64', option: 'arm64', payload: null },
{ key: 'rpi', option: 'Raspberry Pi', payload: null },
2026-05-07 17:44:31 +00:00
]}
></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-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>
2026-05-07 19:49:56 +00:00
<dees-input-text .key= ${ 'sourceImageUrl' } .label= ${ 'Source Image URL' } .description= ${ 'Required for balenaOS raw images (.img, .img.xz, or .zip). Optional for Ubuntu ISO builds.' } .required= ${ false } ></dees-input-text>
2026-05-07 17:44:31 +00:00
<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>
2026-05-07 19:49:56 +00:00
<div class="meta"> ${ data . imageKind || 'ubuntu-iso' } · ${ data . architecture } · ${ data . cloudlyUrl } </div>
2026-05-07 17:44:31 +00:00
</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 ) {
this . isLoading = true ;
try {
const response = await this . fireBaseOsRequest ( 'createBaseOsImageBuild' , {
build : {
architecture : formDataArg.architecture || 'amd64' ,
2026-05-07 19:49:56 +00:00
imageKind : formDataArg.imageKind || undefined ,
2026-05-07 17:44:31 +00:00
cloudlyUrl : formDataArg.cloudlyUrl || window . location . origin ,
hostname : formDataArg.hostname || undefined ,
sourceImageUrl : formDataArg.sourceImageUrl || undefined ,
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 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 ;
}
}