diff --git a/src/common/integrations/protocolIntegrationPicked.ts b/src/common/integrations/protocolIntegrationPicked.ts index ff62cacbd9..4af63a34b1 100644 --- a/src/common/integrations/protocolIntegrationPicked.ts +++ b/src/common/integrations/protocolIntegrationPicked.ts @@ -1,11 +1,11 @@ import { html } from "lit"; import { getConfigEntries } from "../../data/config_entries"; +import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow"; import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box"; import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node"; import type { HomeAssistant } from "../../types"; import { documentationUrl } from "../../util/documentation-url"; import { isComponentLoaded } from "../config/is_component_loaded"; -import { fireEvent } from "../dom/fire_event"; import { navigate } from "../navigate"; export const protocolIntegrationPicked = async ( @@ -39,8 +39,8 @@ export const protocolIntegrationPicked = async ( "ui.panel.config.integrations.config_flow.proceed" ), confirm: () => { - fireEvent(element, "handler-picked", { - handler: "zwave_js", + showConfigFlowDialog(element, { + startFlowHandler: "zwave_js", }); }, }); @@ -75,8 +75,8 @@ export const protocolIntegrationPicked = async ( "ui.panel.config.integrations.config_flow.proceed" ), confirm: () => { - fireEvent(element, "handler-picked", { - handler: "zha", + showConfigFlowDialog(element, { + startFlowHandler: "zha", }); }, }); diff --git a/src/components/ha-dialog.ts b/src/components/ha-dialog.ts index b5b27e911f..3e8dba0946 100644 --- a/src/components/ha-dialog.ts +++ b/src/components/ha-dialog.ts @@ -91,7 +91,7 @@ export class HaDialog extends DialogBase { .header_button { position: absolute; right: 16px; - top: 10px; + top: 14px; text-decoration: none; color: inherit; } diff --git a/src/data/integrations.ts b/src/data/integrations.ts new file mode 100644 index 0000000000..70c799b154 --- /dev/null +++ b/src/data/integrations.ts @@ -0,0 +1,37 @@ +import { HomeAssistant } from "../types"; + +export type IotStandards = "z-wave" | "zigbee" | "homekit" | "matter"; + +export interface Integration { + name?: string; + config_flow?: boolean; + integrations?: Integrations; + iot_standards?: IotStandards[]; + is_built_in?: boolean; + iot_class?: string; +} + +export interface Integrations { + [domain: string]: Integration; +} + +export interface IntegrationDescriptions { + core: { + integration: Integrations; + hardware: Integrations; + helper: Integrations; + translated_name: string[]; + }; + custom: { + integration: Integrations; + hardware: Integrations; + helper: Integrations; + }; +} + +export const getIntegrationDescriptions = ( + hass: HomeAssistant +): Promise => + hass.callWS({ + type: "integration/descriptions", + }); diff --git a/src/data/supported_brands.ts b/src/data/supported_brands.ts index 8f0afcf884..62691ed275 100644 --- a/src/data/supported_brands.ts +++ b/src/data/supported_brands.ts @@ -1,6 +1,13 @@ -import { SupportedBrandObj } from "../dialogs/config-flow/step-flow-pick-handler"; import type { HomeAssistant } from "../types"; +export interface SupportedBrandObj { + name: string; + slug: string; + is_add?: boolean; + is_helper?: boolean; + supported_flows: string[]; +} + export type SupportedBrandHandler = Record; export const getSupportedBrands = (hass: HomeAssistant) => diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index 7654a0b506..3c148b7172 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -18,9 +18,7 @@ import { AreaRegistryEntry, subscribeAreaRegistry, } from "../../data/area_registry"; -import { fetchConfigFlowInProgress } from "../../data/config_flow"; import { - DataEntryFlowProgress, DataEntryFlowStep, subscribeDataEntryFlowProgressed, } from "../../data/data_entry_flow"; @@ -28,14 +26,12 @@ 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"; @@ -44,8 +40,6 @@ 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; @@ -86,12 +80,8 @@ class DataEntryFlowDialog extends LitElement { @state() private _areas?: AreaRegistryEntry[]; - @state() private _handlers?: FlowHandlers; - @state() private _handler?: string; - @state() private _flowsInProgress?: DataEntryFlowProgress[]; - private _unsubAreas?: UnsubscribeFunc; private _unsubDevices?: UnsubscribeFunc; @@ -102,15 +92,39 @@ class DataEntryFlowDialog extends LitElement { this._params = params; this._instance = instance++; - if (params.startFlowHandler) { - this._checkFlowsInProgress(params.startFlowHandler); - return; - } + const curInstance = this._instance; + let step: DataEntryFlowStep; - if (params.continueFlowId) { + if (params.startFlowHandler) { + this._loading = "loading_flow"; + this._handler = params.startFlowHandler; + try { + step = await this._params!.flowConfig.createFlow( + this.hass, + params.startFlowHandler + ); + } 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; + } + } else if (params.continueFlowId) { this._loading = "loading_flow"; - const curInstance = this._instance; - let step: DataEntryFlowStep; try { step = await params.flowConfig.fetchFlow( this.hass, @@ -132,32 +146,17 @@ class DataEntryFlowDialog extends LitElement { }); return; } - - // Happens if second showDialog called - if (curInstance !== this._instance) { - return; - } - - this._processStep(step); - this._loading = undefined; + } else { return; } - // Create a new config flow. Show picker - if (!params.flowConfig.getFlowHandlers) { - throw new Error("No getFlowHandlers defined in flow config"); + // Happens if second showDialog called + if (curInstance !== this._instance) { + return; } - 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; - } - } + this._processStep(step); + this._loading = undefined; } public closeDialog() { @@ -185,7 +184,6 @@ class DataEntryFlowDialog extends LitElement { this._step = undefined; this._params = undefined; this._devices = undefined; - this._flowsInProgress = undefined; this._handler = undefined; if (this._unsubAreas) { this._unsubAreas(); @@ -218,15 +216,12 @@ class DataEntryFlowDialog extends LitElement { hideActions >
- ${this._loading || - (this._step === null && - this._handlers === undefined && - this._handler === undefined) + ${this._loading || this._step === null ? html` @@ -273,24 +268,7 @@ class DataEntryFlowDialog extends LitElement { dialogAction="close" >
- ${this._step === null - ? this._handler - ? html`` - : // Show handler picker - html` - - ` - : this._step.type === "form" + ${this._step.type === "form" ? html` 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 ): Promise { diff --git a/src/dialogs/config-flow/show-dialog-config-flow.ts b/src/dialogs/config-flow/show-dialog-config-flow.ts index c1d56c6d73..84d332492c 100644 --- a/src/dialogs/config-flow/show-dialog-config-flow.ts +++ b/src/dialogs/config-flow/show-dialog-config-flow.ts @@ -3,11 +3,9 @@ import { createConfigFlow, deleteConfigFlow, fetchConfigFlow, - getConfigFlowHandlers, handleConfigFlowStep, } from "../../data/config_flow"; import { domainToName } from "../../data/integration"; -import { getSupportedBrands } from "../../data/supported_brands"; import { DataEntryFlowDialogParams, loadDataEntryFlowDialog, @@ -22,16 +20,6 @@ export const showConfigFlowDialog = ( ): void => showFlowDialog(element, dialogParams, { loadDevicesAndAreas: true, - getFlowHandlers: async (hass) => { - const [integrations, helpers, supportedBrands] = await Promise.all([ - getConfigFlowHandlers(hass, "integration"), - getConfigFlowHandlers(hass, "helper"), - getSupportedBrands(hass), - hass.loadBackendTranslation("title", undefined, true), - ]); - - return { integrations, helpers, supportedBrands }; - }, createFlow: async (hass, handler) => { const [step] = await Promise.all([ createConfigFlow(hass, handler), diff --git a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts index 1c5f407c69..5a6239cb3b 100644 --- a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts @@ -22,8 +22,6 @@ export interface FlowHandlers { export interface FlowConfig { loadDevicesAndAreas: boolean; - getFlowHandlers?: (hass: HomeAssistant) => Promise; - createFlow(hass: HomeAssistant, handler: string): Promise; fetchFlow(hass: HomeAssistant, flowId: string): Promise; diff --git a/src/dialogs/config-flow/step-flow-abort.ts b/src/dialogs/config-flow/step-flow-abort.ts index e1909d0bfe..08b8a46df1 100644 --- a/src/dialogs/config-flow/step-flow-abort.ts +++ b/src/dialogs/config-flow/step-flow-abort.ts @@ -12,10 +12,10 @@ import { DataEntryFlowStepAbort } from "../../data/data_entry_flow"; import { HomeAssistant } from "../../types"; import { showAddApplicationCredentialDialog } from "../../panels/config/application_credentials/show-dialog-add-application-credential"; import { configFlowContentStyles } from "./styles"; -import { showConfirmationDialog } from "../generic/show-dialog-box"; -import { domainToName } from "../../data/integration"; import { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow"; import { showConfigFlowDialog } from "./show-dialog-config-flow"; +import { domainToName } from "../../data/integration"; +import { showConfirmationDialog } from "../generic/show-dialog-box"; @customElement("step-flow-abort") class StepFlowAbort extends LitElement { @@ -56,11 +56,16 @@ class StepFlowAbort extends LitElement { private async _handleMissingCreds() { const confirm = await showConfirmationDialog(this, { title: this.hass.localize( + "ui.panel.config.integrations.config_flow.missing_credentials_title" + ), + text: this.hass.localize( "ui.panel.config.integrations.config_flow.missing_credentials", { integration: domainToName(this.hass.localize, this.domain), } ), + confirmText: this.hass.localize("ui.common.yes"), + dismissText: this.hass.localize("ui.common.no"), }); this._flowDone(); if (!confirm) { diff --git a/src/dialogs/config-flow/step-flow-pick-flow.ts b/src/dialogs/config-flow/step-flow-pick-flow.ts deleted file mode 100644 index fd24243a0b..0000000000 --- a/src/dialogs/config-flow/step-flow-pick-flow.ts +++ /dev/null @@ -1,130 +0,0 @@ -import "@polymer/paper-item"; -import "@polymer/paper-item/paper-icon-item"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-item/paper-item-body"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property } from "lit/decorators"; -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` -

- ${this.hass.localize( - "ui.panel.config.integrations.config_flow.pick_flow_step.title" - )} -

- -
- ${this.flowsInProgress.map( - (flow) => html` - - - - ${localizeConfigFlowTitle(this.hass.localize, flow)} - - - ` - )} - - - ${this.hass.localize( - "ui.panel.config.integrations.config_flow.pick_flow_step.new_flow", - "integration", - domainToName(this.hass.localize, this.handler) - )} - - - -
- `; - } - - 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(): CSSResultGroup { - 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-inline-end: 66px; - direction: var(--direction); - } - @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; - } -} diff --git a/src/dialogs/config-flow/step-flow-pick-handler.ts b/src/dialogs/config-flow/step-flow-pick-handler.ts deleted file mode 100644 index 21fa545da3..0000000000 --- a/src/dialogs/config-flow/step-flow-pick-handler.ts +++ /dev/null @@ -1,372 +0,0 @@ -import "@material/mwc-list/mwc-list"; -import "@material/mwc-list/mwc-list-item"; -import Fuse from "fuse.js"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { styleMap } from "lit/directives/style-map"; -import memoizeOne from "memoize-one"; -import { isComponentLoaded } from "../../common/config/is_component_loaded"; -import { fireEvent } from "../../common/dom/fire_event"; -import { protocolIntegrationPicked } from "../../common/integrations/protocolIntegrationPicked"; -import { navigate } from "../../common/navigate"; -import { caseInsensitiveStringCompare } from "../../common/string/compare"; -import { LocalizeFunc } from "../../common/translations/localize"; -import "../../components/ha-icon-next"; -import "../../components/search-input"; -import { domainToName } from "../../data/integration"; -import { haStyleScrollbar } from "../../resources/styles"; -import { HomeAssistant } from "../../types"; -import { brandsUrl } from "../../util/brands-url"; -import { documentationUrl } from "../../util/documentation-url"; -import { showConfirmationDialog } from "../generic/show-dialog-box"; -import { FlowHandlers } from "./show-dialog-data-entry-flow"; -import { configFlowContentStyles } from "./styles"; - -interface HandlerObj { - name: string; - slug: string; - is_add?: boolean; - is_helper?: boolean; -} - -export interface SupportedBrandObj extends HandlerObj { - supported_flows: string[]; -} - -declare global { - // for fire event - interface HASSDomEvents { - "handler-picked": { - handler: string; - }; - } -} - -@customElement("step-flow-pick-handler") -class StepFlowPickHandler extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public handlers!: FlowHandlers; - - @property() public initialFilter?: string; - - @state() private _filter?: string; - - private _width?: number; - - private _height?: number; - - private _filterHandlers = memoizeOne( - ( - h: FlowHandlers, - filter?: string, - _localize?: LocalizeFunc - ): [(HandlerObj | SupportedBrandObj)[], HandlerObj[]] => { - const integrations: (HandlerObj | SupportedBrandObj)[] = - h.integrations.map((handler) => ({ - name: domainToName(this.hass.localize, handler), - slug: handler, - })); - - for (const [domain, domainBrands] of Object.entries(h.supportedBrands)) { - for (const [slug, name] of Object.entries(domainBrands)) { - integrations.push({ - slug, - name, - supported_flows: [domain], - }); - } - } - - if (filter) { - const options: Fuse.IFuseOptions = { - keys: ["name", "slug"], - isCaseSensitive: false, - minMatchCharLength: 2, - threshold: 0.2, - }; - const helpers: HandlerObj[] = h.helpers.map((handler) => ({ - name: domainToName(this.hass.localize, handler), - slug: handler, - is_helper: true, - })); - return [ - new Fuse(integrations, options) - .search(filter) - .map((result) => result.item), - new Fuse(helpers, options) - .search(filter) - .map((result) => result.item), - ]; - } - return [ - integrations.sort((a, b) => - caseInsensitiveStringCompare(a.name, b.name) - ), - [], - ]; - } - ); - - protected render(): TemplateResult { - const [integrations, helpers] = this._getHandlers(); - - const addDeviceRows: HandlerObj[] = ["zha", "zwave_js"] - .filter((domain) => isComponentLoaded(this.hass, domain)) - .map((domain) => ({ - name: this.hass.localize( - `ui.panel.config.integrations.add_${domain}_device` - ), - slug: domain, - is_add: true, - })) - .sort((a, b) => caseInsensitiveStringCompare(a.name, b.name)); - - return html` -

${this.hass.localize("ui.panel.config.integrations.new")}

- - - ${addDeviceRows.length - ? html` - ${addDeviceRows.map((handler) => this._renderRow(handler))} - - ` - : ""} - ${integrations.length - ? integrations.map((handler) => this._renderRow(handler)) - : html` -

- ${this.hass.localize( - "ui.panel.config.integrations.note_about_integrations" - )}
- ${this.hass.localize( - "ui.panel.config.integrations.note_about_website_reference" - )}${this.hass.localize( - "ui.panel.config.integrations.home_assistant_website" - )}. -

- `} - ${helpers.length - ? html` - - ${helpers.map((handler) => this._renderRow(handler))} - ` - : ""} -
- `; - } - - private _renderRow(handler: HandlerObj) { - return html` - - - ${handler.name} ${handler.is_helper ? " (helper)" : ""} - ${handler.is_add ? "" : html``} - - `; - } - - public willUpdate(changedProps: PropertyValues): void { - super.willUpdate(changedProps); - if (this._filter === undefined && this.initialFilter !== undefined) { - this._filter = this.initialFilter; - } - if (this.initialFilter !== undefined && this._filter === "") { - this.initialFilter = undefined; - this._filter = ""; - this._width = undefined; - this._height = undefined; - } else if ( - this.hasUpdated && - changedProps.has("_filter") && - (!this._width || !this._height) - ) { - // Store the width and height so that when we search, box doesn't jump - const boundingRect = - this.shadowRoot!.querySelector("mwc-list")!.getBoundingClientRect(); - this._width = boundingRect.width; - this._height = boundingRect.height; - } - } - - protected firstUpdated(changedProps) { - super.firstUpdated(changedProps); - setTimeout( - () => this.shadowRoot!.querySelector("search-input")!.focus(), - 0 - ); - } - - private _getHandlers() { - return this._filterHandlers( - this.handlers, - this._filter, - this.hass.localize - ); - } - - private async _filterChanged(e) { - this._filter = e.detail.value; - } - - private async _handlerPicked(ev) { - const handler: HandlerObj | SupportedBrandObj = ev.currentTarget.handler; - - if (handler.is_add) { - this._handleAddPicked(handler.slug); - return; - } - - if (handler.is_helper) { - navigate(`/config/helpers/add?domain=${handler.slug}`); - // This closes dialog. - fireEvent(this, "flow-update"); - return; - } - - if ("supported_flows" in handler) { - const slug = handler.supported_flows[0]; - - showConfirmationDialog(this, { - text: this.hass.localize( - "ui.panel.config.integrations.config_flow.supported_brand_flow", - { - supported_brand: handler.name, - flow_domain_name: domainToName(this.hass.localize, slug), - } - ), - confirm: () => { - if (["zha", "zwave_js"].includes(slug)) { - this._handleAddPicked(slug); - return; - } - - fireEvent(this, "handler-picked", { - handler: slug, - }); - }, - }); - - return; - } - - fireEvent(this, "handler-picked", { - handler: handler.slug, - }); - } - - private async _handleAddPicked(slug: string): Promise { - await protocolIntegrationPicked(this, this.hass, slug); - // This closes dialog. - fireEvent(this, "flow-update"); - } - - private _maybeSubmit(ev: KeyboardEvent) { - if (ev.key !== "Enter") { - return; - } - - const handlers = this._getHandlers(); - - if (handlers.length > 0) { - fireEvent(this, "handler-picked", { - handler: handlers[0][0].slug, - }); - } - } - - static get styles(): CSSResultGroup { - return [ - configFlowContentStyles, - haStyleScrollbar, - css` - img { - width: 40px; - height: 40px; - } - search-input { - display: block; - margin: 16px 16px 0; - } - ha-icon-next { - margin-right: 8px; - } - mwc-list { - overflow: auto; - max-height: 600px; - } - .divider { - border-bottom-color: var(--divider-color); - } - h2 { - padding-inline-end: 66px; - direction: var(--direction); - } - @media all and (max-height: 900px) { - mwc-list { - max-height: calc(100vh - 134px); - } - } - p { - text-align: center; - padding: 16px; - margin: 0; - } - p > a { - color: var(--primary-color); - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "step-flow-pick-handler": StepFlowPickHandler; - } -} diff --git a/src/panels/config/integrations/dialog-add-integration.ts b/src/panels/config/integrations/dialog-add-integration.ts new file mode 100644 index 0000000000..474bbfa57a --- /dev/null +++ b/src/panels/config/integrations/dialog-add-integration.ts @@ -0,0 +1,656 @@ +import "@material/mwc-button"; +import "@material/mwc-list/mwc-list"; +import Fuse from "fuse.js"; +import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { customElement, state } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; +import memoizeOne from "memoize-one"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { protocolIntegrationPicked } from "../../../common/integrations/protocolIntegrationPicked"; +import { navigate } from "../../../common/navigate"; +import { caseInsensitiveStringCompare } from "../../../common/string/compare"; +import { LocalizeFunc } from "../../../common/translations/localize"; +import { createCloseHeading } from "../../../components/ha-dialog"; +import "../../../components/ha-icon-button-prev"; +import "../../../components/search-input"; +import { fetchConfigFlowInProgress } from "../../../data/config_flow"; +import { DataEntryFlowProgress } from "../../../data/data_entry_flow"; +import { + domainToName, + fetchIntegrationManifest, +} from "../../../data/integration"; +import { + getIntegrationDescriptions, + Integrations, +} from "../../../data/integrations"; +import { + getSupportedBrands, + SupportedBrandHandler, +} from "../../../data/supported_brands"; +import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../dialogs/generic/show-dialog-box"; +import { haStyleDialog, haStyleScrollbar } from "../../../resources/styles"; +import type { HomeAssistant } from "../../../types"; +import { documentationUrl } from "../../../util/documentation-url"; +import "./ha-domain-integrations"; +import "./ha-integration-list-item"; + +export interface IntegrationListItem { + name: string; + domain: string; + config_flow?: boolean; + is_helper?: boolean; + integrations?: string[]; + iot_standards?: string[]; + supported_flows?: string[]; + cloud?: boolean; + is_built_in?: boolean; + is_add?: boolean; +} + +@customElement("dialog-add-integration") +class AddIntegrationDialog extends LitElement { + public hass!: HomeAssistant; + + @state() private _integrations?: Integrations; + + @state() private _helpers?: Integrations; + + @state() private _supportedBrands?: Record; + + @state() private _initialFilter?: string; + + @state() private _filter?: string; + + @state() private _pickedBrand?: string; + + @state() private _flowsInProgress?: DataEntryFlowProgress[]; + + @state() private _open = false; + + @state() private _narrow = false; + + private _width?: number; + + private _height?: number; + + public showDialog(params): void { + this._open = true; + this._initialFilter = params.initialFilter; + this._narrow = matchMedia( + "all and (max-width: 450px), all and (max-height: 500px)" + ).matches; + } + + public closeDialog() { + this._open = false; + this._integrations = undefined; + this._helpers = undefined; + this._supportedBrands = undefined; + this._pickedBrand = undefined; + this._flowsInProgress = undefined; + this._filter = undefined; + this._width = undefined; + this._height = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + public willUpdate(changedProps: PropertyValues): void { + super.willUpdate(changedProps); + if (this._filter === undefined && this._initialFilter !== undefined) { + this._filter = this._initialFilter; + } + if (this._initialFilter !== undefined && this._filter === "") { + this._initialFilter = undefined; + this._filter = ""; + this._width = undefined; + this._height = undefined; + } else if ( + this.hasUpdated && + changedProps.has("_filter") && + (!this._width || !this._height) + ) { + // Store the width and height so that when we search, box doesn't jump + const boundingRect = + this.shadowRoot!.querySelector("mwc-list")?.getBoundingClientRect(); + this._width = boundingRect?.width; + this._height = boundingRect?.height; + } + } + + public updated(changedProps: PropertyValues) { + super.updated(changedProps); + if (changedProps.has("_open") && this._open) { + this._load(); + } + } + + private _filterIntegrations = memoizeOne( + ( + i: Integrations, + h: Integrations, + sb: Record, + components: HomeAssistant["config"]["components"], + localize: LocalizeFunc, + filter?: string + ): IntegrationListItem[] => { + const addDeviceRows: IntegrationListItem[] = ["zha", "zwave_js"] + .filter((domain) => components.includes(domain)) + .map((domain) => ({ + name: localize(`ui.panel.config.integrations.add_${domain}_device`), + domain, + config_flow: true, + is_built_in: true, + is_add: true, + })) + .sort((a, b) => caseInsensitiveStringCompare(a.name, b.name)); + + const integrations: IntegrationListItem[] = Object.entries(i) + .filter( + ([_domain, integration]) => + integration.config_flow || + integration.iot_standards || + integration.integrations + ) + .map(([domain, integration]) => ({ + domain, + name: integration.name || domainToName(localize, domain), + config_flow: integration.config_flow, + iot_standards: integration.iot_standards, + integrations: integration.integrations + ? Object.entries(integration.integrations).map( + ([dom, val]) => val.name || domainToName(localize, dom) + ) + : undefined, + is_built_in: integration.is_built_in !== false, + cloud: integration.iot_class?.startsWith("cloud_"), + })); + + for (const [domain, domainBrands] of Object.entries(sb)) { + const integration = i[domain]; + if ( + !integration.config_flow && + !integration.iot_standards && + !integration.integrations + ) { + continue; + } + for (const [slug, name] of Object.entries(domainBrands)) { + integrations.push({ + domain: slug, + name, + config_flow: integration.config_flow, + supported_flows: [domain], + is_built_in: true, + cloud: integration.iot_class?.startsWith("cloud_"), + }); + } + } + + if (filter) { + const options: Fuse.IFuseOptions = { + keys: [ + "name", + "domain", + "supported_flows", + "integrations", + "iot_standards", + ], + isCaseSensitive: false, + minMatchCharLength: 2, + threshold: 0.2, + }; + const helpers = Object.entries(h) + .filter( + ([_domain, integration]) => + integration.config_flow || + integration.iot_standards || + integration.integrations + ) + .map(([domain, integration]) => ({ + domain, + name: integration.name || domainToName(localize, domain), + config_flow: integration.config_flow, + is_helper: true, + is_built_in: integration.is_built_in !== false, + cloud: integration.iot_class?.startsWith("cloud_"), + })); + return [ + ...new Fuse(integrations, options) + .search(filter) + .map((result) => result.item), + ...new Fuse(helpers, options) + .search(filter) + .map((result) => result.item), + ]; + } + return [ + ...addDeviceRows, + ...integrations.sort((a, b) => + caseInsensitiveStringCompare(a.name || "", b.name || "") + ), + ]; + } + ); + + private _getIntegrations() { + return this._filterIntegrations( + this._integrations!, + this._helpers!, + this._supportedBrands!, + this.hass.config.components, + this.hass.localize, + this._filter + ); + } + + protected render(): TemplateResult { + if (!this._open) { + return html``; + } + const integrations = this._integrations + ? this._getIntegrations() + : undefined; + + return html` + ${this._pickedBrand + ? html`
+ +

+ ${this._calculateBrandHeading()} +

+
+ ${this._renderIntegration()}` + : this._renderAll(integrations)} +
`; + } + + private _calculateBrandHeading() { + const brand = this._integrations?.[this._pickedBrand!]; + if ( + brand?.iot_standards && + !brand.integrations && + !this._flowsInProgress?.length + ) { + return "What type of device is it?"; + } + if ( + !brand?.iot_standards && + !brand?.integrations && + this._flowsInProgress?.length + ) { + return "Want to add these discovered devices?"; + } + return "What do you want to add?"; + } + + private _renderIntegration(): TemplateResult { + return html``; + } + + private _renderAll(integrations?: IntegrationListItem[]): TemplateResult { + return html` + ${integrations + ? html` + + + ` + : html``} `; + } + + private _renderRow = (integration: IntegrationListItem) => { + if (!integration) { + return html``; + } + return html` + + + `; + }; + + private async _load() { + const [descriptions, supportedBrands] = await Promise.all([ + getIntegrationDescriptions(this.hass), + getSupportedBrands(this.hass), + ]); + for (const integration in descriptions.custom.integration) { + if ( + !Object.prototype.hasOwnProperty.call( + descriptions.custom.integration, + integration + ) + ) { + continue; + } + descriptions.custom.integration[integration].is_built_in = false; + } + this._integrations = { + ...descriptions.core.integration, + ...descriptions.custom.integration, + }; + for (const integration in descriptions.custom.helper) { + if ( + !Object.prototype.hasOwnProperty.call( + descriptions.custom.helper, + integration + ) + ) { + continue; + } + descriptions.custom.helper[integration].is_built_in = false; + } + this._helpers = { + ...descriptions.core.helper, + ...descriptions.custom.helper, + }; + this._supportedBrands = supportedBrands; + this.hass.loadBackendTranslation( + "title", + descriptions.core.translated_name, + true + ); + } + + private async _filterChanged(e) { + this._filter = e.detail.value; + } + + private _integrationPicked(ev) { + const listItem = ev.target.closest("ha-integration-list-item"); + const integration: IntegrationListItem = listItem.integration; + this._handleIntegrationPicked(integration); + } + + private async _handleIntegrationPicked(integration: IntegrationListItem) { + if ("supported_flows" in integration) { + const domain = integration.supported_flows![0]; + + showConfirmationDialog(this, { + text: this.hass.localize( + "ui.panel.config.integrations.config_flow.supported_brand_flow", + { + supported_brand: integration.name, + flow_domain_name: domainToName(this.hass.localize, domain), + } + ), + confirm: () => { + const supportIntegration = this._integrations?.[domain]; + this.closeDialog(); + if (["zha", "zwave_js"].includes(domain)) { + protocolIntegrationPicked(this, this.hass, domain); + return; + } + if (supportIntegration) { + this._handleIntegrationPicked({ + domain, + name: + supportIntegration.name || + domainToName(this.hass.localize, domain), + config_flow: supportIntegration.config_flow, + iot_standards: supportIntegration.iot_standards, + integrations: supportIntegration.integrations + ? Object.entries(supportIntegration.integrations).map( + ([dom, val]) => + val.name || domainToName(this.hass.localize, dom) + ) + : undefined, + }); + } else { + showAlertDialog(this, { + text: "Integration not found", + warning: true, + }); + } + }, + }); + + return; + } + + if (integration.is_add) { + protocolIntegrationPicked(this, this.hass, integration.domain); + this.closeDialog(); + return; + } + + if (integration.is_helper) { + this.closeDialog(); + navigate(`/config/helpers/add?domain=${integration.domain}`); + return; + } + + if (integration.integrations) { + this._fetchFlowsInProgress(Object.keys(integration.integrations)); + this._pickedBrand = integration.domain; + return; + } + + if ( + ["zha", "zwave_js"].includes(integration.domain) && + isComponentLoaded(this.hass, integration.domain) + ) { + this._pickedBrand = integration.domain; + return; + } + + if (integration.iot_standards) { + this._pickedBrand = integration.domain; + return; + } + + if (integration.config_flow) { + this._createFlow(integration); + return; + } + + const manifest = await fetchIntegrationManifest( + this.hass, + integration.domain + ); + this.closeDialog(); + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.integrations.config_flow.yaml_only_title" + ), + text: this.hass.localize( + "ui.panel.config.integrations.config_flow.yaml_only_text", + { + link: + manifest?.is_built_in || manifest?.documentation + ? html` + ${this.hass.localize( + "ui.panel.config.integrations.config_flow.documentation" + )} + ` + : this.hass.localize( + "ui.panel.config.integrations.config_flow.documentation" + ), + } + ), + }); + } + + private async _createFlow(integration: IntegrationListItem) { + const flowsInProgress = await this._fetchFlowsInProgress([ + integration.domain, + ]); + + if (flowsInProgress?.length) { + this._pickedBrand = integration.domain; + return; + } + + const manifest = await fetchIntegrationManifest( + this.hass, + integration.domain + ); + + this.closeDialog(); + + showConfigFlowDialog(this, { + startFlowHandler: integration.domain, + showAdvanced: this.hass.userData?.showAdvanced, + manifest, + }); + } + + private async _fetchFlowsInProgress(domains: string[]) { + const flowsInProgress = ( + await fetchConfigFlowInProgress(this.hass.connection) + ).filter((flow) => domains.includes(flow.handler)); + + if (flowsInProgress.length) { + this._flowsInProgress = flowsInProgress; + } + return flowsInProgress; + } + + private _maybeSubmit(ev: KeyboardEvent) { + if (ev.key !== "Enter") { + return; + } + + const integrations = this._getIntegrations(); + + if (integrations.length > 0) { + this._handleIntegrationPicked(integrations[0]); + } + } + + private _prevClicked() { + this._pickedBrand = undefined; + this._flowsInProgress = undefined; + } + + static styles = [ + haStyleScrollbar, + haStyleDialog, + css` + ha-dialog { + --dialog-content-padding: 0; + } + search-input { + display: block; + margin: 16px 16px 0; + } + .divider { + border-bottom-color: var(--divider-color); + } + h2 { + padding-inline-end: 66px; + direction: var(--direction); + } + p { + text-align: center; + padding: 16px; + margin: 0; + } + p > a { + color: var(--primary-color); + } + ha-circular-progress { + width: 100%; + display: flex; + justify-content: center; + margin: 24px 0; + } + lit-virtualizer { + contain: size layout !important; + } + ha-integration-list-item { + width: 100%; + } + ha-icon-button-prev { + color: var(--secondary-text-color); + position: absolute; + left: 16px; + top: 14px; + inset-inline-end: initial; + inset-inline-start: 16px; + direction: var(--direction); + } + .mdc-dialog__title { + margin: 0; + margin-bottom: 8px; + margin-left: 48px; + padding: 24px 24px 0 24px; + color: var(--mdc-dialog-heading-ink-color, rgba(0, 0, 0, 0.87)); + font-size: var(--mdc-typography-headline6-font-size, 1.25rem); + line-height: var(--mdc-typography-headline6-line-height, 2rem); + font-weight: var(--mdc-typography-headline6-font-weight, 500); + letter-spacing: var( + --mdc-typography-headline6-letter-spacing, + 0.0125em + ); + text-decoration: var( + --mdc-typography-headline6-text-decoration, + inherit + ); + text-transform: var(--mdc-typography-headline6-text-transform, inherit); + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-add-integration": AddIntegrationDialog; + } +} diff --git a/src/panels/config/integrations/ha-config-integrations.ts b/src/panels/config/integrations/ha-config-integrations.ts index daee2bee47..ac1caa367d 100644 --- a/src/panels/config/integrations/ha-config-integrations.ts +++ b/src/panels/config/integrations/ha-config-integrations.ts @@ -14,7 +14,6 @@ import { customElement, property, state } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; -import { fireEvent } from "../../../common/dom/fire_event"; import { protocolIntegrationPicked } from "../../../common/integrations/protocolIntegrationPicked"; import { navigate } from "../../../common/navigate"; import { caseInsensitiveStringCompare } from "../../../common/string/compare"; @@ -75,6 +74,7 @@ import "./ha-ignored-config-entry-card"; import "./ha-integration-card"; import type { HaIntegrationCard } from "./ha-integration-card"; import "./ha-integration-overflow-menu"; +import { showAddIntegrationDialog } from "./show-add-integration-dialog"; export interface ConfigEntryUpdatedEvent { entry: ConfigEntry; @@ -312,7 +312,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { undefined, true ); - this._fetchManifests(); if (this.route.path === "/add") { this._handleAdd(localizePromise); } @@ -599,7 +598,9 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { // Make a copy so we can keep track of previously loaded manifests // for discovered flows (which are not part of these results) const manifests = { ...this._manifests }; - for (const manifest of fetched) manifests[manifest.domain] = manifest; + for (const manifest of fetched) { + manifests[manifest.domain] = manifest; + } this._manifests = manifests; } @@ -630,15 +631,9 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { } private _createFlow() { - showConfigFlowDialog(this, { - searchQuery: this._filter, - dialogClosedCallback: () => { - this._handleFlowUpdated(); - }, - showAdvanced: this.showAdvanced, + showAddIntegrationDialog(this, { + initialFilter: this._filter, }); - // For config entries. Also loading config flow ones for added integration - this.hass.loadBackendTranslation("title", undefined, true); } private _handleMenuAction(ev: CustomEvent) { @@ -735,9 +730,13 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { protocolIntegrationPicked(this, this.hass, slug); return; } - - fireEvent(this, "handler-picked", { - handler: slug, + showConfigFlowDialog(this, { + dialogClosedCallback: () => { + this._handleFlowUpdated(); + }, + startFlowHandler: slug, + manifest: this._manifests[slug], + showAdvanced: this.hass.userData?.showAdvanced, }); }, }); diff --git a/src/panels/config/integrations/ha-domain-integrations.ts b/src/panels/config/integrations/ha-domain-integrations.ts new file mode 100644 index 0000000000..d54f9e2872 --- /dev/null +++ b/src/panels/config/integrations/ha-domain-integrations.ts @@ -0,0 +1,225 @@ +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { protocolIntegrationPicked } from "../../../common/integrations/protocolIntegrationPicked"; +import { localizeConfigFlowTitle } from "../../../data/config_flow"; +import { DataEntryFlowProgress } from "../../../data/data_entry_flow"; +import { + domainToName, + fetchIntegrationManifest, +} from "../../../data/integration"; +import { Integration } from "../../../data/integrations"; +import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; +import { haStyle } from "../../../resources/styles"; +import { HomeAssistant } from "../../../types"; +import { brandsUrl } from "../../../util/brands-url"; +import "./ha-integration-list-item"; + +const standardToDomain = { zigbee: "zha", "z-wave": "zwave_js" } as const; + +@customElement("ha-domain-integrations") +class HaDomainIntegrations extends LitElement { + public hass!: HomeAssistant; + + @property() public domain!: string; + + @property({ attribute: false }) public integration!: Integration; + + @property({ attribute: false }) + public flowsInProgress?: DataEntryFlowProgress[]; + + protected render() { + return html` + ${this.flowsInProgress?.length + ? html`

We discovered the following:

+ ${this.flowsInProgress.map( + (flow) => html` + + ${localizeConfigFlowTitle(this.hass.localize, flow)} + + ` + )}` + : ""} + ${this.integration?.iot_standards + ? this.integration.iot_standards.map((standard) => { + const domain: string = standardToDomain[standard] || standard; + return html` + + ${this.hass.localize( + `ui.panel.config.integrations.add_${domain}_device` + )} + + `; + }) + : ""} + ${this.integration?.integrations + ? Object.entries(this.integration.integrations).map( + ([dom, val]) => html` + ` + ) + : ""} + ${["zha", "zwave_js"].includes(this.domain) + ? html` + + ${this.hass.localize( + `ui.panel.config.integrations.add_${this.domain}_device` + )} + + ` + : ""} + ${this.integration?.config_flow + ? html`${this.flowsInProgress?.length + ? html` + Setup another instance of + ${this.integration.name || + domainToName(this.hass.localize, this.domain)} + + ` + : html` + `}` + : ""} + `; + } + + private async _integrationPicked(ev) { + const domain = ev.currentTarget.domain; + const root = this.getRootNode(); + showConfigFlowDialog( + root instanceof ShadowRoot ? (root.host as HTMLElement) : this, + { + startFlowHandler: domain, + showAdvanced: this.hass.userData?.showAdvanced, + manifest: await fetchIntegrationManifest(this.hass, domain), + } + ); + fireEvent(this, "close-dialog"); + } + + private async _flowInProgressPicked(ev) { + const flow: DataEntryFlowProgress = ev.currentTarget.flow; + const root = this.getRootNode(); + showConfigFlowDialog( + root instanceof ShadowRoot ? (root.host as HTMLElement) : this, + { + continueFlowId: flow.flow_id, + showAdvanced: this.hass.userData?.showAdvanced, + manifest: await fetchIntegrationManifest(this.hass, flow.handler), + } + ); + fireEvent(this, "close-dialog"); + } + + private _standardPicked(ev) { + const domain = ev.currentTarget.domain; + const root = this.getRootNode(); + fireEvent(this, "close-dialog"); + protocolIntegrationPicked( + root instanceof ShadowRoot ? (root.host as HTMLElement) : this, + this.hass, + domain + ); + } + + static styles = [ + haStyle, + css` + :host { + display: block; + } + h3 { + margin: 0 24px; + color: var(--primary-text-color); + font-size: 14px; + } + img { + width: 40px; + height: 40px; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-domain-integrations": HaDomainIntegrations; + } +} diff --git a/src/panels/config/integrations/ha-integration-list-item.ts b/src/panels/config/integrations/ha-integration-list-item.ts new file mode 100644 index 0000000000..26f28b5051 --- /dev/null +++ b/src/panels/config/integrations/ha-integration-list-item.ts @@ -0,0 +1,151 @@ +import { + GraphicType, + ListItemBase, +} from "@material/mwc-list/mwc-list-item-base"; +import { styles } from "@material/mwc-list/mwc-list-item.css"; +import { mdiCloudOutline, mdiCodeBraces, mdiPackageVariant } from "@mdi/js"; +import { css, CSSResultGroup, html } from "lit"; +import { classMap } from "lit/directives/class-map"; +import { customElement, property } from "lit/decorators"; +import { domainToName } from "../../../data/integration"; +import { HomeAssistant } from "../../../types"; +import { brandsUrl } from "../../../util/brands-url"; +import { IntegrationListItem } from "./dialog-add-integration"; + +@customElement("ha-integration-list-item") +export class HaIntegrationListItem extends ListItemBase { + public hass!: HomeAssistant; + + @property({ attribute: false }) public integration?: IntegrationListItem; + + @property({ type: String, reflect: true }) graphic: GraphicType = "medium"; + + @property({ type: Boolean }) hasMeta = true; + + renderSingleLine() { + if (!this.integration) { + return html``; + } + return html`${this.integration.name || + domainToName(this.hass.localize, this.integration.domain)} + ${this.integration.is_helper ? " (helper)" : ""}`; + } + + protected renderGraphic() { + if (!this.integration) { + return html``; + } + const graphicClasses = { + multi: this.multipleGraphics, + }; + + return html` + + `; + } + + protected renderMeta() { + if (!this.integration) { + return html``; + } + return html` + ${!this.integration.config_flow && + !this.integration.integrations && + !this.integration.iot_standards + ? html`${this.hass.localize( + "ui.panel.config.integrations.config_entry.yaml_only" + )}` + : ""} + ${this.integration.cloud + ? html`${this.hass.localize( + "ui.panel.config.integrations.config_entry.depends_on_cloud" + )}` + : ""} + ${!this.integration.is_built_in + ? html`${this.hass.localize( + "ui.panel.config.integrations.config_entry.provided_by_custom_integration" + )}` + : ""} + + `; + } + + static get styles(): CSSResultGroup { + return [ + styles, + css` + :host { + padding-left: var(--mdc-list-side-padding, 20px); + padding-right: var(--mdc-list-side-padding, 20px); + } + :host([graphic="avatar"]:not([twoLine])), + :host([graphic="icon"]:not([twoLine])) { + height: 48px; + } + span.material-icons:first-of-type { + margin-inline-start: 0px !important; + margin-inline-end: var( + --mdc-list-item-graphic-margin, + 16px + ) !important; + direction: var(--direction); + } + span.material-icons:last-of-type { + margin-inline-start: auto !important; + margin-inline-end: 0px !important; + direction: var(--direction); + } + img { + width: 40px; + height: 40px; + } + .mdc-deprecated-list-item__meta { + width: auto; + } + .mdc-deprecated-list-item__meta > * { + margin-right: 8px; + } + .mdc-deprecated-list-item__meta > *:last-child { + margin-right: 0px; + } + ha-icon-next { + margin-right: 8px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-integration-list-item": HaIntegrationListItem; + } +} diff --git a/src/panels/config/integrations/show-add-integration-dialog.ts b/src/panels/config/integrations/show-add-integration-dialog.ts new file mode 100644 index 0000000000..cb3fb8b9e1 --- /dev/null +++ b/src/panels/config/integrations/show-add-integration-dialog.ts @@ -0,0 +1,12 @@ +import { fireEvent } from "../../../common/dom/fire_event"; + +export const showAddIntegrationDialog = ( + element: HTMLElement, + dialogParams?: any +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-add-integration", + dialogImport: () => import("./dialog-add-integration"), + dialogParams: dialogParams, + }); +}; diff --git a/src/translations/en.json b/src/translations/en.json index 5bbe3ca627..139fdd1707 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2835,7 +2835,7 @@ "discovered": "Discovered", "attention": "Attention required", "configured": "Configured", - "new": "Set up a new integration", + "new": "Select brand", "confirm_new": "Do you want to set up {integration}?", "add_integration": "Add integration", "no_integrations": "Seems like you don't have any integrations configured yet. Click on the button below to add your first integration!", @@ -2852,6 +2852,7 @@ "rename_dialog": "Edit the name of this config entry", "rename_input_label": "Entry name", "search": "Search integrations", + "search_brand": "Search for a brand name", "add_zwave_js_device": "Add Z-Wave device", "add_zha_device": "Add Zigbee device", "disable": { @@ -2922,6 +2923,7 @@ }, "provided_by_custom_integration": "Provided by a custom integration", "depends_on_cloud": "Depends on the cloud", + "yaml_only": "Can not be setup from the UI", "disabled_polling": "Automatic polling for updated data disabled", "state": { "loaded": "Loaded", @@ -2943,6 +2945,9 @@ "submit": "Submit", "next": "Next", "found_following_devices": "We found the following devices", + "yaml_only_title": "This device can not be added from the UI", + "yaml_only_text": "You can add this device by adding it to your `configuration.yaml`. See the {link} for more information.", + "documentation": "documentation", "no_config_flow": "This integration does not support configuration via the UI. If you followed this link from the Home Assistant website, make sure you run the latest version of Home Assistant.", "not_all_required_fields": "Not all required fields are filled in.", "error_saving_area": "Error saving area: {error}", @@ -2963,6 +2968,7 @@ "error": "Error", "could_not_load": "Config flow could not be loaded", "not_loaded": "The integration could not be loaded, try to restart Home Assistant.", + "missing_credentials_title": "Add application credentials?", "missing_credentials": "Setting up {integration} requires configuring application credentials. Do you want to do that now?", "supported_brand_flow": "Support for {supported_brand} devices is provided by {flow_domain_name}. Do you want to continue?", "missing_zwave_zigbee": "To add a {integration} device, you first need {supported_hardware_link} and the {integration} integration set up. If you already have the hardware then you can proceed with the setup of {integration}.",