diff --git a/src/components/ha-related-items.ts b/src/components/ha-related-items.ts index 6f26471ea2..a2ed283db1 100644 --- a/src/components/ha-related-items.ts +++ b/src/components/ha-related-items.ts @@ -1,31 +1,30 @@ -import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; +import "@material/mwc-list/mwc-list"; +import { mdiDevices, mdiPaletteSwatch, mdiSofa } from "@mdi/js"; +import { HassEntity } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, - PropertyValues, nothing, + PropertyValues, } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; import { fireEvent } from "../common/dom/fire_event"; -import { - AreaRegistryEntry, - subscribeAreaRegistry, -} from "../data/area_registry"; +import { Blueprints, fetchBlueprints } from "../data/blueprint"; import { ConfigEntry, getConfigEntries } from "../data/config_entries"; -import { - DeviceRegistryEntry, - subscribeDeviceRegistry, -} from "../data/device_registry"; import { SceneEntity } from "../data/scene"; import { findRelated, ItemType, RelatedResult } from "../data/search"; -import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { HomeAssistant } from "../types"; +import { brandsUrl } from "../util/brands-url"; +import "./ha-icon-next"; +import "./ha-list-item"; +import "./ha-state-icon"; import "./ha-switch"; @customElement("ha-related-items") -export class HaRelatedItems extends SubscribeMixin(LitElement) { +export class HaRelatedItems extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property() public itemType!: ItemType; @@ -34,29 +33,31 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) { @state() private _entries?: ConfigEntry[]; - @state() private _devices?: DeviceRegistryEntry[]; - - @state() private _areas?: AreaRegistryEntry[]; + @state() private _blueprints?: Record<"automation" | "script", Blueprints>; @state() private _related?: RelatedResult; - public hassSubscribe(): UnsubscribeFunc[] { - return [ - subscribeDeviceRegistry(this.hass.connection!, (devices) => { - this._devices = devices; - }), - subscribeAreaRegistry(this.hass.connection!, (areas) => { - this._areas = areas; - }), - ]; - } - protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); - getConfigEntries(this.hass).then((configEntries) => { - this._entries = configEntries; - }); + } + + private async _fetchConfigEntries() { + if (this._entries) { + return; + } this.hass.loadBackendTranslation("title"); + this._entries = await getConfigEntries(this.hass); + } + + private async _fetchBlueprints() { + if (this._blueprints) { + return; + } + const [automation, script] = await Promise.all([ + fetchBlueprints(this.hass, "automation"), + fetchBlueprints(this.hass, "script"), + ]); + this._blueprints = { automation, script }; } protected updated(changedProps: PropertyValues) { @@ -81,77 +82,112 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) { } return html` ${this._related.config_entry && this._entries - ? this._related.config_entry.map((relatedConfigEntryId) => { - const entry: ConfigEntry | undefined = this._entries!.find( - (configEntry) => configEntry.entry_id === relatedConfigEntryId - ); - if (!entry) { - return ""; - } - return html` -

- ${this.hass.localize( - "ui.components.related-items.integration" - )}: -

- + ? html`

+ ${this.hass.localize("ui.components.related-items.integration")}: +

${this._related.config_entry.map( + (relatedConfigEntryId) => { + const entry: ConfigEntry | undefined = this._entries!.find( + (configEntry) => configEntry.entry_id === relatedConfigEntryId + ); + if (!entry) { + return ""; + } + return html` +
+ + ${entry.domain} ${this.hass.localize(`component.${entry.domain}.title`)}: - ${entry.title} - - `; - }) + ${entry.title} + + + `; + } + )}` : ""} - ${this._related.device && this._devices - ? this._related.device.map((relatedDeviceId) => { - const device: DeviceRegistryEntry | undefined = this._devices!.find( - (dev) => dev.id === relatedDeviceId - ); - if (!device) { - return ""; - } - return html` -

- ${this.hass.localize("ui.components.related-items.device")}: -

- - ${device.name_by_user || device.name} - - `; - }) + ${this._related.device + ? html`

+ ${this.hass.localize("ui.components.related-items.device")}: +

+ ${this._related.device.map((relatedDeviceId) => { + const device = this.hass.devices[relatedDeviceId]; + if (!device) { + return ""; + } + return html` + + + + ${device.name_by_user || device.name} + + + + `; + })} + ` : ""} - ${this._related.area && this._areas - ? this._related.area.map((relatedAreaId) => { - const area: AreaRegistryEntry | undefined = this._areas!.find( - (ar) => ar.area_id === relatedAreaId - ); - if (!area) { - return ""; - } - return html` -

