Files
frontend/src/dialogs/config-flow/dialog-data-entry-flow.ts
Allen Porter dd49fd2788 Make the "Aborted: Reauthentication successful" more user friendly (#12530)
Replace the "Aborted" in the title with the integration name to make the user error
messages more user friendly. The message itself ("Reauthentication successful" or "Missing configuraiton, etc) error
message is descriptive enought that we can replace the title with the integration
name and still preserve the meeting. The advance is that this doesn't confuse users
who are surprised by it saying "Aborted" when things were successful

https://github.com/home-assistant/core/issues/47135
2022-05-01 11:02:32 -05:00

539 lines
16 KiB
TypeScript

import "@material/mwc-button";
import { mdiClose, mdiHelpCircle } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, state } from "lit/decorators";
import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event";
import "../../components/ha-circular-progress";
import "../../components/ha-dialog";
import "../../components/ha-icon-button";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../data/area_registry";
import { fetchConfigFlowInProgress } from "../../data/config_flow";
import {
DataEntryFlowProgress,
DataEntryFlowStep,
subscribeDataEntryFlowProgressed,
} from "../../data/data_entry_flow";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../data/device_registry";
import { fetchIntegrationManifest } from "../../data/integration";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import { showAlertDialog } from "../generic/show-dialog-box";
import {
DataEntryFlowDialogParams,
FlowHandlers,
LoadingReason,
} from "./show-dialog-data-entry-flow";
import "./step-flow-abort";
import "./step-flow-create-entry";
import "./step-flow-external";
import "./step-flow-form";
import "./step-flow-loading";
import "./step-flow-menu";
import "./step-flow-pick-flow";
import "./step-flow-pick-handler";
import "./step-flow-progress";
let instance = 0;
interface FlowUpdateEvent {
step?: DataEntryFlowStep;
stepPromise?: Promise<DataEntryFlowStep>;
}
declare global {
// for fire event
interface HASSDomEvents {
"flow-update": FlowUpdateEvent;
}
// for add event listener
interface HTMLElementEventMap {
"flow-update": HASSDomEvent<FlowUpdateEvent>;
}
}
@customElement("dialog-data-entry-flow")
class DataEntryFlowDialog extends LitElement {
public hass!: HomeAssistant;
@state() private _params?: DataEntryFlowDialogParams;
@state() private _loading?: LoadingReason;
private _instance = instance;
@state() private _step:
| DataEntryFlowStep
| undefined
// Null means we need to pick a config flow
| null;
@state() private _devices?: DeviceRegistryEntry[];
@state() private _areas?: AreaRegistryEntry[];
@state() private _handlers?: FlowHandlers;
@state() private _handler?: string;
@state() private _flowsInProgress?: DataEntryFlowProgress[];
private _unsubAreas?: UnsubscribeFunc;
private _unsubDevices?: UnsubscribeFunc;
private _unsubDataEntryFlowProgressed?: Promise<UnsubscribeFunc>;
public async showDialog(params: DataEntryFlowDialogParams): Promise<void> {
this._params = params;
this._instance = instance++;
if (params.startFlowHandler) {
this._checkFlowsInProgress(params.startFlowHandler);
return;
}
if (params.continueFlowId) {
this._loading = "loading_flow";
const curInstance = this._instance;
let step: DataEntryFlowStep;
try {
step = await params.flowConfig.fetchFlow(
this.hass,
params.continueFlowId
);
} catch (err: any) {
this.closeDialog();
let message = err.message || err.body || "Unknown error";
if (typeof message !== "string") {
message = JSON.stringify(message);
}
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"
)}: ${message}`,
});
return;
}
// Happens if second showDialog called
if (curInstance !== this._instance) {
return;
}
this._processStep(step);
this._loading = undefined;
return;
}
// Create a new config flow. Show picker
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 = "loading_handlers";
try {
this._handlers = await params.flowConfig.getFlowHandlers(this.hass);
} finally {
this._loading = undefined;
}
}
}
public closeDialog() {
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 && this._params.dialogClosedCallback) {
this._params.dialogClosedCallback({
flowFinished,
entryId:
"result" in this._step ? this._step.result?.entry_id : undefined,
});
}
this._loading = undefined;
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;
}
if (this._unsubDataEntryFlowProgressed) {
this._unsubDataEntryFlowProgressed.then((unsub) => {
unsub();
});
this._unsubDataEntryFlowProgressed = undefined;
}
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._params) {
return html``;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
hideActions
>
<div>
${this._loading ||
(this._step === null &&
this._handlers === undefined &&
this._handler === undefined)
? html`
<step-flow-loading
.flowConfig=${this._params.flowConfig}
.hass=${this.hass}
.loadingReason=${this._loading || "loading_handlers"}
.handler=${this._handler}
.step=${this._step}
></step-flow-loading>
`
: this._step === undefined
? // When we are going to next step, we render 1 round of empty
// to reset the element.
""
: html`
<div class="dialog-actions">
${([
"form",
"menu",
"external",
"progress",
"data_entry_flow_progressed",
].includes(this._step?.type as any) &&
this._params.manifest?.is_built_in) ||
this._params.manifest?.documentation
? html`
<a
href=${this._params.manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${this._params.manifest.domain}`
)
: this._params?.manifest?.documentation}
target="_blank"
rel="noreferrer noopener"
>
<ha-icon-button
.label=${this.hass.localize("ui.common.help")}
.path=${mdiHelpCircle}
>
</ha-icon-button
></a>
`
: ""}
<ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.integrations.config_flow.dismiss"
)}
.path=${mdiClose}
dialogAction="close"
></ha-icon-button>
</div>
${this._step === null
? 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`
<step-flow-pick-handler
.hass=${this.hass}
.handlers=${this._handlers}
.initialFilter=${this._params.searchQuery}
@handler-picked=${this._handlerPicked}
></step-flow-pick-handler>
`
: this._step.type === "form"
? html`
<step-flow-form
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
></step-flow-form>
`
: this._step.type === "external"
? html`
<step-flow-external
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
></step-flow-external>
`
: this._step.type === "abort"
? html`
<step-flow-abort
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
.domain=${this._step.handler}
></step-flow-abort>
`
: this._step.type === "progress"
? html`
<step-flow-progress
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
></step-flow-progress>
`
: this._step.type === "menu"
? html`
<step-flow-menu
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
></step-flow-menu>
`
: this._devices === undefined || this._areas === undefined
? // When it's a create entry result, we will fetch device & area registry
html`
<step-flow-loading
.flowConfig=${this._params.flowConfig}
.hass=${this.hass}
loadingReason="loading_devices_areas"
></step-flow-loading>
`
: html`
<step-flow-create-entry
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
.devices=${this._devices}
.areas=${this._areas}
></step-flow-create-entry>
`}
`}
</div>
</ha-dialog>
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this.addEventListener("flow-update", (ev) => {
const { step, stepPromise } = ev.detail;
this._processStep(step || stepPromise);
});
}
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!changedProps.has("_step") || !this._step) {
return;
}
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) {
this._fetchDevices(this._step.result.entry_id);
this._fetchAreas();
} else {
this._devices = [];
this._areas = [];
}
}
}
private async _fetchDevices(configEntryId) {
this._unsubDevices = subscribeDeviceRegistry(
this.hass.connection,
(devices) => {
this._devices = devices.filter((device) =>
device.config_entries.includes(configEntryId)
);
}
);
}
private async _fetchAreas() {
this._unsubAreas = subscribeAreaRegistry(this.hass.connection, (areas) => {
this._areas = areas;
});
}
private async _checkFlowsInProgress(handler: string) {
this._loading = "loading_handlers";
this._handler = handler;
const flowsInProgress = (
await fetchConfigFlowInProgress(this.hass.connection)
).filter((flow) => flow.handler === handler);
if (!flowsInProgress.length) {
// No flows in progress, create a new flow
this._loading = "loading_flow";
let step: DataEntryFlowStep;
try {
step = await this._params!.flowConfig.createFlow(this.hass, handler);
} catch (err: any) {
this.closeDialog();
const message =
err?.status_code === 404
? this.hass.localize(
"ui.panel.config.integrations.config_flow.no_config_flow"
)
: `${this.hass.localize(
"ui.panel.config.integrations.config_flow.could_not_load"
)}: ${err?.body?.message || err?.message}`;
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.error"
),
text: message,
});
return;
} finally {
this._handler = undefined;
}
this._processStep(step);
if (this._params!.manifest === undefined) {
try {
this._params!.manifest = await fetchIntegrationManifest(
this.hass,
this._params?.domain || step.handler
);
} catch (_) {
// No manifest
this._params!.manifest = null;
}
}
} else {
this._step = null;
this._flowsInProgress = flowsInProgress;
}
this._loading = undefined;
}
private _handlerPicked(ev) {
this._checkFlowsInProgress(ev.detail.handler);
}
private async _processStep(
step: DataEntryFlowStep | undefined | Promise<DataEntryFlowStep>
): Promise<void> {
if (step instanceof Promise) {
this._loading = "loading_step";
try {
this._step = await step;
} catch (err: any) {
this.closeDialog();
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.error"
),
text: err?.body?.message,
});
return;
} finally {
this._loading = undefined;
}
return;
}
if (step === undefined) {
this.closeDialog();
return;
}
this._step = undefined;
await this.updateComplete;
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 {
return [
haStyleDialog,
css`
ha-dialog {
--dialog-content-padding: 0;
}
.dialog-actions {
padding: 16px;
position: absolute;
top: 0;
right: 0;
}
:host-context([style*="direction: rtl;"]) .dialog-actions {
right: auto;
left: 0;
}
.dialog-actions > * {
color: var(--secondary-text-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-data-entry-flow": DataEntryFlowDialog;
}
}