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) =>
hass.callApi<string[]>("GET", "config/config_entries/flow_handlers");
const fetchConfigFlowInProgress = (conn) =>
export const fetchConfigFlowInProgress = (
conn: Connection
): Promise<DataEntryFlowProgress[]> =>
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,

View File

@ -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 {
>
<div>
${this._loading ||
(this._step === null && this._handlers === undefined)
(this._step === null &&
this._handlers === undefined &&
this._handler === undefined)
? html`
<step-flow-loading
.label=${this.hass.localize(
@ -178,15 +221,22 @@ class DataEntryFlowDialog extends LitElement {
?rtl=${computeRTL(this.hass)}
></ha-icon-button>
${this._step === null
? // Show handler picker
html`
<step-flow-pick-handler
? this._handler
? html`<step-flow-pick-flow
.flowConfig=${this._params.flowConfig}
.hass=${this.hass}
.handlers=${this._handlers}
.showAdvanced=${this._params.showAdvanced}
></step-flow-pick-handler>
`
.handler=${this._handler}
.flowsInProgress=${this._flowsInProgress}
></step-flow-pick-flow>`
: // Show handler picker
html`
<step-flow-pick-handler
.hass=${this.hass}
.handlers=${this._handlers}
.showAdvanced=${this._params.showAdvanced}
@handler-picked=${this._handlerPicked}
></step-flow-pick-handler>
`
: this._step.type === "form"
? html`
<step-flow-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(
step: DataEntryFlowStep | undefined | Promise<DataEntryFlowStep>
): Promise<void> {
@ -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,

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 { 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 {
<h2>${this.hass.localize("ui.panel.config.integrations.new")}</h2>
<search-input
autofocus
.filter=${this.filter}
.filter=${this._filter}
@value-changed=${this._filterChanged}
.label=${this.hass.localize("ui.panel.config.integrations.search")}
></search-input>
@ -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);

View File

@ -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": {