Change Data Entry Flow loading step description logic + cleanups (#9558)

* Change Data Entry Flow loading step description logic + cleanups

Fixes #6251

* Lint

* Address comment
This commit is contained in:
Bram Kragten 2021-07-15 21:07:25 +02:00 committed by GitHub
parent cc81239b9d
commit 1206e2d75f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 177 additions and 76 deletions

View File

@ -1,3 +1,4 @@
import { Connection } from "home-assistant-js-websocket";
import { HaFormSchema } from "../components/ha-form/ha-form"; import { HaFormSchema } from "../components/ha-form/ha-form";
import { ConfigEntry } from "./config_entries"; import { ConfigEntry } from "./config_entries";
@ -74,3 +75,12 @@ export type DataEntryFlowStep =
| DataEntryFlowStepCreateEntry | DataEntryFlowStepCreateEntry
| DataEntryFlowStepAbort | DataEntryFlowStepAbort
| DataEntryFlowStepProgress; | DataEntryFlowStepProgress;
export const subscribeDataEntryFlowProgressed = (
conn: Connection,
callback: (ev: DataEntryFlowProgressedEvent) => void
) =>
conn.subscribeEvents<DataEntryFlowProgressedEvent>(
callback,
"data_entry_flow_progressed"
);

View File

@ -10,7 +10,7 @@ import {
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, state } from "lit/decorators"; 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 { computeRTL } from "../../common/util/compute_rtl";
import "../../components/ha-circular-progress"; import "../../components/ha-circular-progress";
import "../../components/ha-dialog"; import "../../components/ha-dialog";
@ -22,10 +22,10 @@ import {
subscribeAreaRegistry, subscribeAreaRegistry,
} from "../../data/area_registry"; } from "../../data/area_registry";
import { fetchConfigFlowInProgress } from "../../data/config_flow"; import { fetchConfigFlowInProgress } from "../../data/config_flow";
import type { import {
DataEntryFlowProgress, DataEntryFlowProgress,
DataEntryFlowProgressedEvent,
DataEntryFlowStep, DataEntryFlowStep,
subscribeDataEntryFlowProgressed,
} from "../../data/data_entry_flow"; } from "../../data/data_entry_flow";
import { import {
DeviceRegistryEntry, DeviceRegistryEntry,
@ -34,7 +34,10 @@ import {
import { haStyleDialog } from "../../resources/styles"; import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { showAlertDialog } from "../generic/show-dialog-box"; 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-abort";
import "./step-flow-create-entry"; import "./step-flow-create-entry";
import "./step-flow-external"; import "./step-flow-external";
@ -46,13 +49,19 @@ import "./step-flow-progress";
let instance = 0; let instance = 0;
interface FlowUpdateEvent {
step?: DataEntryFlowStep;
stepPromise?: Promise<DataEntryFlowStep>;
}
declare global { declare global {
// for fire event // for fire event
interface HASSDomEvents { interface HASSDomEvents {
"flow-update": { "flow-update": FlowUpdateEvent;
step?: DataEntryFlowStep; }
stepPromise?: Promise<DataEntryFlowStep>; // for add event listener
}; interface HTMLElementEventMap {
"flow-update": HASSDomEvent<FlowUpdateEvent>;
} }
} }
@ -62,7 +71,7 @@ class DataEntryFlowDialog extends LitElement {
@state() private _params?: DataEntryFlowDialogParams; @state() private _params?: DataEntryFlowDialogParams;
@state() private _loading = true; @state() private _loading?: LoadingReason;
private _instance = instance; private _instance = instance;
@ -86,6 +95,8 @@ class DataEntryFlowDialog extends LitElement {
private _unsubDevices?: UnsubscribeFunc; private _unsubDevices?: UnsubscribeFunc;
private _unsubDataEntryFlowProgressed?: Promise<UnsubscribeFunc>;
public async showDialog(params: DataEntryFlowDialogParams): Promise<void> { public async showDialog(params: DataEntryFlowDialogParams): Promise<void> {
this._params = params; this._params = params;
this._instance = instance++; this._instance = instance++;
@ -96,7 +107,7 @@ class DataEntryFlowDialog extends LitElement {
} }
if (params.continueFlowId) { if (params.continueFlowId) {
this._loading = true; this._loading = "loading_flow";
const curInstance = this._instance; const curInstance = this._instance;
let step: DataEntryFlowStep; let step: DataEntryFlowStep;
try { try {
@ -124,7 +135,7 @@ class DataEntryFlowDialog extends LitElement {
} }
this._processStep(step); this._processStep(step);
this._loading = false; this._loading = undefined;
return; return;
} }
@ -136,14 +147,13 @@ class DataEntryFlowDialog extends LitElement {
// We only load the handlers once // We only load the handlers once
if (this._handlers === undefined) { if (this._handlers === undefined) {
this._loading = true; this._loading = "loading_handlers";
try { try {
this._handlers = await params.flowConfig.getFlowHandlers(this.hass); this._handlers = await params.flowConfig.getFlowHandlers(this.hass);
} finally { } finally {
this._loading = false; this._loading = undefined;
} }
} }
await this.updateComplete;
} }
public closeDialog() { public closeDialog() {
@ -178,6 +188,12 @@ class DataEntryFlowDialog extends LitElement {
this._unsubDevices(); this._unsubDevices();
this._unsubDevices = undefined; this._unsubDevices = undefined;
} }
if (this._unsubDataEntryFlowProgressed) {
this._unsubDataEntryFlowProgressed.then((unsub) => {
unsub();
});
this._unsubDataEntryFlowProgressed = undefined;
}
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
@ -201,9 +217,11 @@ class DataEntryFlowDialog extends LitElement {
this._handler === undefined) this._handler === undefined)
? html` ? html`
<step-flow-loading <step-flow-loading
.label=${this.hass.localize( .flowConfig=${this._params.flowConfig}
"ui.panel.config.integrations.config_flow.loading_first_time" .hass=${this.hass}
)} .loadingReason=${this._loading || "loading_handlers"}
.handler=${this._handler}
.step=${this._step}
></step-flow-loading> ></step-flow-loading>
` `
: this._step === undefined : this._step === undefined
@ -269,7 +287,13 @@ class DataEntryFlowDialog extends LitElement {
` `
: this._devices === undefined || this._areas === undefined : this._devices === undefined || this._areas === undefined
? // When it's a create entry result, we will fetch device & area registry ? // When it's a create entry result, we will fetch device & area registry
html` <step-flow-loading></step-flow-loading> ` html`
<step-flow-loading
.flowConfig=${this._params.flowConfig}
.hass=${this.hass}
loadingReason="loading_devices_areas"
></step-flow-loading>
`
: html` : html`
<step-flow-create-entry <step-flow-create-entry
.flowConfig=${this._params.flowConfig} .flowConfig=${this._params.flowConfig}
@ -287,31 +311,22 @@ class DataEntryFlowDialog extends LitElement {
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this.hass.connection.subscribeEvents<DataEntryFlowProgressedEvent>(
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) => { this.addEventListener("flow-update", (ev) => {
const { step, stepPromise } = (ev as any).detail; const { step, stepPromise } = ev.detail;
this._processStep(step || stepPromise); this._processStep(step || stepPromise);
}); });
} }
protected updated(changedProps: PropertyValues) { public willUpdate(changedProps: PropertyValues) {
if ( super.willUpdate(changedProps);
changedProps.has("_step") && if (!changedProps.has("_step") || !this._step) {
this._step && return;
this._step.type === "create_entry" }
) { 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) { if (this._step.result && this._params!.flowConfig.loadDevicesAndAreas) {
this._fetchDevices(this._step.result.entry_id); this._fetchDevices(this._step.result.entry_id);
this._fetchAreas(); this._fetchAreas();
@ -340,13 +355,16 @@ class DataEntryFlowDialog extends LitElement {
} }
private async _checkFlowsInProgress(handler: string) { private async _checkFlowsInProgress(handler: string) {
this._loading = true; this._loading = "loading_handlers";
this._handler = handler;
const flowsInProgress = ( const flowsInProgress = (
await fetchConfigFlowInProgress(this.hass.connection) await fetchConfigFlowInProgress(this.hass.connection)
).filter((flow) => flow.handler === handler); ).filter((flow) => flow.handler === handler);
if (!flowsInProgress.length) { if (!flowsInProgress.length) {
// No flows in progress, create a new flow
this._loading = "loading_flow";
let step: DataEntryFlowStep; let step: DataEntryFlowStep;
try { try {
step = await this._params!.flowConfig.createFlow(this.hass, handler); step = await this._params!.flowConfig.createFlow(this.hass, handler);
@ -362,14 +380,15 @@ class DataEntryFlowDialog extends LitElement {
), ),
}); });
return; return;
} finally {
this._handler = undefined;
} }
this._processStep(step); this._processStep(step);
} else { } else {
this._step = null; this._step = null;
this._handler = handler;
this._flowsInProgress = flowsInProgress; this._flowsInProgress = flowsInProgress;
} }
this._loading = false; this._loading = undefined;
} }
private _handlerPicked(ev) { private _handlerPicked(ev) {
@ -380,11 +399,11 @@ class DataEntryFlowDialog extends LitElement {
step: DataEntryFlowStep | undefined | Promise<DataEntryFlowStep> step: DataEntryFlowStep | undefined | Promise<DataEntryFlowStep>
): Promise<void> { ): Promise<void> {
if (step instanceof Promise) { if (step instanceof Promise) {
this._loading = true; this._loading = "loading_step";
try { try {
this._step = await step; this._step = await step;
} finally { } finally {
this._loading = false; this._loading = undefined;
} }
return; return;
} }
@ -398,6 +417,23 @@ class DataEntryFlowDialog extends LitElement {
this._step = step; 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 { static get styles(): CSSResultGroup {
return [ return [
haStyleDialog, haStyleDialog,

View File

@ -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"
),
}
);
},
}); });

View File

@ -79,8 +79,21 @@ export interface FlowConfig {
hass: HomeAssistant, hass: HomeAssistant,
step: DataEntryFlowStepProgress step: DataEntryFlowStepProgress
): TemplateResult | ""; ): 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 { export interface DataEntryFlowDialogParams {
startFlowHandler?: string; startFlowHandler?: string;
continueFlowId?: string; continueFlowId?: string;

View File

@ -1,5 +1,6 @@
import { html } from "lit"; import { html } from "lit";
import { ConfigEntry } from "../../data/config_entries"; import { ConfigEntry } from "../../data/config_entries";
import { domainToName } from "../../data/integration";
import { import {
createOptionsFlow, createOptionsFlow,
deleteOptionsFlow, 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),
})
);
},
} }
); );

