2024-01-15 19:42:15 +01:00
|
|
|
import {
|
|
|
|
|
customElement,
|
|
|
|
|
DeesElement,
|
|
|
|
|
type TemplateResult,
|
|
|
|
|
html,
|
|
|
|
|
property,
|
2026-04-16 14:21:16 +00:00
|
|
|
css,
|
2024-01-15 19:42:15 +01:00
|
|
|
} from '@design.estate/dees-element';
|
2024-01-15 12:57:49 +01:00
|
|
|
import { demoFunc } from './dees-updater.demo.js';
|
2026-04-16 14:21:16 +00:00
|
|
|
import {
|
|
|
|
|
DeesStepper,
|
|
|
|
|
type IStep,
|
|
|
|
|
type IStepProgressState,
|
|
|
|
|
} from '../../00group-layout/dees-stepper/dees-stepper.js';
|
|
|
|
|
|
|
|
|
|
export type TDeesUpdaterSuccessAction = 'close' | 'reload';
|
2021-02-13 21:52:36 +00:00
|
|
|
|
2026-04-16 14:21:16 +00:00
|
|
|
export interface IDeesUpdaterOptions {
|
|
|
|
|
currentVersion?: string;
|
|
|
|
|
updatedVersion?: string;
|
|
|
|
|
moreInfoUrl?: string;
|
|
|
|
|
changelogUrl?: string;
|
|
|
|
|
successAction?: TDeesUpdaterSuccessAction;
|
|
|
|
|
successDelayMs?: number;
|
|
|
|
|
successActionLabel?: string;
|
|
|
|
|
onSuccessAction?: () => Promise<void> | void;
|
|
|
|
|
}
|
2021-02-13 21:52:36 +00:00
|
|
|
|
|
|
|
|
declare global {
|
|
|
|
|
interface HTMLElementTagNameMap {
|
|
|
|
|
'dees-updater': DeesUpdater;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@customElement('dees-updater')
|
2022-01-24 01:39:47 +01:00
|
|
|
export class DeesUpdater extends DeesElement {
|
2024-01-15 12:57:49 +01:00
|
|
|
public static demo = demoFunc;
|
2026-01-27 10:57:42 +00:00
|
|
|
public static demoGroups = ['Utility'];
|
2024-01-15 12:57:49 +01:00
|
|
|
|
2026-04-16 14:21:16 +00:00
|
|
|
public static async createAndShow(optionsArg: IDeesUpdaterOptions = {}) {
|
2024-01-15 19:42:15 +01:00
|
|
|
const updater = new DeesUpdater();
|
2026-04-16 14:21:16 +00:00
|
|
|
updater.currentVersion = optionsArg.currentVersion ?? '';
|
|
|
|
|
updater.updatedVersion = optionsArg.updatedVersion ?? '';
|
|
|
|
|
updater.moreInfoUrl = optionsArg.moreInfoUrl ?? '';
|
|
|
|
|
updater.changelogUrl = optionsArg.changelogUrl ?? '';
|
|
|
|
|
updater.successAction = optionsArg.successAction ?? 'close';
|
|
|
|
|
updater.successDelayMs = optionsArg.successDelayMs ?? 10000;
|
|
|
|
|
updater.successActionLabel = optionsArg.successActionLabel ?? '';
|
|
|
|
|
updater.onSuccessAction = optionsArg.onSuccessAction ?? null;
|
2024-01-15 19:42:15 +01:00
|
|
|
document.body.appendChild(updater);
|
2026-04-16 14:21:16 +00:00
|
|
|
await updater.show();
|
2024-01-15 19:42:15 +01:00
|
|
|
return updater;
|
2024-01-15 12:57:49 +01:00
|
|
|
}
|
2021-02-13 21:52:36 +00:00
|
|
|
|
2021-11-26 20:06:09 +01:00
|
|
|
@property({
|
|
|
|
|
type: String,
|
|
|
|
|
})
|
2026-04-16 14:21:16 +00:00
|
|
|
accessor currentVersion = '';
|
2021-02-13 21:52:36 +00:00
|
|
|
|
2021-11-26 20:06:09 +01:00
|
|
|
@property({
|
|
|
|
|
type: String,
|
|
|
|
|
})
|
2026-04-16 14:21:16 +00:00
|
|
|
accessor updatedVersion = '';
|
2021-02-13 21:52:36 +00:00
|
|
|
|
2026-04-16 14:21:16 +00:00
|
|
|
@property({
|
|
|
|
|
type: String,
|
|
|
|
|
})
|
|
|
|
|
accessor moreInfoUrl = '';
|
2021-02-13 21:52:36 +00:00
|
|
|
|
2026-04-16 14:21:16 +00:00
|
|
|
@property({
|
|
|
|
|
type: String,
|
|
|
|
|
})
|
|
|
|
|
accessor changelogUrl = '';
|
2021-08-29 17:10:25 +02:00
|
|
|
|
2026-04-16 14:21:16 +00:00
|
|
|
@property({
|
|
|
|
|
type: String,
|
|
|
|
|
})
|
|
|
|
|
accessor successAction: TDeesUpdaterSuccessAction = 'close';
|
2021-08-29 17:10:25 +02:00
|
|
|
|
2026-04-16 14:21:16 +00:00
|
|
|
@property({
|
|
|
|
|
type: Number,
|
|
|
|
|
})
|
|
|
|
|
accessor successDelayMs = 10000;
|
2021-08-29 17:10:25 +02:00
|
|
|
|
2026-04-16 14:21:16 +00:00
|
|
|
@property({
|
|
|
|
|
type: String,
|
|
|
|
|
})
|
|
|
|
|
accessor successActionLabel = '';
|
|
|
|
|
|
|
|
|
|
private stepper: DeesStepper | null = null;
|
|
|
|
|
private progressStep: IStep | null = null;
|
|
|
|
|
private showPromise: Promise<void> | null = null;
|
|
|
|
|
private onSuccessAction: (() => Promise<void> | void) | null = null;
|
|
|
|
|
|
|
|
|
|
public static styles = [
|
|
|
|
|
css`
|
|
|
|
|
:host {
|
|
|
|
|
display: none;
|
2024-01-15 19:42:15 +01:00
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
];
|
2021-09-01 22:43:31 +02:00
|
|
|
|
|
|
|
|
public render(): TemplateResult {
|
2026-04-16 14:21:16 +00:00
|
|
|
return html``;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async connectedCallback(): Promise<void> {
|
|
|
|
|
await super.connectedCallback();
|
|
|
|
|
void this.show();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async show(): Promise<void> {
|
|
|
|
|
if (this.stepper?.isConnected) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.showPromise) {
|
|
|
|
|
return this.showPromise;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.showPromise = this.openStepperFlow();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this.showPromise;
|
|
|
|
|
} finally {
|
|
|
|
|
this.showPromise = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public updateProgress(progressStateArg: Partial<IStepProgressState>) {
|
|
|
|
|
if (!this.stepper || !this.progressStep) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.stepper.updateProgressStep(progressStateArg, this.progressStep);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public appendProgressLine(lineArg: string) {
|
|
|
|
|
if (!this.stepper || !this.progressStep) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.stepper.appendProgressStepLine(lineArg, this.progressStep);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public markUpdateError(messageArg: string) {
|
|
|
|
|
this.appendProgressLine(`Error: ${messageArg}`);
|
|
|
|
|
this.updateProgress({
|
|
|
|
|
indeterminate: false,
|
|
|
|
|
statusText: messageArg,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async markUpdateReady() {
|
|
|
|
|
if (!this.stepper || !this.progressStep) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.stepper.updateProgressStep(
|
|
|
|
|
{
|
|
|
|
|
percentage: 100,
|
|
|
|
|
indeterminate: false,
|
|
|
|
|
statusText: 'Update ready.',
|
|
|
|
|
},
|
|
|
|
|
this.progressStep,
|
|
|
|
|
);
|
|
|
|
|
this.stepper.appendProgressStepLine('Update ready', this.progressStep);
|
|
|
|
|
|
|
|
|
|
if (this.stepper.selectedStep === this.progressStep) {
|
|
|
|
|
this.stepper.goNext();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async destroy() {
|
|
|
|
|
const stepper = this.stepper;
|
|
|
|
|
this.stepper = null;
|
|
|
|
|
this.progressStep = null;
|
|
|
|
|
|
|
|
|
|
if (stepper?.isConnected) {
|
|
|
|
|
await stepper.destroy();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.parentElement) {
|
|
|
|
|
this.parentElement.removeChild(this);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async openStepperFlow() {
|
|
|
|
|
const { steps, progressStep } = this.createUpdaterSteps();
|
|
|
|
|
this.progressStep = progressStep;
|
|
|
|
|
this.stepper = await DeesStepper.createAndShow({
|
|
|
|
|
steps,
|
|
|
|
|
cancelable: false,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private createUpdaterSteps(): { steps: IStep[]; progressStep: IStep } {
|
|
|
|
|
const infoMenuOptions = this.getLinkMenuOptions();
|
|
|
|
|
const progressStep: IStep = {
|
|
|
|
|
title: 'Updating the application',
|
|
|
|
|
content: this.renderProgressContent(),
|
|
|
|
|
progressStep: {
|
|
|
|
|
label: this.getProgressLabel(),
|
|
|
|
|
percentage: 5,
|
|
|
|
|
indeterminate: true,
|
|
|
|
|
statusRows: 4,
|
|
|
|
|
statusText: 'Preparing update...',
|
|
|
|
|
terminalLines: ['Preparing update'],
|
|
|
|
|
},
|
|
|
|
|
menuOptions: infoMenuOptions.length > 0 ? infoMenuOptions : undefined,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const readyStep: IStep = {
|
|
|
|
|
title: this.updatedVersion ? `Version ${this.updatedVersion} ready` : 'Update ready',
|
|
|
|
|
content: this.renderReadyContent(),
|
|
|
|
|
progressStep: {
|
|
|
|
|
label: this.getSuccessCountdownLabel(this.getSuccessDelaySeconds()),
|
|
|
|
|
percentage: 0,
|
|
|
|
|
indeterminate: false,
|
|
|
|
|
showPercentage: false,
|
|
|
|
|
statusRows: 2,
|
|
|
|
|
statusText: this.getSuccessCountdownStatus(this.getSuccessDelaySeconds()),
|
|
|
|
|
},
|
|
|
|
|
validationFunc: async (stepper, _htmlElement, signal) => {
|
|
|
|
|
await this.runSuccessCountdown(stepper, readyStep, signal);
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
steps: [progressStep, readyStep],
|
|
|
|
|
progressStep,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getProgressLabel(): string {
|
|
|
|
|
if (this.currentVersion && this.updatedVersion) {
|
|
|
|
|
return `${this.currentVersion} -> ${this.updatedVersion}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.updatedVersion) {
|
|
|
|
|
return `Preparing ${this.updatedVersion}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 'Application update';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getSuccessDelaySeconds(): number {
|
|
|
|
|
return Math.max(1, Math.ceil(this.successDelayMs / 1000));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getSuccessActionDisplayLabel(): string {
|
|
|
|
|
if (this.successActionLabel) {
|
|
|
|
|
return this.successActionLabel;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.onSuccessAction) {
|
|
|
|
|
return 'Continuing automatically';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.successAction === 'reload') {
|
|
|
|
|
return 'Reloading application';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 'Closing updater';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getSuccessCountdownLabel(secondsArg: number): string {
|
|
|
|
|
return `${this.getSuccessActionDisplayLabel()} in ${secondsArg}s`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getSuccessCountdownStatus(secondsArg: number): string {
|
|
|
|
|
const secondLabel = secondsArg === 1 ? 'second' : 'seconds';
|
|
|
|
|
return `${this.getSuccessActionDisplayLabel()} in ${secondsArg} ${secondLabel}.`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getSuccessActionNowLabel(): string {
|
|
|
|
|
return `${this.getSuccessActionDisplayLabel()} now...`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getLinkMenuOptions() {
|
|
|
|
|
const menuOptions: Array<{ name: string; action: () => Promise<void> }> = [];
|
|
|
|
|
|
|
|
|
|
if (this.moreInfoUrl) {
|
|
|
|
|
menuOptions.push({
|
|
|
|
|
name: 'More info',
|
|
|
|
|
action: async () => {
|
|
|
|
|
this.openExternalUrl(this.moreInfoUrl);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.changelogUrl) {
|
|
|
|
|
menuOptions.push({
|
|
|
|
|
name: 'Changelog',
|
|
|
|
|
action: async () => {
|
|
|
|
|
this.openExternalUrl(this.changelogUrl);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return menuOptions;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderProgressContent(): TemplateResult {
|
2021-09-01 22:43:31 +02:00
|
|
|
return html`
|
2026-04-16 14:21:16 +00:00
|
|
|
<div style="display: grid; gap: 12px; color: var(--dees-color-text-secondary); line-height: 1.6;">
|
|
|
|
|
<p style="margin: 0; text-align: center;">
|
|
|
|
|
Downloading and applying the latest application release.
|
|
|
|
|
${this.currentVersion && this.updatedVersion
|
|
|
|
|
? html`Moving from <strong>${this.currentVersion}</strong> to <strong>${this.updatedVersion}</strong>.`
|
|
|
|
|
: this.updatedVersion
|
|
|
|
|
? html`Preparing <strong>${this.updatedVersion}</strong>.`
|
|
|
|
|
: ''}
|
|
|
|
|
</p>
|
|
|
|
|
<p style="margin: 0; text-align: center; font-size: 13px; color: var(--dees-color-text-muted);">
|
|
|
|
|
The updater advances automatically once the new build is installed and verified.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
2021-02-13 21:52:36 +00:00
|
|
|
`;
|
|
|
|
|
}
|
2021-09-01 21:48:22 +02:00
|
|
|
|
2026-04-16 14:21:16 +00:00
|
|
|
private renderReadyContent(): TemplateResult {
|
|
|
|
|
const successDelaySeconds = this.getSuccessDelaySeconds();
|
|
|
|
|
|
|
|
|
|
return html`
|
|
|
|
|
<div style="display: grid; gap: 12px; color: var(--dees-color-text-secondary); line-height: 1.6;">
|
|
|
|
|
<p style="margin: 0; text-align: center;">
|
|
|
|
|
${this.updatedVersion
|
|
|
|
|
? html`Version <strong>${this.updatedVersion}</strong> is ready to use.`
|
|
|
|
|
: 'The new version is ready to use.'}
|
|
|
|
|
</p>
|
|
|
|
|
<p style="margin: 0; text-align: center; font-size: 13px; color: var(--dees-color-text-muted);">
|
|
|
|
|
Configured next action: ${this.getSuccessActionDisplayLabel()}. It runs automatically in ${successDelaySeconds} seconds.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async runSuccessCountdown(
|
|
|
|
|
stepperArg: DeesStepper,
|
|
|
|
|
stepArg: IStep,
|
|
|
|
|
signal?: AbortSignal,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
const totalDuration = Math.max(1000, this.successDelayMs);
|
|
|
|
|
const startTime = Date.now();
|
|
|
|
|
|
|
|
|
|
while (!signal?.aborted) {
|
|
|
|
|
const elapsed = Math.min(totalDuration, Date.now() - startTime);
|
|
|
|
|
const remainingMilliseconds = Math.max(0, totalDuration - elapsed);
|
|
|
|
|
const remainingSeconds = Math.max(0, Math.ceil(remainingMilliseconds / 1000));
|
|
|
|
|
|
|
|
|
|
stepperArg.updateProgressStep(
|
|
|
|
|
{
|
|
|
|
|
label: remainingMilliseconds > 0
|
|
|
|
|
? this.getSuccessCountdownLabel(remainingSeconds)
|
|
|
|
|
: this.getSuccessActionNowLabel(),
|
|
|
|
|
percentage: (elapsed / totalDuration) * 100,
|
|
|
|
|
indeterminate: false,
|
|
|
|
|
showPercentage: false,
|
|
|
|
|
statusText: remainingMilliseconds > 0
|
|
|
|
|
? this.getSuccessCountdownStatus(remainingSeconds)
|
|
|
|
|
: this.getSuccessActionNowLabel(),
|
|
|
|
|
},
|
|
|
|
|
stepArg,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (remainingMilliseconds <= 0) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const completed = await this.waitForCountdownTick(100, signal);
|
|
|
|
|
if (!completed) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.runConfiguredSuccessAction();
|
2021-09-01 21:48:22 +02:00
|
|
|
}
|
2024-01-15 19:42:15 +01:00
|
|
|
|
2026-04-16 14:21:16 +00:00
|
|
|
private async waitForCountdownTick(timeoutArg: number, signal?: AbortSignal): Promise<boolean> {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
let completed = false;
|
|
|
|
|
|
|
|
|
|
const finish = (result: boolean) => {
|
|
|
|
|
if (completed) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
completed = true;
|
|
|
|
|
if (signal) {
|
|
|
|
|
signal.removeEventListener('abort', handleAbort);
|
|
|
|
|
}
|
|
|
|
|
resolve(result);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleAbort = () => {
|
|
|
|
|
window.clearTimeout(timeoutId);
|
|
|
|
|
finish(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const timeoutId = window.setTimeout(() => {
|
|
|
|
|
finish(true);
|
|
|
|
|
}, timeoutArg);
|
|
|
|
|
|
|
|
|
|
if (signal) {
|
|
|
|
|
signal.addEventListener('abort', handleAbort, { once: true });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async runConfiguredSuccessAction(): Promise<void> {
|
|
|
|
|
if (this.onSuccessAction) {
|
|
|
|
|
await this.onSuccessAction();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.successAction === 'reload') {
|
|
|
|
|
await this.destroy();
|
|
|
|
|
window.location.reload();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.destroy();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private openExternalUrl(urlArg: string) {
|
|
|
|
|
window.open(urlArg, '_blank', 'noopener,noreferrer');
|
|
|
|
|
}
|
2021-02-13 21:52:36 +00:00
|
|
|
}
|