Move picking new integration into dialog (#3110)

This commit is contained in:
Paulus Schoutsen 2019-04-24 12:51:41 -07:00 committed by GitHub
parent ff2f573dd0
commit 2f36304f06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 199 additions and 50 deletions

View File

@ -6,6 +6,12 @@ export interface FieldSchema {
optional: boolean; optional: boolean;
} }
export interface ConfigFlowProgress {
flow_id: string;
handler: string;
context: { [key: string]: any };
}
export interface ConfigFlowStepForm { export interface ConfigFlowStepForm {
type: "form"; type: "form";
flow_id: string; flow_id: string;
@ -62,3 +68,9 @@ export const handleConfigFlowStep = (
export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) => export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) =>
hass.callApi("DELETE", `config/config_entries/flow/${flowId}`); hass.callApi("DELETE", `config/config_entries/flow/${flowId}`);
export const getConfigFlowsInProgress = (hass: HomeAssistant) =>
hass.callApi<ConfigFlowProgress[]>("GET", "config/config_entries/flow");
export const getConfigFlowHandlers = (hass: HomeAssistant) =>
hass.callApi<string[]>("GET", "config/config_entries/flow_handlers");

View File

@ -23,13 +23,14 @@ import { HaPaperDialog } from "../../components/dialog/ha-paper-dialog";
import { haStyleDialog } from "../../resources/styles"; import { haStyleDialog } from "../../resources/styles";
import { import {
fetchConfigFlow, fetchConfigFlow,
createConfigFlow,
ConfigFlowStep, ConfigFlowStep,
deleteConfigFlow, deleteConfigFlow,
getConfigFlowHandlers,
} from "../../data/config_entries"; } from "../../data/config_entries";
import { PolymerChangedEvent } from "../../polymer-types"; import { PolymerChangedEvent } from "../../polymer-types";
import { HaConfigFlowParams } from "./show-dialog-config-flow"; import { HaConfigFlowParams } from "./show-dialog-config-flow";
import "./step-flow-pick-handler";
import "./step-flow-loading"; import "./step-flow-loading";
import "./step-flow-form"; import "./step-flow-form";
import "./step-flow-abort"; import "./step-flow-abort";
@ -39,6 +40,7 @@ import {
fetchDeviceRegistry, fetchDeviceRegistry,
} from "../../data/device_registry"; } from "../../data/device_registry";
import { AreaRegistryEntry, fetchAreaRegistry } from "../../data/area_registry"; import { AreaRegistryEntry, fetchAreaRegistry } from "../../data/area_registry";
import { HomeAssistant } from "../../types";
let instance = 0; let instance = 0;
@ -47,12 +49,15 @@ declare global {
interface HASSDomEvents { interface HASSDomEvents {
"flow-update": { "flow-update": {
step?: ConfigFlowStep; step?: ConfigFlowStep;
stepPromise?: Promise<ConfigFlowStep>;
}; };
} }
} }
@customElement("dialog-config-flow") @customElement("dialog-config-flow")
class ConfigFlowDialog extends LitElement { class ConfigFlowDialog extends LitElement {
public hass!: HomeAssistant;
@property() @property()
private _params?: HaConfigFlowParams; private _params?: HaConfigFlowParams;
@ -62,7 +67,11 @@ class ConfigFlowDialog extends LitElement {
private _instance = instance; private _instance = instance;
@property() @property()
private _step?: ConfigFlowStep; private _step:
| ConfigFlowStep
| undefined
// Null means we need to pick a config flow
| null;
@property() @property()
private _devices?: DeviceRegistryEntry[]; private _devices?: DeviceRegistryEntry[];
@ -70,25 +79,35 @@ class ConfigFlowDialog extends LitElement {
@property() @property()
private _areas?: AreaRegistryEntry[]; private _areas?: AreaRegistryEntry[];
@property()
private _handlers?: string[];
public async showDialog(params: HaConfigFlowParams): Promise<void> { public async showDialog(params: HaConfigFlowParams): Promise<void> {
this._params = params; this._params = params;
this._loading = true;
this._instance = instance++; this._instance = instance++;
const fetchStep = params.continueFlowId // Create a new config flow. Show picker
? fetchConfigFlow(params.hass, params.continueFlowId) if (!params.continueFlowId) {
: params.newFlowForHandler this._step = null;
? createConfigFlow(params.hass, params.newFlowForHandler)
: undefined;
if (!fetchStep) { // We only load the handlers once
throw new Error(`Pass in either continueFlowId or newFlorForHandler`); if (this._handlers === undefined) {
this._loading = true;
this.updateComplete.then(() => this._scheduleCenterDialog());
try {
this._handlers = await getConfigFlowHandlers(this.hass);
} finally {
this._loading = false;
}
}
await this.updateComplete;
this._scheduleCenterDialog();
return;
} }
this._loading = true;
const curInstance = this._instance; const curInstance = this._instance;
const step = await fetchConfigFlow(this.hass, params.continueFlowId);
await this.updateComplete;
const step = await fetchStep;
// Happens if second showDialog called // Happens if second showDialog called
if (curInstance !== this._instance) { if (curInstance !== this._instance) {
@ -99,7 +118,7 @@ class ConfigFlowDialog extends LitElement {
this._loading = false; this._loading = false;
// When the flow changes, center the dialog. // When the flow changes, center the dialog.
// Don't do it on each step or else the dialog keeps bouncing. // Don't do it on each step or else the dialog keeps bouncing.
setTimeout(() => this._dialog.center(), 0); this._scheduleCenterDialog();
} }
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
@ -113,7 +132,7 @@ class ConfigFlowDialog extends LitElement {
opened opened
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
> >
${this._loading ${this._loading || (this._step === null && this._handlers === undefined)
? html` ? html`
<step-flow-loading></step-flow-loading> <step-flow-loading></step-flow-loading>
` `
@ -121,18 +140,26 @@ class ConfigFlowDialog extends LitElement {
? // When we are going to next step, we render 1 round of empty ? // When we are going to next step, we render 1 round of empty
// to reset the element. // to reset the element.
"" ""
: this._step === null
? // Show handler picker
html`
<step-flow-pick-handler
.hass=${this.hass}
.handlers=${this._handlers}
></step-flow-pick-handler>
`
: this._step.type === "form" : this._step.type === "form"
? html` ? html`
<step-flow-form <step-flow-form
.step=${this._step} .step=${this._step}
.hass=${this._params.hass} .hass=${this.hass}
></step-flow-form> ></step-flow-form>
` `
: this._step.type === "abort" : this._step.type === "abort"
? html` ? html`
<step-flow-abort <step-flow-abort
.step=${this._step} .step=${this._step}
.hass=${this._params.hass} .hass=${this.hass}
></step-flow-abort> ></step-flow-abort>
` `
: this._devices === undefined || this._areas === undefined : this._devices === undefined || this._areas === undefined
@ -143,7 +170,7 @@ class ConfigFlowDialog extends LitElement {
: html` : html`
<step-flow-create-entry <step-flow-create-entry
.step=${this._step} .step=${this._step}
.hass=${this._params.hass} .hass=${this.hass}
.devices=${this._devices} .devices=${this._devices}
.areas=${this._areas} .areas=${this._areas}
></step-flow-create-entry> ></step-flow-create-entry>
@ -155,7 +182,8 @@ class ConfigFlowDialog extends LitElement {
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this.addEventListener("flow-update", (ev) => { this.addEventListener("flow-update", (ev) => {
this._processStep((ev as any).detail.step); const { step, stepPromise } = (ev as any).detail;
this._processStep(step || stepPromise);
}); });
} }
@ -170,6 +198,10 @@ class ConfigFlowDialog extends LitElement {
} }
} }
private _scheduleCenterDialog() {
setTimeout(() => this._dialog.center(), 0);
}
private get _dialog(): HaPaperDialog { private get _dialog(): HaPaperDialog {
return this.shadowRoot!.querySelector("ha-paper-dialog")!; return this.shadowRoot!.querySelector("ha-paper-dialog")!;
} }
@ -177,17 +209,29 @@ class ConfigFlowDialog extends LitElement {
private async _fetchDevices(configEntryId) { private async _fetchDevices(configEntryId) {
// Wait 5 seconds to give integrations time to find devices // Wait 5 seconds to give integrations time to find devices
await new Promise((resolve) => setTimeout(resolve, 5000)); await new Promise((resolve) => setTimeout(resolve, 5000));
const devices = await fetchDeviceRegistry(this._params!.hass); const devices = await fetchDeviceRegistry(this.hass);
this._devices = devices.filter((device) => this._devices = devices.filter((device) =>
device.config_entries.includes(configEntryId) device.config_entries.includes(configEntryId)
); );
} }
private async _fetchAreas() { private async _fetchAreas() {
this._areas = await fetchAreaRegistry(this._params!.hass); this._areas = await fetchAreaRegistry(this.hass);
} }
private async _processStep(step: ConfigFlowStep): Promise<void> { private async _processStep(
step: ConfigFlowStep | undefined | Promise<ConfigFlowStep>
): Promise<void> {
if (step instanceof Promise) {
this._loading = true;
try {
this._step = await step;
} finally {
this._loading = false;
}
return;
}
if (step === undefined) { if (step === undefined) {
this._flowDone(); this._flowDone();
return; return;
@ -206,8 +250,8 @@ class ConfigFlowDialog extends LitElement {
); );
// If we created this flow, delete it now. // If we created this flow, delete it now.
if (this._step && !flowFinished && this._params.newFlowForHandler) { if (this._step && !flowFinished && !this._params.continueFlowId) {
deleteConfigFlow(this._params.hass, this._step.flow_id); deleteConfigFlow(this.hass, this._step.flow_id);
} }
this._params.dialogClosedCallback({ this._params.dialogClosedCallback({
@ -221,8 +265,14 @@ class ConfigFlowDialog extends LitElement {
private _openedChanged(ev: PolymerChangedEvent<boolean>): void { private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
// Closed dialog by clicking on the overlay // Closed dialog by clicking on the overlay
if (this._step && !ev.detail.value) { if (!ev.detail.value) {
this._flowDone(); if (this._step) {
this._flowDone();
} else if (this._step === null) {
// Flow aborted during picking flow
this._step = undefined;
this._params = undefined;
}
} }
} }

View File

@ -1,10 +1,7 @@
import { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
export interface HaConfigFlowParams { export interface HaConfigFlowParams {
hass: HomeAssistant;
continueFlowId?: string; continueFlowId?: string;
newFlowForHandler?: string;
dialogClosedCallback: (params: { flowFinished: boolean }) => void; dialogClosedCallback: (params: { flowFinished: boolean }) => void;
} }

View File

@ -167,6 +167,8 @@ class StepFlowForm extends LitElement {
toSendData toSendData
); );
// make sure we're still showing the same step as when we
// fired off request.
if (!this.step || flowId !== this.step.flow_id) { if (!this.step || flowId !== this.step.flow_id) {
return; return;
} }

View File

@ -0,0 +1,67 @@
import {
LitElement,
TemplateResult,
html,
css,
customElement,
CSSResult,
} from "lit-element";
import "@polymer/paper-spinner/paper-spinner-lite";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import { HomeAssistant } from "../../types";
import { createConfigFlow } from "../../data/config_entries";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-icon-next";
@customElement("step-flow-pick-handler")
class StepFlowPickHandler extends LitElement {
public hass!: HomeAssistant;
public handlers!: string[];
protected render(): TemplateResult | void {
return html`
<h2>${this.hass.localize("ui.panel.config.integrations.new")}</h2>
<div>
${this.handlers.map(
(handler) =>
html`
<paper-item @click=${this._handlerPicked} .handler=${handler}>
<paper-item-body>
${this.hass.localize(`component.${handler}.config.title`)}
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
`
)}
</div>
`;
}
private async _handlerPicked(ev) {
fireEvent(this, "flow-update", {
stepPromise: createConfigFlow(this.hass, ev.currentTarget.handler),
});
}
static get styles(): CSSResult {
return css`
h2 {
padding-left: 16px;
}
div {
overflow: auto;
max-height: 600px;
}
paper-item {
cursor: pointer;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"step-flow-pick-handler": StepFlowPickHandler;
}
}

View File

@ -1,6 +1,7 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes"; import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-fab/paper-fab";
import "@polymer/paper-card/paper-card"; import "@polymer/paper-card/paper-card";
import "@polymer/iron-icon/iron-icon"; import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
@ -13,6 +14,7 @@ import "../../../layouts/hass-subpage";
import "../../../resources/ha-style"; import "../../../resources/ha-style";
import "../../../components/ha-icon-next"; import "../../../components/ha-icon-next";
import { computeRTL } from "../../../common/util/compute_rtl";
import "../ha-config-section"; import "../ha-config-section";
import EventsMixin from "../../../mixins/events-mixin"; import EventsMixin from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin"; import LocalizeMixin from "../../../mixins/localize-mixin";
@ -50,6 +52,28 @@ class HaConfigManagerDashboard extends LocalizeMixin(
color: var(--primary-text-color); color: var(--primary-text-color);
text-decoration: none; text-decoration: none;
} }
paper-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
}
paper-fab[is-wide] {
bottom: 24px;
right: 24px;
}
paper-fab[rtl] {
right: auto;
left: 16px;
}
paper-fab[rtl][is-wide] {
bottom: 24px;
right: auto;
left: 24px;
}
</style> </style>
<hass-subpage <hass-subpage
@ -119,23 +143,13 @@ class HaConfigManagerDashboard extends LocalizeMixin(
</paper-card> </paper-card>
</ha-config-section> </ha-config-section>
<ha-config-section> <paper-fab
<span slot="header" icon="hass:plus"
>[[localize('ui.panel.config.integrations.new')]]</span title="[[localize('ui.panel.config.integrations.new')]]"
> on-click="_createFlow"
<paper-card> is-wide$="[[isWide]]"
<template is="dom-repeat" items="[[handlers]]"> rtl$="[[rtl]]"
<div class="config-entry-row"> ></paper-fab>
<paper-item-body>
[[_computeIntegrationTitle(localize, item)]]
</paper-item-body>
<mwc-button on-click="_createFlow"
>[[localize('ui.panel.config.integrations.configure')]]</mwc-button
>
</div>
</template>
</paper-card>
</ha-config-section>
</hass-subpage> </hass-subpage>
`; `;
} }
@ -162,6 +176,12 @@ class HaConfigManagerDashboard extends LocalizeMixin(
progress: Array, progress: Array,
handlers: Array, handlers: Array,
rtl: {
type: Boolean,
reflectToAttribute: true,
computed: "_computeRTL(hass)",
},
}; };
} }
@ -170,17 +190,14 @@ class HaConfigManagerDashboard extends LocalizeMixin(
loadConfigFlowDialog(); loadConfigFlowDialog();
} }
_createFlow(ev) { _createFlow() {
showConfigFlowDialog(this, { showConfigFlowDialog(this, {
hass: this.hass,
newFlowForHandler: ev.model.item,
dialogClosedCallback: () => this.fire("hass-reload-entries"), dialogClosedCallback: () => this.fire("hass-reload-entries"),
}); });
} }
_continueFlow(ev) { _continueFlow(ev) {
showConfigFlowDialog(this, { showConfigFlowDialog(this, {
hass: this.hass,
continueFlowId: ev.model.item.flow_id, continueFlowId: ev.model.item.flow_id,
dialogClosedCallback: () => this.fire("hass-reload-entries"), dialogClosedCallback: () => this.fire("hass-reload-entries"),
}); });
@ -230,6 +247,10 @@ class HaConfigManagerDashboard extends LocalizeMixin(
_handleMoreInfo(ev) { _handleMoreInfo(ev) {
this.fire("hass-more-info", { entityId: ev.model.item.entity_id }); this.fire("hass-more-info", { entityId: ev.model.item.entity_id });
} }
_computeRTL(hass) {
return computeRTL(hass);
}
} }
customElements.define("ha-config-entries-dashboard", HaConfigManagerDashboard); customElements.define("ha-config-entries-dashboard", HaConfigManagerDashboard);