Show flows in progress when picking a handler (#8368)

This commit is contained in:
Bram Kragten 2021-02-22 20:06:18 +01:00 committed by GitHub
parent 6092af8de6
commit b7d4c40736
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 303 additions and 104 deletions

View File

@ -65,16 +65,18 @@ export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) =>
export const getConfigFlowHandlers = (hass: HomeAssistant) => export const getConfigFlowHandlers = (hass: HomeAssistant) =>
hass.callApi<string[]>("GET", "config/config_entries/flow_handlers"); hass.callApi<string[]>("GET", "config/config_entries/flow_handlers");
const fetchConfigFlowInProgress = (conn) => export const fetchConfigFlowInProgress = (
conn: Connection
): Promise<DataEntryFlowProgress[]> =>
conn.sendMessagePromise({ conn.sendMessagePromise({
type: "config_entries/flow/progress", type: "config_entries/flow/progress",
}); });
const subscribeConfigFlowInProgressUpdates = (conn, store) => const subscribeConfigFlowInProgressUpdates = (conn: Connection, store) =>
conn.subscribeEvents( conn.subscribeEvents(
debounce( debounce(
() => () =>
fetchConfigFlowInProgress(conn).then((flows) => fetchConfigFlowInProgress(conn).then((flows: DataEntryFlowProgress[]) =>
store.setState(flows, true) store.setState(flows, true)
), ),
500, 500,

View File

@ -22,7 +22,9 @@ import {
AreaRegistryEntry, AreaRegistryEntry,
subscribeAreaRegistry, subscribeAreaRegistry,
} from "../../data/area_registry"; } from "../../data/area_registry";
import { fetchConfigFlowInProgress } from "../../data/config_flow";
import type { import type {
DataEntryFlowProgress,
DataEntryFlowProgressedEvent, DataEntryFlowProgressedEvent,
DataEntryFlowStep, DataEntryFlowStep,
} from "../../data/data_entry_flow"; } from "../../data/data_entry_flow";
@ -41,6 +43,7 @@ import "./step-flow-form";
import "./step-flow-loading"; import "./step-flow-loading";
import "./step-flow-pick-handler"; import "./step-flow-pick-handler";
import "./step-flow-progress"; import "./step-flow-progress";
import "./step-flow-pick-flow";
let instance = 0; let instance = 0;
@ -76,6 +79,10 @@ class DataEntryFlowDialog extends LitElement {
@internalProperty() private _handlers?: string[]; @internalProperty() private _handlers?: string[];
@internalProperty() private _handler?: string;
@internalProperty() private _flowsInProgress?: DataEntryFlowProgress[];
private _unsubAreas?: UnsubscribeFunc; private _unsubAreas?: UnsubscribeFunc;
private _unsubDevices?: UnsubscribeFunc; private _unsubDevices?: UnsubscribeFunc;
@ -84,8 +91,45 @@ class DataEntryFlowDialog extends LitElement {
this._params = params; this._params = params;
this._instance = instance++; 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 // Create a new config flow. Show picker
if (!params.continueFlowId && !params.startFlowHandler) {
if (!params.flowConfig.getFlowHandlers) { if (!params.flowConfig.getFlowHandlers) {
throw new Error("No getFlowHandlers defined in flow config"); throw new Error("No getFlowHandlers defined in flow config");
} }
@ -101,42 +145,39 @@ class DataEntryFlowDialog extends LitElement {
} }
} }
await this.updateComplete; 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;
} }
public closeDialog() { public closeDialog() {
if (this._step) { if (!this._params) {
this._flowDone(); return;
} else if (this._step === null) { }
// Flow aborted during picking flow 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._step = undefined;
this._params = 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 }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
@ -156,7 +197,9 @@ class DataEntryFlowDialog extends LitElement {
> >
<div> <div>
${this._loading || ${this._loading ||
(this._step === null && this._handlers === undefined) (this._step === null &&
this._handlers === undefined &&
this._handler === undefined)
? html` ? html`
<step-flow-loading <step-flow-loading
.label=${this.hass.localize( .label=${this.hass.localize(
@ -178,13 +221,20 @@ class DataEntryFlowDialog extends LitElement {
?rtl=${computeRTL(this.hass)} ?rtl=${computeRTL(this.hass)}
></ha-icon-button> ></ha-icon-button>
${this._step === null ${this._step === null
? // Show handler picker ? this._handler
? html`<step-flow-pick-flow
.flowConfig=${this._params.flowConfig}
.hass=${this.hass}
.handler=${this._handler}
.flowsInProgress=${this._flowsInProgress}
></step-flow-pick-flow>`
: // Show handler picker
html` html`
<step-flow-pick-handler <step-flow-pick-handler
.flowConfig=${this._params.flowConfig}
.hass=${this.hass} .hass=${this.hass}
.handlers=${this._handlers} .handlers=${this._handlers}
.showAdvanced=${this._params.showAdvanced} .showAdvanced=${this._params.showAdvanced}
@handler-picked=${this._handlerPicked}
></step-flow-pick-handler> ></step-flow-pick-handler>
` `
: this._step.type === "form" : this._step.type === "form"
@ -291,6 +341,43 @@ class DataEntryFlowDialog extends LitElement {
}); });
} }
private async _checkFlowsInProgress(handler: string) {
this._loading = true;
const flowsInProgress = (
await fetchConfigFlowInProgress(this.hass.connection)
).filter((flow) => 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( private async _processStep(
step: DataEntryFlowStep | undefined | Promise<DataEntryFlowStep> step: DataEntryFlowStep | undefined | Promise<DataEntryFlowStep>
): Promise<void> { ): Promise<void> {
@ -305,7 +392,7 @@ class DataEntryFlowDialog extends LitElement {
} }
if (step === undefined) { if (step === undefined) {
this._flowDone(); this.closeDialog();
return; return;
} }
this._step = undefined; this._step = undefined;
@ -313,38 +400,6 @@ class DataEntryFlowDialog extends LitElement {
this._step = step; 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 { static get styles(): CSSResultArray {
return [ return [
haStyleDialog, haStyleDialog,

View File

@ -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`
<h2>
${this.hass.localize(
"ui.panel.config.integrations.config_flow.pick_flow_step.title"
)}
</h2>
<div>
${this.flowsInProgress.map(
(flow) => html` <paper-icon-item
@click=${this._flowInProgressPicked}
.flow=${flow}
>
<img
slot="item-icon"
loading="lazy"
src=${brandsUrl(flow.handler, "icon", true)}
referrerpolicy="no-referrer"
/>
<paper-item-body>
${localizeConfigFlowTitle(this.hass.localize, flow)}
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-icon-item>`
)}
<paper-item @click=${this._startNewFlowPicked} .handler=${this.handler}>
<paper-item-body>
${this.hass.localize(
"ui.panel.config.integrations.config_flow.pick_flow_step.new_flow",
"integration",
domainToName(this.hass.localize, this.handler)
)}
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</div>
`;
}
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;
}
}

View File

@ -22,7 +22,6 @@ import { domainToName } from "../../data/integration";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url"; import { brandsUrl } from "../../util/brands-url";
import { documentationUrl } from "../../util/documentation-url"; import { documentationUrl } from "../../util/documentation-url";
import { FlowConfig } from "./show-dialog-data-entry-flow";
import { configFlowContentStyles } from "./styles"; import { configFlowContentStyles } from "./styles";
interface HandlerObj { interface HandlerObj {
@ -30,17 +29,24 @@ interface HandlerObj {
slug: string; slug: string;
} }
declare global {
// for fire event
interface HASSDomEvents {
"handler-picked": {
handler: string;
};
}
}
@customElement("step-flow-pick-handler") @customElement("step-flow-pick-handler")
class StepFlowPickHandler extends LitElement { class StepFlowPickHandler extends LitElement {
public flowConfig!: FlowConfig;
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public handlers!: string[]; @property() public handlers!: string[];
@property() public showAdvanced?: boolean; @property() public showAdvanced?: boolean;
@internalProperty() private filter?: string; @internalProperty() private _filter?: string;
private _width?: number; private _width?: number;
@ -74,7 +80,7 @@ class StepFlowPickHandler extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
const handlers = this._getHandlers( const handlers = this._getHandlers(
this.handlers, this.handlers,
this.filter, this._filter,
this.hass.localize this.hass.localize
); );
@ -82,7 +88,7 @@ class StepFlowPickHandler extends LitElement {
<h2>${this.hass.localize("ui.panel.config.integrations.new")}</h2> <h2>${this.hass.localize("ui.panel.config.integrations.new")}</h2>
<search-input <search-input
autofocus autofocus
.filter=${this.filter} .filter=${this._filter}
@value-changed=${this._filterChanged} @value-changed=${this._filterChanged}
.label=${this.hass.localize("ui.panel.config.integrations.search")} .label=${this.hass.localize("ui.panel.config.integrations.search")}
></search-input> ></search-input>
@ -164,15 +170,12 @@ class StepFlowPickHandler extends LitElement {
} }
private async _filterChanged(e) { private async _filterChanged(e) {
this.filter = e.detail.value; this._filter = e.detail.value;
} }
private async _handlerPicked(ev) { private async _handlerPicked(ev) {
fireEvent(this, "flow-update", { fireEvent(this, "handler-picked", {
stepPromise: this.flowConfig.createFlow( handler: ev.currentTarget.handler.slug,
this.hass,
ev.currentTarget.handler.slug
),
}); });
} }
@ -195,6 +198,9 @@ class StepFlowPickHandler extends LitElement {
overflow: auto; overflow: auto;
max-height: 600px; max-height: 600px;
} }
h2 {
padding-right: 66px;
}
@media all and (max-height: 900px) { @media all and (max-height: 900px) {
div { div {
max-height: calc(100vh - 134px); max-height: calc(100vh - 134px);

View File

@ -2074,7 +2074,13 @@
"description": "This step requires you to visit an external website to be completed.", "description": "This step requires you to visit an external website to be completed.",
"open_site": "Open website" "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": { "users": {