mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-08 18:39:40 +00:00
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
539 lines
16 KiB
TypeScript
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;
|
|
}
|
|
}
|