View File

@ -9,13 +9,11 @@ import { configFlowContentStyles } from "./styles";
@customElement("step-flow-abort") @customElement("step-flow-abort")
class StepFlowAbort extends LitElement { class StepFlowAbort extends LitElement {
public flowConfig!: FlowConfig; @property({ attribute: false }) public flowConfig!: FlowConfig;
@property() @property({ attribute: false }) public hass!: HomeAssistant;
public hass!: HomeAssistant;
@property() @property({ attribute: false }) public step!: DataEntryFlowStepAbort;
private step!: DataEntryFlowStepAbort;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`

View File

@ -18,16 +18,13 @@ import { configFlowContentStyles } from "./styles";
@customElement("step-flow-create-entry") @customElement("step-flow-create-entry")
class StepFlowCreateEntry extends LitElement { class StepFlowCreateEntry extends LitElement {
public flowConfig!: FlowConfig; @property({ attribute: false }) public flowConfig!: FlowConfig;
@property() @property({ attribute: false }) public hass!: HomeAssistant;
public hass!: HomeAssistant;
@property() @property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry;
public step!: DataEntryFlowStepCreateEntry;
@property() @property({ attribute: false }) public devices!: DeviceRegistryEntry[];
public devices!: DeviceRegistryEntry[];
protected render(): TemplateResult { protected render(): TemplateResult {
const localize = this.hass.localize; const localize = this.hass.localize;

View File

@ -8,13 +8,11 @@ import { configFlowContentStyles } from "./styles";
@customElement("step-flow-external") @customElement("step-flow-external")
class StepFlowExternal extends LitElement { class StepFlowExternal extends LitElement {
public flowConfig!: FlowConfig; @property({ attribute: false }) public flowConfig!: FlowConfig;
@property() @property({ attribute: false }) public hass!: HomeAssistant;
public hass!: HomeAssistant;
@property() @property({ attribute: false }) public step!: DataEntryFlowStepExternal;
private step!: DataEntryFlowStepExternal;
protected render(): TemplateResult { protected render(): TemplateResult {
const localize = this.hass.localize; const localize = this.hass.localize;

View File

@ -8,7 +8,7 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-circular-progress"; import "../../components/ha-circular-progress";
import "../../components/ha-form/ha-form"; import "../../components/ha-form/ha-form";
@ -21,22 +21,17 @@ import { configFlowContentStyles } from "./styles";
@customElement("step-flow-form") @customElement("step-flow-form")
class StepFlowForm extends LitElement { class StepFlowForm extends LitElement {
public flowConfig!: FlowConfig; @property({ attribute: false }) public flowConfig!: FlowConfig;
@property() @property({ attribute: false }) public step!: DataEntryFlowStepForm;
public step!: DataEntryFlowStepForm;
@property() @property({ attribute: false }) public hass!: HomeAssistant;
public hass!: HomeAssistant;
@property() @state() private _loading = false;
private _loading = false;
@property() @state() private _stepData?: Record<string, any>;
private _stepData?: Record<string, any>;
@property() @state() private _errorMsg?: string;
private _errorMsg?: string;
protected render(): TemplateResult { protected render(): TemplateResult {
const step = this.step; const step = this.step;

View File

@ -1,15 +1,32 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "../../components/ha-circular-progress"; 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") @customElement("step-flow-loading")
class StepFlowLoading extends LitElement { 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 { protected render(): TemplateResult {
const description = this.flowConfig.renderLoadingDescription(
this.hass,
this.loadingReason,
this.handler,
this.step
);
return html` return html`
<div class="init-spinner"> <div class="init-spinner">
${this.label ? html` <div>${this.label}</div> ` : ""} ${description ? html`<div>${description}</div>` : ""}
<ha-circular-progress active></ha-circular-progress> <ha-circular-progress active></ha-circular-progress>
</div> </div>
`; `;

View File

@ -9,13 +9,14 @@ import { configFlowContentStyles } from "./styles";
@customElement("step-flow-progress") @customElement("step-flow-progress")
class StepFlowProgress extends LitElement { class StepFlowProgress extends LitElement {
@property({ attribute: false })
public flowConfig!: FlowConfig; public flowConfig!: FlowConfig;
@property({ attribute: false }) @property({ attribute: false })
public hass!: HomeAssistant; public hass!: HomeAssistant;
@property({ attribute: false }) @property({ attribute: false })
private step!: DataEntryFlowStepProgress; public step!: DataEntryFlowStepProgress;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`

View File

@ -753,6 +753,10 @@
"form": { "form": {
"header": "Options" "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": { "success": {
"description": "Options successfully saved." "description": "Options successfully saved."
} }
@ -2224,7 +2228,11 @@
"title": "We discovered these, want to set them up?", "title": "We discovered these, want to set them up?",
"new_flow": "No, set up an other instance of {integration}" "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", "error": "Error",
"could_not_load": "Config flow could not be loaded", "could_not_load": "Config flow could not be loaded",
"not_loaded": "The integration could not be loaded, try to restart Home Assistant." "not_loaded": "The integration could not be loaded, try to restart Home Assistant."