From b7d4c4073687b7d9e334459f59c11a167875b488 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 22 Feb 2021 20:06:18 +0100 Subject: [PATCH] Show flows in progress when picking a handler (#8368) --- src/data/config_flow.ts | 8 +- .../config-flow/dialog-data-entry-flow.ts | 231 +++++++++++------- .../config-flow/step-flow-pick-flow.ts | 130 ++++++++++ .../config-flow/step-flow-pick-handler.ts | 30 ++- src/translations/en.json | 8 +- 5 files changed, 303 insertions(+), 104 deletions(-) create mode 100644 src/dialogs/config-flow/step-flow-pick-flow.ts diff --git a/src/data/config_flow.ts b/src/data/config_flow.ts index 6a3a8ca8db..04ecff9915 100644 --- a/src/data/config_flow.ts +++ b/src/data/config_flow.ts @@ -65,16 +65,18 @@ export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) => export const getConfigFlowHandlers = (hass: HomeAssistant) => hass.callApi("GET", "config/config_entries/flow_handlers"); -const fetchConfigFlowInProgress = (conn) => +export const fetchConfigFlowInProgress = ( + conn: Connection +): Promise => conn.sendMessagePromise({ type: "config_entries/flow/progress", }); -const subscribeConfigFlowInProgressUpdates = (conn, store) => +const subscribeConfigFlowInProgressUpdates = (conn: Connection, store) => conn.subscribeEvents( debounce( () => - fetchConfigFlowInProgress(conn).then((flows) => + fetchConfigFlowInProgress(conn).then((flows: DataEntryFlowProgress[]) => store.setState(flows, true) ), 500, diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index 206276ce50..76ac0d966b 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -22,7 +22,9 @@ import { AreaRegistryEntry, subscribeAreaRegistry, } from "../../data/area_registry"; +import { fetchConfigFlowInProgress } from "../../data/config_flow"; import type { + DataEntryFlowProgress, DataEntryFlowProgressedEvent, DataEntryFlowStep, } from "../../data/data_entry_flow"; @@ -41,6 +43,7 @@ import "./step-flow-form"; import "./step-flow-loading"; import "./step-flow-pick-handler"; import "./step-flow-progress"; +import "./step-flow-pick-flow"; let instance = 0; @@ -76,6 +79,10 @@ class DataEntryFlowDialog extends LitElement { @internalProperty() private _handlers?: string[]; + @internalProperty() private _handler?: string; + + @internalProperty() private _flowsInProgress?: DataEntryFlowProgress[]; + private _unsubAreas?: UnsubscribeFunc; private _unsubDevices?: UnsubscribeFunc; @@ -84,59 +91,93 @@ class DataEntryFlowDialog extends LitElement { this._params = params; this._instance = instance++; + if (params.startFlowHandler) { + this._checkFlowsInProgress(params.startFlowHandler); + return; + } + + if (params.continueFlowId) { + this._loading = true; + const curInstance = this._instance; + let step: DataEntryFlowStep; + try { + step = await params.flowConfig.fetchFlow( + this.hass, + params.continueFlowId + ); + } catch (err) { + this._step = undefined; + this._params = undefined; + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.integrations.config_flow.error" + ), + text: this.hass.localize( + "ui.panel.config.integrations.config_flow.could_not_load" + ), + }); + return; + } + + // Happens if second showDialog called + if (curInstance !== this._instance) { + return; + } + + this._processStep(step); + this._loading = false; + return; + } + // Create a new config flow. Show picker - if (!params.continueFlowId && !params.startFlowHandler) { - if (!params.flowConfig.getFlowHandlers) { - throw new Error("No getFlowHandlers defined in flow config"); + if (!params.flowConfig.getFlowHandlers) { + throw new Error("No getFlowHandlers defined in flow config"); + } + this._step = null; + + // We only load the handlers once + if (this._handlers === undefined) { + this._loading = true; + try { + this._handlers = await params.flowConfig.getFlowHandlers(this.hass); + } finally { + this._loading = false; } - this._step = null; - - // We only load the handlers once - if (this._handlers === undefined) { - this._loading = true; - try { - this._handlers = await params.flowConfig.getFlowHandlers(this.hass); - } finally { - this._loading = false; - } - } - await this.updateComplete; - return; } - - this._loading = true; - const curInstance = this._instance; - let step: DataEntryFlowStep; - try { - step = await (params.continueFlowId - ? params.flowConfig.fetchFlow(this.hass, params.continueFlowId) - : params.flowConfig.createFlow(this.hass, params.startFlowHandler!)); - } catch (err) { - this._step = undefined; - this._params = undefined; - showAlertDialog(this, { - title: "Error", - text: "Config flow could not be loaded", - }); - return; - } - - // Happens if second showDialog called - if (curInstance !== this._instance) { - return; - } - - this._processStep(step); - this._loading = false; + await this.updateComplete; } public closeDialog() { - if (this._step) { - this._flowDone(); - } else if (this._step === null) { - // Flow aborted during picking flow - this._step = undefined; - this._params = undefined; + if (!this._params) { + return; + } + const flowFinished = Boolean( + this._step && ["create_entry", "abort"].includes(this._step.type) + ); + + // If we created this flow, delete it now. + if (this._step && !flowFinished && !this._params.continueFlowId) { + this._params.flowConfig.deleteFlow(this.hass, this._step.flow_id); + } + + if (this._step !== null && this._params.dialogClosedCallback) { + this._params.dialogClosedCallback({ + flowFinished, + }); + } + + this._step = undefined; + this._params = undefined; + this._devices = undefined; + this._flowsInProgress = undefined; + this._handler = undefined; + if (this._unsubAreas) { + this._unsubAreas(); + this._unsubAreas = undefined; + } + if (this._unsubDevices) { + this._unsubDevices(); + this._unsubDevices = undefined; } fireEvent(this, "dialog-closed", { dialog: this.localName }); } @@ -156,7 +197,9 @@ class DataEntryFlowDialog extends LitElement { >
${this._loading || - (this._step === null && this._handlers === undefined) + (this._step === null && + this._handlers === undefined && + this._handler === undefined) ? html` ${this._step === null - ? // Show handler picker - html` - - ` + .handler=${this._handler} + .flowsInProgress=${this._flowsInProgress} + >` + : // Show handler picker + html` + + ` : this._step.type === "form" ? html` flow.handler === handler); + + if (!flowsInProgress.length) { + let step: DataEntryFlowStep; + try { + step = await this._params!.flowConfig.createFlow(this.hass, handler); + } catch (err) { + this._step = undefined; + this._params = undefined; + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.integrations.config_flow.error" + ), + text: this.hass.localize( + "ui.panel.config.integrations.config_flow.could_not_load" + ), + }); + return; + } + this._processStep(step); + } else { + this._step = null; + this._handler = handler; + this._flowsInProgress = flowsInProgress; + } + this._loading = false; + } + + private _handlerPicked(ev) { + this._checkFlowsInProgress(ev.detail.handler); + } + private async _processStep( step: DataEntryFlowStep | undefined | Promise ): Promise { @@ -305,7 +392,7 @@ class DataEntryFlowDialog extends LitElement { } if (step === undefined) { - this._flowDone(); + this.closeDialog(); return; } this._step = undefined; @@ -313,38 +400,6 @@ class DataEntryFlowDialog extends LitElement { this._step = step; } - private _flowDone(): void { - if (!this._params) { - return; - } - const flowFinished = Boolean( - this._step && ["create_entry", "abort"].includes(this._step.type) - ); - - // If we created this flow, delete it now. - if (this._step && !flowFinished && !this._params.continueFlowId) { - this._params.flowConfig.deleteFlow(this.hass, this._step.flow_id); - } - - if (this._params.dialogClosedCallback) { - this._params.dialogClosedCallback({ - flowFinished, - }); - } - - this._step = undefined; - this._params = undefined; - this._devices = undefined; - if (this._unsubAreas) { - this._unsubAreas(); - this._unsubAreas = undefined; - } - if (this._unsubDevices) { - this._unsubDevices(); - this._unsubDevices = undefined; - } - } - static get styles(): CSSResultArray { return [ haStyleDialog, diff --git a/src/dialogs/config-flow/step-flow-pick-flow.ts b/src/dialogs/config-flow/step-flow-pick-flow.ts new file mode 100644 index 0000000000..b75f2e3b38 --- /dev/null +++ b/src/dialogs/config-flow/step-flow-pick-flow.ts @@ -0,0 +1,130 @@ +import "@polymer/paper-item/paper-icon-item"; +import "@polymer/paper-item"; +import "@polymer/paper-item/paper-item-body"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import { fireEvent } from "../../common/dom/fire_event"; +import "../../components/ha-icon-next"; +import { localizeConfigFlowTitle } from "../../data/config_flow"; +import { DataEntryFlowProgress } from "../../data/data_entry_flow"; +import { domainToName } from "../../data/integration"; +import { HomeAssistant } from "../../types"; +import { brandsUrl } from "../../util/brands-url"; +import { FlowConfig } from "./show-dialog-data-entry-flow"; +import { configFlowContentStyles } from "./styles"; + +@customElement("step-flow-pick-flow") +class StepFlowPickFlow extends LitElement { + public flowConfig!: FlowConfig; + + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) + public flowsInProgress!: DataEntryFlowProgress[]; + + @property() public handler!: string; + + protected render(): TemplateResult { + return html` +

+ ${this.hass.localize( + "ui.panel.config.integrations.config_flow.pick_flow_step.title" + )} +

+ +
+ ${this.flowsInProgress.map( + (flow) => html` + + + + ${localizeConfigFlowTitle(this.hass.localize, flow)} + + + ` + )} + + + ${this.hass.localize( + "ui.panel.config.integrations.config_flow.pick_flow_step.new_flow", + "integration", + domainToName(this.hass.localize, this.handler) + )} + + + +
+ `; + } + + private _startNewFlowPicked(ev) { + this._startFlow(ev.currentTarget.handler); + } + + private _startFlow(handler: string) { + fireEvent(this, "flow-update", { + stepPromise: this.flowConfig.createFlow(this.hass, handler), + }); + } + + private _flowInProgressPicked(ev) { + const flow: DataEntryFlowProgress = ev.currentTarget.flow; + fireEvent(this, "flow-update", { + stepPromise: this.flowConfig.fetchFlow(this.hass, flow.flow_id), + }); + } + + static get styles(): CSSResult[] { + return [ + configFlowContentStyles, + css` + img { + width: 40px; + height: 40px; + } + ha-icon-next { + margin-right: 8px; + } + div { + overflow: auto; + max-height: 600px; + margin: 16px 0; + } + h2 { + padding-right: 66px; + } + @media all and (max-height: 900px) { + div { + max-height: calc(100vh - 134px); + } + } + paper-icon-item, + paper-item { + cursor: pointer; + margin-bottom: 4px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "step-flow-pick-flow": StepFlowPickFlow; + } +} diff --git a/src/dialogs/config-flow/step-flow-pick-handler.ts b/src/dialogs/config-flow/step-flow-pick-handler.ts index 774c9778b0..9c4da85cb4 100644 --- a/src/dialogs/config-flow/step-flow-pick-handler.ts +++ b/src/dialogs/config-flow/step-flow-pick-handler.ts @@ -22,7 +22,6 @@ import { domainToName } from "../../data/integration"; import { HomeAssistant } from "../../types"; import { brandsUrl } from "../../util/brands-url"; import { documentationUrl } from "../../util/documentation-url"; -import { FlowConfig } from "./show-dialog-data-entry-flow"; import { configFlowContentStyles } from "./styles"; interface HandlerObj { @@ -30,17 +29,24 @@ interface HandlerObj { slug: string; } +declare global { + // for fire event + interface HASSDomEvents { + "handler-picked": { + handler: string; + }; + } +} + @customElement("step-flow-pick-handler") class StepFlowPickHandler extends LitElement { - public flowConfig!: FlowConfig; - @property({ attribute: false }) public hass!: HomeAssistant; @property() public handlers!: string[]; @property() public showAdvanced?: boolean; - @internalProperty() private filter?: string; + @internalProperty() private _filter?: string; private _width?: number; @@ -74,7 +80,7 @@ class StepFlowPickHandler extends LitElement { protected render(): TemplateResult { const handlers = this._getHandlers( this.handlers, - this.filter, + this._filter, this.hass.localize ); @@ -82,7 +88,7 @@ class StepFlowPickHandler extends LitElement {

${this.hass.localize("ui.panel.config.integrations.new")}

@@ -164,15 +170,12 @@ class StepFlowPickHandler extends LitElement { } private async _filterChanged(e) { - this.filter = e.detail.value; + this._filter = e.detail.value; } private async _handlerPicked(ev) { - fireEvent(this, "flow-update", { - stepPromise: this.flowConfig.createFlow( - this.hass, - ev.currentTarget.handler.slug - ), + fireEvent(this, "handler-picked", { + handler: ev.currentTarget.handler.slug, }); } @@ -195,6 +198,9 @@ class StepFlowPickHandler extends LitElement { overflow: auto; max-height: 600px; } + h2 { + padding-right: 66px; + } @media all and (max-height: 900px) { div { max-height: calc(100vh - 134px); diff --git a/src/translations/en.json b/src/translations/en.json index c8da5c3475..4a93556872 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2074,7 +2074,13 @@ "description": "This step requires you to visit an external website to be completed.", "open_site": "Open website" }, - "loading_first_time": "Please wait while the integration is being installed" + "pick_flow_step": { + "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", + "error": "Error", + "could_not_load": "Config flow could not be loaded" } }, "users": {