- ${this.hass.localize("ui.components.related-items.area")}: -

- - ${area.name} - - `; - }) + ${this._related.area + ? html`

+ ${this.hass.localize("ui.components.related-items.area")}: +

+ ${this._related.area.map((relatedAreaId) => { + const area = this.hass.areas[relatedAreaId]; + if (!area) { + return ""; + } + return html` + + + ${area.picture + ? html`
` + : html``} + ${area.name} + +
+
+ `; + })}
` : ""} ${this._related.entity ? html`

${this.hass.localize("ui.components.related-items.entity")}:

- + ` : ""} ${this._related.group ? html`

${this.hass.localize("ui.components.related-items.group")}:

- + ` : ""} ${this._related.scene ? html`

${this.hass.localize("ui.components.related-items.scene")}:

- + + ` + : ""} + ${this._related.automation_blueprint + ? html` +

+ ${this.hass.localize("ui.components.related-items.blueprint")}: +

+ + ${this._related.automation_blueprint.map((path) => { + const blueprintMeta = this._blueprints + ? this._blueprints.automation[path] + : undefined; + return html` + + + ${!blueprintMeta || "error" in blueprintMeta + ? path + : blueprintMeta.metadata.name || path} + + + `; + })} + ` : ""} ${this._related.automation @@ -227,7 +304,7 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {

${this.hass.localize("ui.components.related-items.automation")}:

- + + ` + : ""} + ${this._related.script_blueprint + ? html` +

+ ${this.hass.localize("ui.components.related-items.blueprint")}: +

+ + ${this._related.script_blueprint.map((path) => { + const blueprintMeta = this._blueprints + ? this._blueprints.script[path] + : undefined; + return html` + + + ${!blueprintMeta || "error" in blueprintMeta + ? path + : blueprintMeta.metadata.name || path} + + + `; + })} + ` : ""} ${this._related.script @@ -255,7 +365,7 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {

${this.hass.localize("ui.components.related-items.script")}:

- + ` : ""} `; @@ -290,8 +404,12 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) { private async _findRelated() { this._related = await findRelated(this.hass, this.itemType, this.itemId); - await this.updateComplete; - fireEvent(this, "iron-resize"); + if (this._related.config_entry) { + this._fetchConfigEntries(); + } + if (this._related.script_blueprint || this._related.automation_blueprint) { + this._fetchBlueprints(); + } } private _openMoreInfo(ev: CustomEvent) { @@ -303,19 +421,10 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) { return css` a { color: var(--primary-color); + text-decoration: none; } - button.link { - color: var(--primary-color); - text-align: left; - cursor: pointer; - background: none; - border-width: initial; - border-style: none; - border-color: initial; - border-image: initial; - padding: 0px; - font: inherit; - text-decoration: underline; + ha-list-item { + --mdc-list-side-padding: 24px; } h3 { font-family: var(--paper-font-title_-_font-family); @@ -327,10 +436,15 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) { letter-spacing: var(--paper-font-title_-_letter-spacing); line-height: var(--paper-font-title_-_line-height); opacity: var(--dark-primary-opacity); + padding: 0 24px; } h3:first-child { margin-top: 0; } + .avatar { + background-position: center center; + background-size: cover; + } `; } } diff --git a/src/data/search.ts b/src/data/search.ts index 1e00372c9d..47ccd71514 100644 --- a/src/data/search.ts +++ b/src/data/search.ts @@ -3,14 +3,23 @@ import { HomeAssistant } from "../types"; export interface RelatedResult { area?: string[]; automation?: string[]; + automation_blueprint?: string[]; config_entry?: string[]; device?: string[]; entity?: string[]; group?: string[]; scene?: string[]; script?: string[]; + script_blueprint?: string[]; } +export const SearchableDomains = new Set([ + "automation", + "script", + "scene", + "group", +]); + export type ItemType = | "area" | "automation" @@ -19,7 +28,9 @@ export type ItemType = | "entity" | "group" | "scene" - | "script"; + | "script" + | "automation_blueprint" + | "script_blueprint"; export const findRelated = ( hass: HomeAssistant, diff --git a/src/dialogs/more-info/ha-more-info-dialog.ts b/src/dialogs/more-info/ha-more-info-dialog.ts index 201714098c..a35f1d7e4e 100644 --- a/src/dialogs/more-info/ha-more-info-dialog.ts +++ b/src/dialogs/more-info/ha-more-info-dialog.ts @@ -30,6 +30,7 @@ import { ExtEntityRegistryEntry, getExtendedEntityRegistryEntry, } from "../../data/entity_registry"; +import { SearchableDomains } from "../../data/search"; import { haStyleDialog } from "../../resources/styles"; import "../../state-summary/state-card-content"; import { HomeAssistant } from "../../types"; @@ -406,7 +407,9 @@ export class MoreInfoDialog extends LitElement { ` : nothing @@ -464,7 +467,6 @@ export class MoreInfoDialog extends LitElement { flex: 1; } - ha-related-items, ha-more-info-history-and-logbook { padding: 8px 24px 24px 24px; display: block; diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index ed461c4c96..cb89fd8af5 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -50,6 +50,8 @@ import { HomeAssistant, Route } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; import { configSections } from "../ha-panel-config"; import { showNewAutomationDialog } from "./show-dialog-new-automation"; +import { findRelated } from "../../../data/search"; +import { fetchBlueprints } from "../../../data/blueprint"; @customElement("ha-automation-picker") class HaAutomationPicker extends LitElement { @@ -65,6 +67,8 @@ class HaAutomationPicker extends LitElement { @property() private _activeFilters?: string[]; + @state() private _searchParms = new URLSearchParams(window.location.search); + @state() private _filteredAutomations?: string[] | null; @state() private _filterValue?; @@ -308,6 +312,34 @@ class HaAutomationPicker extends LitElement { `; } + firstUpdated() { + if (this._searchParms.has("blueprint")) { + this._filterBlueprint(); + } + } + + private async _filterBlueprint() { + const blueprint = this._searchParms.get("blueprint"); + if (!blueprint) { + return; + } + const [related, blueprints] = await Promise.all([ + findRelated(this.hass, "automation_blueprint", blueprint), + fetchBlueprints(this.hass, "automation"), + ]); + this._filteredAutomations = related.automation || []; + const blueprintMeta = blueprints[blueprint]; + this._activeFilters = [ + this.hass.localize( + "ui.panel.config.automation.picker.filtered_by_blueprint", + "name", + !blueprintMeta || "error" in blueprintMeta + ? blueprint + : blueprintMeta.metadata.name || blueprint + ), + ]; + } + private _relatedFilterChanged(ev: CustomEvent) { this._filterValue = ev.detail.value; if (!this._filterValue) { diff --git a/src/panels/config/blueprint/ha-blueprint-overview.ts b/src/panels/config/blueprint/ha-blueprint-overview.ts index 2436d5b039..ea795b3cd2 100644 --- a/src/panels/config/blueprint/ha-blueprint-overview.ts +++ b/src/panels/config/blueprint/ha-blueprint-overview.ts @@ -1,11 +1,13 @@ +import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; import { + mdiAlertCircle, mdiDelete, mdiDownload, + mdiEye, mdiHelpCircle, - mdiRobot, + mdiPlus, mdiShareVariant, } from "@mdi/js"; -import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; import { CSSResultGroup, html, @@ -15,13 +17,18 @@ import { } from "lit"; import { customElement, property } from "lit/decorators"; import memoizeOne from "memoize-one"; -import { fireEvent } from "../../../common/dom/fire_event"; +import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event"; +import { computeStateName } from "../../../common/entity/compute_state_name"; import { navigate } from "../../../common/navigate"; import { extractSearchParam } from "../../../common/url/search-params"; -import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table"; +import { + DataTableColumnContainer, + RowClickedEvent, +} from "../../../components/data-table/ha-data-table"; import "../../../components/entity/ha-entity-toggle"; import "../../../components/ha-fab"; import "../../../components/ha-icon-button"; +import "../../../components/ha-icon-overflow-menu"; import "../../../components/ha-svg-icon"; import { showAutomationEditor } from "../../../data/automation"; import { @@ -31,6 +38,7 @@ import { deleteBlueprint, } from "../../../data/blueprint"; import { showScriptEditor } from "../../../data/script"; +import { findRelated } from "../../../data/search"; import { showAlertDialog, showConfirmationDialog, @@ -73,7 +81,7 @@ class HaBlueprintOverview extends LitElement { @property({ attribute: false }) public route!: Route; @property({ attribute: false }) public blueprints!: Record< - string, + "automation" | "script", Blueprints >; @@ -104,7 +112,7 @@ class HaBlueprintOverview extends LitElement { ); private _columns = memoizeOne( - (narrow, _language): DataTableColumnContainer => ({ + (narrow, _language): DataTableColumnContainer => ({ name: { title: this.hass.localize( "ui.panel.config.blueprint.overview.headers.name" @@ -146,64 +154,60 @@ class HaBlueprintOverview extends LitElement { direction: "asc", width: "25%", }, - create: { + actions: { title: "", - width: narrow ? undefined : "20%", - type: narrow ? "icon-button" : undefined, - template: (_, blueprint: BlueprintMetaDataPath) => + width: this.narrow ? undefined : "10%", + type: "overflow-menu", + template: (_: string, blueprint) => blueprint.error - ? "" - : narrow - ? html` - ` - : html` - ${this.hass.localize( - `ui.panel.config.blueprint.overview.create_${blueprint.domain}` - )} - `, - }, - share: { - title: "", - type: "icon-button", - template: (_, blueprint: any) => - blueprint.error - ? "" - : html``, - }, - delete: { - title: "", - type: "icon-button", - template: (_, blueprint: any) => - blueprint.error - ? "" - : html``, + ? html`` + : html` + this._createNew(blueprint), + }, + { + path: mdiEye, + label: this.hass.localize( + `ui.panel.config.blueprint.overview.view_${blueprint.domain}` + ), + action: () => this._showUsed(blueprint), + }, + { + path: mdiShareVariant, + disabled: !blueprint.source_url, + label: this.hass.localize( + blueprint.source_url + ? "ui.panel.config.blueprint.overview.share_blueprint" + : "ui.panel.config.blueprint.overview.share_blueprint_no_url" + ), + action: () => this._share(blueprint), + }, + { + divider: true, + }, + { + label: this.hass.localize( + "ui.panel.config.blueprint.overview.delete_blueprint" + ), + path: mdiDelete, + action: () => this._delete(blueprint), + warning: true, + }, + ]} + > + + `, }, }) ); @@ -229,11 +233,13 @@ class HaBlueprintOverview extends LitElement { .tabs=${configSections.automations} .columns=${this._columns(this.narrow, this.hass.language)} .data=${this._processedBlueprints(this.blueprints)} - id="entity_id" + id="path" .noDataText=${this.hass.localize( "ui.panel.config.blueprint.overview.no_blueprints" )} hasFab + clickable + @row-click=${this._handleRowClicked} .appendRow=${html`
{ - const blueprint = ev.currentTarget.blueprint as BlueprintMetaDataPath; + private _handleRowClicked(ev: HASSDomEvent) { + const blueprint = this._processedBlueprints(this.blueprints).find( + (b) => b.path === ev.detail.id + ); + if (blueprint.error) { + return; + } + this._createNew(blueprint); + } + + private _showUsed = (blueprint: BlueprintMetaDataPath) => { + navigate( + `/config/${blueprint.domain}/dashboard?blueprint=${encodeURIComponent( + blueprint.path + )}` + ); + }; + + private _createNew = (blueprint: BlueprintMetaDataPath) => { createNewFunctions[blueprint.domain](blueprint); }; - private _share = (ev) => { - const blueprint = ev.currentTarget.blueprint; + private _share = (blueprint: BlueprintMetaDataPath) => { const params = new URLSearchParams(); params.append("redirect", "blueprint_import"); - params.append("blueprint_url", blueprint.source_url); + params.append("blueprint_url", blueprint.source_url!); window.open( `https://my.home-assistant.io/create-link/?${params.toString()}` ); }; - private _delete = async (ev) => { - const blueprint = ev.currentTarget.blueprint; + private _delete = async (blueprint: BlueprintMetaDataPath) => { + const related = await findRelated( + this.hass, + `${blueprint.domain}_blueprint`, + blueprint.path + ); + if (related.automation?.length || related.script?.length) { + const type = this.hass.localize( + `ui.panel.config.blueprint.overview.types_plural.${blueprint.domain}` + ); + const result = await showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.blueprint.overview.blueprint_in_use_title" + ), + text: this.hass.localize( + "ui.panel.config.blueprint.overview.blueprint_in_use_text", + { + type, + list: html`
    + ${[...(related.automation || []), ...(related.script || [])].map( + (item) => { + const state = this.hass.states[item]; + return html`
  • + ${state ? `${computeStateName(state)} (${item})` : item} +
  • `; + } + )} +
`, + } + ), + confirmText: this.hass!.localize( + "ui.panel.config.blueprint.overview.blueprint_in_use_view", + { type } + ), + }); + if (result) { + navigate( + `/config/${blueprint.domain}/dashboard?blueprint=${encodeURIComponent( + blueprint.path + )}` + ); + } + return; + } if ( !(await showConfirmationDialog(this, { title: this.hass.localize( diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts index 70ddcc8758..f5d80a60be 100644 --- a/src/panels/config/script/ha-script-picker.ts +++ b/src/panels/config/script/ha-script-picker.ts @@ -45,6 +45,8 @@ import { documentationUrl } from "../../../util/documentation-url"; import { showToast } from "../../../util/toast"; import { configSections } from "../ha-panel-config"; import { EntityRegistryEntry } from "../../../data/entity_registry"; +import { findRelated } from "../../../data/search"; +import { fetchBlueprints } from "../../../data/blueprint"; @customElement("ha-script-picker") class HaScriptPicker extends LitElement { @@ -60,6 +62,8 @@ class HaScriptPicker extends LitElement { @property({ attribute: false }) public entityRegistry!: EntityRegistryEntry[]; + @state() private _searchParms = new URLSearchParams(window.location.search); + @state() private _activeFilters?: string[]; @state() private _filteredScripts?: string[] | null; @@ -251,6 +255,34 @@ class HaScriptPicker extends LitElement { `; } + firstUpdated() { + if (this._searchParms.has("blueprint")) { + this._filterBlueprint(); + } + } + + private async _filterBlueprint() { + const blueprint = this._searchParms.get("blueprint"); + if (!blueprint) { + return; + } + const [related, blueprints] = await Promise.all([ + findRelated(this.hass, "script_blueprint", blueprint), + fetchBlueprints(this.hass, "script"), + ]); + this._filteredScripts = related.script || []; + const blueprintMeta = blueprints[blueprint]; + this._activeFilters = [ + this.hass.localize( + "ui.panel.config.script.picker.filtered_by_blueprint", + "name", + !blueprintMeta || "error" in blueprintMeta + ? blueprint + : blueprintMeta.metadata.name || blueprint + ), + ]; + } + private _relatedFilterChanged(ev: CustomEvent) { this._filterValue = ev.detail.value; if (!this._filterValue) { diff --git a/src/translations/en.json b/src/translations/en.json index 4976c9f1a9..29fa8846a3 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -540,7 +540,8 @@ "group": "Part of the following groups", "scene": "Part of the following scenes", "script": "Part of the following scripts", - "automation": "Part of the following automations" + "automation": "Part of the following automations", + "blueprint": "Using the following blueprints" }, "data-table": { "search": "Search", @@ -2147,6 +2148,7 @@ "delete_confirm_text": "{name} will be permanently deleted.", "duplicate": "[%key:ui::common::duplicate%]", "disabled": "Disabled", + "filtered_by_blueprint": "blueprint: {name}", "headers": { "toggle": "Enable/disable", "name": "Name", @@ -2622,12 +2624,21 @@ "automation": "Automation", "script": "Script" }, + "types_plural": { + "automation": "automations", + "script": "scripts" + }, + "blueprint_in_use_title": "This blueprint is in use, and can not be deleted", + "blueprint_in_use_text": "Please remove all below {type} that use this blueprint before deleting it. {list}", + "blueprint_in_use_view": "view {type}", "confirm_delete_title": "Delete blueprint?", "confirm_delete_text": "{name} will be permanently deleted.", "add_blueprint": "Import blueprint", "no_blueprints": "[%key:ui::panel::config::automation::editor::blueprint::no_blueprints%]", "create_automation": "Create automation", "create_script": "Create script", + "view_automation": "Show automations using this blueprint", + "view_script": "Show scripts using this blueprint", "delete_blueprint": "Delete blueprint", "share_blueprint": "Share blueprint", "share_blueprint_no_url": "Unable to share blueprint: no source url", @@ -2662,6 +2673,7 @@ "run": "[%key:ui::panel::config::automation::editor::actions::run%]", "show_trace": "[%key:ui::panel::config::automation::editor::show_trace%]", "show_info": "[%key:ui::panel::config::automation::editor::show_info%]", + "filtered_by_blueprint": "[%key:ui::panel::config::automation::picker::filtered_by_blueprint%]", "headers": { "name": "Name", "state": "State"