diff --git a/src/data/data_entry_flow.ts b/src/data/data_entry_flow.ts index 5a7d8088a8..8be5367632 100644 --- a/src/data/data_entry_flow.ts +++ b/src/data/data_entry_flow.ts @@ -1,3 +1,4 @@ +import { Connection } from "home-assistant-js-websocket"; import { HaFormSchema } from "../components/ha-form/ha-form"; import { ConfigEntry } from "./config_entries"; @@ -74,3 +75,12 @@ export type DataEntryFlowStep = | DataEntryFlowStepCreateEntry | DataEntryFlowStepAbort | DataEntryFlowStepProgress; + +export const subscribeDataEntryFlowProgressed = ( + conn: Connection, + callback: (ev: DataEntryFlowProgressedEvent) => void +) => + conn.subscribeEvents( + callback, + "data_entry_flow_progressed" + ); diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index 243b879334..0ac9cfd073 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -10,7 +10,7 @@ import { TemplateResult, } from "lit"; import { customElement, state } from "lit/decorators"; -import { fireEvent } from "../../common/dom/fire_event"; +import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event"; import { computeRTL } from "../../common/util/compute_rtl"; import "../../components/ha-circular-progress"; import "../../components/ha-dialog"; @@ -22,10 +22,10 @@ import { subscribeAreaRegistry, } from "../../data/area_registry"; import { fetchConfigFlowInProgress } from "../../data/config_flow"; -import type { +import { DataEntryFlowProgress, - DataEntryFlowProgressedEvent, DataEntryFlowStep, + subscribeDataEntryFlowProgressed, } from "../../data/data_entry_flow"; import { DeviceRegistryEntry, @@ -34,7 +34,10 @@ import { import { haStyleDialog } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; import { showAlertDialog } from "../generic/show-dialog-box"; -import { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow"; +import { + DataEntryFlowDialogParams, + LoadingReason, +} from "./show-dialog-data-entry-flow"; import "./step-flow-abort"; import "./step-flow-create-entry"; import "./step-flow-external"; @@ -46,13 +49,19 @@ import "./step-flow-progress"; let instance = 0; +interface FlowUpdateEvent { + step?: DataEntryFlowStep; + stepPromise?: Promise; +} + declare global { // for fire event interface HASSDomEvents { - "flow-update": { - step?: DataEntryFlowStep; - stepPromise?: Promise; - }; + "flow-update": FlowUpdateEvent; + } + // for add event listener + interface HTMLElementEventMap { + "flow-update": HASSDomEvent; } } @@ -62,7 +71,7 @@ class DataEntryFlowDialog extends LitElement { @state() private _params?: DataEntryFlowDialogParams; - @state() private _loading = true; + @state() private _loading?: LoadingReason; private _instance = instance; @@ -86,6 +95,8 @@ class DataEntryFlowDialog extends LitElement { private _unsubDevices?: UnsubscribeFunc; + private _unsubDataEntryFlowProgressed?: Promise; + public async showDialog(params: DataEntryFlowDialogParams): Promise { this._params = params; this._instance = instance++; @@ -96,7 +107,7 @@ class DataEntryFlowDialog extends LitElement { } if (params.continueFlowId) { - this._loading = true; + this._loading = "loading_flow"; const curInstance = this._instance; let step: DataEntryFlowStep; try { @@ -124,7 +135,7 @@ class DataEntryFlowDialog extends LitElement { } this._processStep(step); - this._loading = false; + this._loading = undefined; return; } @@ -136,14 +147,13 @@ class DataEntryFlowDialog extends LitElement { // We only load the handlers once if (this._handlers === undefined) { - this._loading = true; + this._loading = "loading_handlers"; try { this._handlers = await params.flowConfig.getFlowHandlers(this.hass); } finally { - this._loading = false; + this._loading = undefined; } } - await this.updateComplete; } public closeDialog() { @@ -178,6 +188,12 @@ class DataEntryFlowDialog extends LitElement { this._unsubDevices(); this._unsubDevices = undefined; } + if (this._unsubDataEntryFlowProgressed) { + this._unsubDataEntryFlowProgressed.then((unsub) => { + unsub(); + }); + this._unsubDataEntryFlowProgressed = undefined; + } fireEvent(this, "dialog-closed", { dialog: this.localName }); } @@ -201,9 +217,11 @@ class DataEntryFlowDialog extends LitElement { this._handler === undefined) ? html` ` : this._step === undefined @@ -269,7 +287,13 @@ class DataEntryFlowDialog extends LitElement { ` : this._devices === undefined || this._areas === undefined ? // When it's a create entry result, we will fetch device & area registry - html` ` + html` + + ` : html` ( - async (ev) => { - if (ev.data.flow_id !== this._step?.flow_id) { - return; - } - const step = await this._params!.flowConfig.fetchFlow( - this.hass, - this._step?.flow_id - ); - this._processStep(step); - }, - "data_entry_flow_progressed" - ); this.addEventListener("flow-update", (ev) => { - const { step, stepPromise } = (ev as any).detail; + const { step, stepPromise } = ev.detail; this._processStep(step || stepPromise); }); } - protected updated(changedProps: PropertyValues) { - if ( - changedProps.has("_step") && - this._step && - this._step.type === "create_entry" - ) { + public willUpdate(changedProps: PropertyValues) { + super.willUpdate(changedProps); + if (!changedProps.has("_step") || !this._step) { + return; + } + if (["external", "progress"].includes(this._step.type)) { + // external and progress step will send update event from the backend, so we should subscribe to them + this._subscribeDataEntryFlowProgressed(); + } + if (this._step.type === "create_entry") { if (this._step.result && this._params!.flowConfig.loadDevicesAndAreas) { this._fetchDevices(this._step.result.entry_id); this._fetchAreas(); @@ -340,13 +355,16 @@ class DataEntryFlowDialog extends LitElement { } private async _checkFlowsInProgress(handler: string) { - this._loading = true; + this._loading = "loading_handlers"; + this._handler = handler; const flowsInProgress = ( await fetchConfigFlowInProgress(this.hass.connection) ).filter((flow) => flow.handler === handler); if (!flowsInProgress.length) { + // No flows in progress, create a new flow + this._loading = "loading_flow"; let step: DataEntryFlowStep; try { step = await this._params!.flowConfig.createFlow(this.hass, handler); @@ -362,14 +380,15 @@ class DataEntryFlowDialog extends LitElement { ), }); return; + } finally { + this._handler = undefined; } this._processStep(step); } else { this._step = null; - this._handler = handler; this._flowsInProgress = flowsInProgress; } - this._loading = false; + this._loading = undefined; } private _handlerPicked(ev) { @@ -380,11 +399,11 @@ class DataEntryFlowDialog extends LitElement { step: DataEntryFlowStep | undefined | Promise ): Promise { if (step instanceof Promise) { - this._loading = true; + this._loading = "loading_step"; try { this._step = await step; } finally { - this._loading = false; + this._loading = undefined; } return; } @@ -398,6 +417,23 @@ class DataEntryFlowDialog extends LitElement { this._step = step; } + private _subscribeDataEntryFlowProgressed() { + if (this._unsubDataEntryFlowProgressed) { + return; + } + this._unsubDataEntryFlowProgressed = subscribeDataEntryFlowProgressed( + this.hass.connection, + async (ev) => { + if (ev.data.flow_id !== this._step?.flow_id) { + return; + } + this._processStep( + this._params!.flowConfig.fetchFlow(this.hass, this._step?.flow_id) + ); + } + ); + } + static get styles(): CSSResultGroup { return [ haStyleDialog, diff --git a/src/dialogs/config-flow/show-dialog-config-flow.ts b/src/dialogs/config-flow/show-dialog-config-flow.ts index 86cf77be2d..92652cb7ed 100644 --- a/src/dialogs/config-flow/show-dialog-config-flow.ts +++ b/src/dialogs/config-flow/show-dialog-config-flow.ts @@ -178,4 +178,22 @@ export const showConfigFlowDialog = ( ` : ""; }, + + renderLoadingDescription(hass, reason, handler, step) { + if (!["loading_flow", "loading_step"].includes(reason)) { + return ""; + } + const domain = step?.handler || handler; + return hass.localize( + `ui.panel.config.integrations.config_flow.loading.${reason}`, + { + integration: domain + ? domainToName(hass.localize, domain) + : // when we are continuing a config flow, we only know the ID and not the domain + hass.localize( + "ui.panel.config.integrations.config_flow.loading.fallback_title" + ), + } + ); + }, }); diff --git a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts index 1f48db7e77..d6b70f7645 100644 --- a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts @@ -79,8 +79,21 @@ export interface FlowConfig { hass: HomeAssistant, step: DataEntryFlowStepProgress ): TemplateResult | ""; + + renderLoadingDescription( + hass: HomeAssistant, + loadingReason: LoadingReason, + handler?: string, + step?: DataEntryFlowStep | null + ): string; } +export type LoadingReason = + | "loading_handlers" + | "loading_flow" + | "loading_step" + | "loading_devices_areas"; + export interface DataEntryFlowDialogParams { startFlowHandler?: string; continueFlowId?: string; diff --git a/src/dialogs/config-flow/show-dialog-options-flow.ts b/src/dialogs/config-flow/show-dialog-options-flow.ts index dc508ee30e..7c7fde94dc 100644 --- a/src/dialogs/config-flow/show-dialog-options-flow.ts +++ b/src/dialogs/config-flow/show-dialog-options-flow.ts @@ -1,5 +1,6 @@ import { html } from "lit"; import { ConfigEntry } from "../../data/config_entries"; +import { domainToName } from "../../data/integration"; import { createOptionsFlow, deleteOptionsFlow, @@ -132,5 +133,14 @@ export const showOptionsFlowDialog = ( ` : ""; }, + + renderLoadingDescription(hass, reason) { + return ( + hass.localize(`component.${configEntry.domain}.options.loading`) || + hass.localize(`ui.dialogs.options_flow.loading.${reason}`, { + integration: domainToName(hass.localize, configEntry.domain), + }) + ); + }, } ); diff --git a/src/dialogs/config-flow/step-flow-abort.ts b/src/dialogs/config-flow/step-flow-abort.ts index b581717f8b..91e8211ad2 100644 --- a/src/dialogs/config-flow/step-flow-abort.ts +++ b/src/dialogs/config-flow/step-flow-abort.ts @@ -9,13 +9,11 @@ import { configFlowContentStyles } from "./styles"; @customElement("step-flow-abort") class StepFlowAbort extends LitElement { - public flowConfig!: FlowConfig; + @property({ attribute: false }) public flowConfig!: FlowConfig; - @property() - public hass!: HomeAssistant; + @property({ attribute: false }) public hass!: HomeAssistant; - @property() - private step!: DataEntryFlowStepAbort; + @property({ attribute: false }) public step!: DataEntryFlowStepAbort; protected render(): TemplateResult { return html` diff --git a/src/dialogs/config-flow/step-flow-create-entry.ts b/src/dialogs/config-flow/step-flow-create-entry.ts index 2d08078c88..a69a21e055 100644 --- a/src/dialogs/config-flow/step-flow-create-entry.ts +++ b/src/dialogs/config-flow/step-flow-create-entry.ts @@ -18,16 +18,13 @@ import { configFlowContentStyles } from "./styles"; @customElement("step-flow-create-entry") class StepFlowCreateEntry extends LitElement { - public flowConfig!: FlowConfig; + @property({ attribute: false }) public flowConfig!: FlowConfig; - @property() - public hass!: HomeAssistant; + @property({ attribute: false }) public hass!: HomeAssistant; - @property() - public step!: DataEntryFlowStepCreateEntry; + @property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry; - @property() - public devices!: DeviceRegistryEntry[]; + @property({ attribute: false }) public devices!: DeviceRegistryEntry[]; protected render(): TemplateResult { const localize = this.hass.localize; diff --git a/src/dialogs/config-flow/step-flow-external.ts b/src/dialogs/config-flow/step-flow-external.ts index 419d9bcde5..27261588da 100644 --- a/src/dialogs/config-flow/step-flow-external.ts +++ b/src/dialogs/config-flow/step-flow-external.ts @@ -8,13 +8,11 @@ import { configFlowContentStyles } from "./styles"; @customElement("step-flow-external") class StepFlowExternal extends LitElement { - public flowConfig!: FlowConfig; + @property({ attribute: false }) public flowConfig!: FlowConfig; - @property() - public hass!: HomeAssistant; + @property({ attribute: false }) public hass!: HomeAssistant; - @property() - private step!: DataEntryFlowStepExternal; + @property({ attribute: false }) public step!: DataEntryFlowStepExternal; protected render(): TemplateResult { const localize = this.hass.localize; diff --git a/src/dialogs/config-flow/step-flow-form.ts b/src/dialogs/config-flow/step-flow-form.ts index 403eccb3c8..75e2432666 100644 --- a/src/dialogs/config-flow/step-flow-form.ts +++ b/src/dialogs/config-flow/step-flow-form.ts @@ -8,7 +8,7 @@ import { PropertyValues, TemplateResult, } from "lit"; -import { customElement, property } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; import "../../components/ha-circular-progress"; import "../../components/ha-form/ha-form"; @@ -21,22 +21,17 @@ import { configFlowContentStyles } from "./styles"; @customElement("step-flow-form") class StepFlowForm extends LitElement { - public flowConfig!: FlowConfig; + @property({ attribute: false }) public flowConfig!: FlowConfig; - @property() - public step!: DataEntryFlowStepForm; + @property({ attribute: false }) public step!: DataEntryFlowStepForm; - @property() - public hass!: HomeAssistant; + @property({ attribute: false }) public hass!: HomeAssistant; - @property() - private _loading = false; + @state() private _loading = false; - @property() - private _stepData?: Record; + @state() private _stepData?: Record; - @property() - private _errorMsg?: string; + @state() private _errorMsg?: string; protected render(): TemplateResult { const step = this.step; diff --git a/src/dialogs/config-flow/step-flow-loading.ts b/src/dialogs/config-flow/step-flow-loading.ts index bc10d71c7a..bbfe1a8508 100644 --- a/src/dialogs/config-flow/step-flow-loading.ts +++ b/src/dialogs/config-flow/step-flow-loading.ts @@ -1,15 +1,32 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import "../../components/ha-circular-progress"; +import { DataEntryFlowStep } from "../../data/data_entry_flow"; +import { HomeAssistant } from "../../types"; +import { FlowConfig, LoadingReason } from "./show-dialog-data-entry-flow"; @customElement("step-flow-loading") class StepFlowLoading extends LitElement { - @property() public label?: string; + @property({ attribute: false }) public flowConfig!: FlowConfig; + + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public loadingReason!: LoadingReason; + + @property() public handler?: string; + + @property({ attribute: false }) public step?: DataEntryFlowStep | null; protected render(): TemplateResult { + const description = this.flowConfig.renderLoadingDescription( + this.hass, + this.loadingReason, + this.handler, + this.step + ); return html`
- ${this.label ? html`
${this.label}
` : ""} + ${description ? html`
${description}
` : ""}
`; diff --git a/src/dialogs/config-flow/step-flow-progress.ts b/src/dialogs/config-flow/step-flow-progress.ts index 3235ec55cb..726f5fc1a5 100644 --- a/src/dialogs/config-flow/step-flow-progress.ts +++ b/src/dialogs/config-flow/step-flow-progress.ts @@ -9,13 +9,14 @@ import { configFlowContentStyles } from "./styles"; @customElement("step-flow-progress") class StepFlowProgress extends LitElement { + @property({ attribute: false }) public flowConfig!: FlowConfig; @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) - private step!: DataEntryFlowStepProgress; + public step!: DataEntryFlowStepProgress; protected render(): TemplateResult { return html` diff --git a/src/translations/en.json b/src/translations/en.json index a652e70a64..8d70bafe0d 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -753,6 +753,10 @@ "form": { "header": "Options" }, + "loading": { + "loading_flow": "Please wait while the options for {integration} are being initialized", + "loading_step": "[%key:ui::panel::config::integrations::config_flow::loading::loading_step%]" + }, "success": { "description": "Options successfully saved." } @@ -2224,7 +2228,11 @@ "title": "We discovered these, want to set them up?", "new_flow": "No, set up an other instance of {integration}" }, - "loading_first_time": "Please wait while the integration is being installed", + "loading": { + "loading_flow": "Please wait while {integration} is being setup", + "loading_step": "Loading next step for {integration}", + "fallback_title": "the integration" + }, "error": "Error", "could_not_load": "Config flow could not be loaded", "not_loaded": "The integration could not be loaded, try to restart Home Assistant."