Files
cloudly/ts_web/elements/views/baseos/index.ts
T

207 lines
7.0 KiB
TypeScript
Raw Normal View History

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">
<dees-panel .title=${'Create Image'} .subtitle=${'Build a Cloudly-bound BaseOS ISO'} .variant=${'outline'}>
<dees-form @formData=${(eventArg: CustomEvent) => this.createBuild((eventArg.detail as any).data)}>
<dees-input-dropdown
.key=${'architecture'}
.label=${'Architecture'}
.selectedOption=${'amd64'}
.options=${[
{ key: 'amd64', option: 'amd64 ISO', payload: null },
{ key: 'arm64', option: 'arm64 ISO', 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-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=${'Source ISO URL'} .description=${'Optional. Defaults to Ubuntu 24.04 through isocreator.'} .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.architecture} · ${data.cloudlyUrl}</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) {
this.isLoading = true;
try {
const response = await this.fireBaseOsRequest('createBaseOsImageBuild', {
build: {
architecture: formDataArg.architecture || 'amd64',
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;
}
}