Related blueprints (#16618)

This commit is contained in:
Bram Kragten 2023-05-30 15:04:45 +02:00 committed by GitHub
parent e0c1f98803
commit a70d7d8de3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 501 additions and 234 deletions

View File

@ -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 { import {
css, css,
CSSResultGroup, CSSResultGroup,
html, html,
LitElement, LitElement,
PropertyValues,
nothing, nothing,
PropertyValues,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { import { Blueprints, fetchBlueprints } from "../data/blueprint";
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../data/area_registry";
import { ConfigEntry, getConfigEntries } from "../data/config_entries"; import { ConfigEntry, getConfigEntries } from "../data/config_entries";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../data/device_registry";
import { SceneEntity } from "../data/scene"; import { SceneEntity } from "../data/scene";
import { findRelated, ItemType, RelatedResult } from "../data/search"; import { findRelated, ItemType, RelatedResult } from "../data/search";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { HomeAssistant } from "../types"; 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"; import "./ha-switch";
@customElement("ha-related-items") @customElement("ha-related-items")
export class HaRelatedItems extends SubscribeMixin(LitElement) { export class HaRelatedItems extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public itemType!: ItemType; @property() public itemType!: ItemType;
@ -34,29 +33,31 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
@state() private _entries?: ConfigEntry[]; @state() private _entries?: ConfigEntry[];
@state() private _devices?: DeviceRegistryEntry[]; @state() private _blueprints?: Record<"automation" | "script", Blueprints>;
@state() private _areas?: AreaRegistryEntry[];
@state() private _related?: RelatedResult; @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) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
getConfigEntries(this.hass).then((configEntries) => { }
this._entries = configEntries;
}); private async _fetchConfigEntries() {
if (this._entries) {
return;
}
this.hass.loadBackendTranslation("title"); 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) { protected updated(changedProps: PropertyValues) {
@ -81,7 +82,10 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
} }
return html` return html`
${this._related.config_entry && this._entries ${this._related.config_entry && this._entries
? this._related.config_entry.map((relatedConfigEntryId) => { ? html`<h3>
${this.hass.localize("ui.components.related-items.integration")}:
</h3><mwc-list>${this._related.config_entry.map(
(relatedConfigEntryId) => {
const entry: ConfigEntry | undefined = this._entries!.find( const entry: ConfigEntry | undefined = this._entries!.find(
(configEntry) => configEntry.entry_id === relatedConfigEntryId (configEntry) => configEntry.entry_id === relatedConfigEntryId
); );
@ -89,69 +93,101 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
return ""; return "";
} }
return html` return html`
<h3>
${this.hass.localize(
"ui.components.related-items.integration"
)}:
</h3>
<a <a
href=${`/config/integrations#config_entry=${relatedConfigEntryId}`} href=${`/config/integrations#config_entry=${relatedConfigEntryId}`}
@click=${this._navigateAwayClose} @click=${this._navigateAwayClose}
> >
<ha-list-item hasMeta graphic="icon">
<img
.src=${brandsUrl({
domain: entry.domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
alt=${entry.domain}
slot="graphic"
/>
${this.hass.localize(`component.${entry.domain}.title`)}: ${this.hass.localize(`component.${entry.domain}.title`)}:
${entry.title} ${entry.title} <ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a> </a>
`; `;
}) }
)}</mw-list>`
: ""} : ""}
${this._related.device && this._devices ${this._related.device
? this._related.device.map((relatedDeviceId) => { ? html`<h3>
const device: DeviceRegistryEntry | undefined = this._devices!.find( ${this.hass.localize("ui.components.related-items.device")}:
(dev) => dev.id === relatedDeviceId </h3>
); ${this._related.device.map((relatedDeviceId) => {
const device = this.hass.devices[relatedDeviceId];
if (!device) { if (!device) {
return ""; return "";
} }
return html` return html`
<h3>
${this.hass.localize("ui.components.related-items.device")}:
</h3>
<a <a
href="/config/devices/device/${relatedDeviceId}" href="/config/devices/device/${relatedDeviceId}"
@click=${this._navigateAwayClose} @click=${this._navigateAwayClose}
> >
<ha-list-item hasMeta graphic="icon">
<ha-svg-icon
.path=${mdiDevices}
slot="graphic"
></ha-svg-icon>
${device.name_by_user || device.name} ${device.name_by_user || device.name}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a> </a>
`; `;
}) })} </mwc-list>
`
: ""} : ""}
${this._related.area && this._areas ${this._related.area
? this._related.area.map((relatedAreaId) => { ? html`<h3>
const area: AreaRegistryEntry | undefined = this._areas!.find( ${this.hass.localize("ui.components.related-items.area")}:
(ar) => ar.area_id === relatedAreaId </h3>
); <mwc-list
>${this._related.area.map((relatedAreaId) => {
const area = this.hass.areas[relatedAreaId];
if (!area) { if (!area) {
return ""; return "";
} }
return html` return html`
<h3>
${this.hass.localize("ui.components.related-items.area")}:
</h3>
<a <a
href="/config/areas/area/${relatedAreaId}" href="/config/areas/area/${relatedAreaId}"
@click=${this._navigateAwayClose} @click=${this._navigateAwayClose}
> >
<ha-list-item
hasMeta
.graphic=${area.picture ? "avatar" : "icon"}
>
${area.picture
? html` <div
class="avatar"
style=${styleMap({
backgroundImage: `url(${area.picture})`,
})}
slot="graphic"
></div>`
: html`<ha-svg-icon
.path=${mdiSofa}
slot="graphic"
></ha-svg-icon>`}
${area.name} ${area.name}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a> </a>
`; `;
}) })}</mwc-list
>`
: ""} : ""}
${this._related.entity ${this._related.entity
? html` ? html`
<h3> <h3>
${this.hass.localize("ui.components.related-items.entity")}: ${this.hass.localize("ui.components.related-items.entity")}:
</h3> </h3>
<ul> <mwc-list>
${this._related.entity.map((entityId) => { ${this._related.entity.map((entityId) => {
const entity: HassEntity | undefined = const entity: HassEntity | undefined =
this.hass.states[entityId]; this.hass.states[entityId];
@ -159,48 +195,56 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
return ""; return "";
} }
return html` return html`
<li> <ha-list-item
<button
@click=${this._openMoreInfo} @click=${this._openMoreInfo}
.entityId=${entityId} .entityId=${entityId}
class="link" hasMeta
graphic="icon"
> >
${entity.attributes.friendly_name || entityId} <ha-state-icon
</button> .state=${entity}
</li> slot="graphic"
></ha-state-icon>
${entity.attributes.friendly_name || entity.entity_id}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`; `;
})} })}
</ul> </mwc-list>
` `
: ""} : ""}
${this._related.group ${this._related.group
? html` ? html`
<h3>${this.hass.localize("ui.components.related-items.group")}:</h3> <h3>${this.hass.localize("ui.components.related-items.group")}:</h3>
<ul> <mwc-list>
${this._related.group.map((groupId) => { ${this._related.group.map((groupId) => {
const group: HassEntity | undefined = this.hass.states[groupId]; const group: HassEntity | undefined = this.hass.states[groupId];
if (!group) { if (!group) {
return ""; return "";
} }
return html` return html`
<li> <ha-list-item
<button
class="link"
@click=${this._openMoreInfo} @click=${this._openMoreInfo}
.entityId=${groupId} .entityId=${groupId}
hasMeta
graphic="icon"
> >
<ha-state-icon
.state=${group}
slot="graphic"
></ha-state-icon>
${group.attributes.friendly_name || group.entity_id} ${group.attributes.friendly_name || group.entity_id}
</button> <ha-icon-next slot="meta"></ha-icon-next>
</li> </ha-list-item>
`; `;
})} })}
</ul> </mwc-list>
` `
: ""} : ""}
${this._related.scene ${this._related.scene
? html` ? html`
<h3>${this.hass.localize("ui.components.related-items.scene")}:</h3> <h3>${this.hass.localize("ui.components.related-items.scene")}:</h3>
<ul> <mwc-list>
${this._related.scene.map((sceneId) => { ${this._related.scene.map((sceneId) => {
const scene: SceneEntity | undefined = const scene: SceneEntity | undefined =
this.hass.states[sceneId]; this.hass.states[sceneId];
@ -208,18 +252,51 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
return ""; return "";
} }
return html` return html`
<li> <ha-list-item
<button
class="link"
@click=${this._openMoreInfo} @click=${this._openMoreInfo}
.entityId=${sceneId} .entityId=${sceneId}
hasMeta
graphic="icon"
> >
<ha-state-icon
.state=${scene}
slot="graphic"
></ha-state-icon>
${scene.attributes.friendly_name || scene.entity_id} ${scene.attributes.friendly_name || scene.entity_id}
</button> <ha-icon-next slot="meta"></ha-icon-next>
</li> </ha-list-item>
`; `;
})} })}
</ul> </mwc-list>
`
: ""}
${this._related.automation_blueprint
? html`
<h3>
${this.hass.localize("ui.components.related-items.blueprint")}:
</h3>
<mwc-list>
${this._related.automation_blueprint.map((path) => {
const blueprintMeta = this._blueprints
? this._blueprints.automation[path]
: undefined;
return html`<a
href="/config/blueprint/dashboard"
@click=${this._navigateAwayClose}
>
<ha-list-item hasMeta graphic="icon">
<ha-svg-icon
.path=${mdiPaletteSwatch}
slot="graphic"
></ha-svg-icon>
${!blueprintMeta || "error" in blueprintMeta
? path
: blueprintMeta.metadata.name || path}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>`;
})}
</mwc-list>
` `
: ""} : ""}
${this._related.automation ${this._related.automation
@ -227,7 +304,7 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
<h3> <h3>
${this.hass.localize("ui.components.related-items.automation")}: ${this.hass.localize("ui.components.related-items.automation")}:
</h3> </h3>
<ul> <mwc-list>
${this._related.automation.map((automationId) => { ${this._related.automation.map((automationId) => {
const automation: HassEntity | undefined = const automation: HassEntity | undefined =
this.hass.states[automationId]; this.hass.states[automationId];
@ -235,19 +312,52 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
return ""; return "";
} }
return html` return html`
<li> <ha-list-item
<button
class="link"
@click=${this._openMoreInfo} @click=${this._openMoreInfo}
.entityId=${automationId} .entityId=${automationId}
hasMeta
graphic="icon"
> >
<ha-state-icon
.state=${automation}
slot="graphic"
></ha-state-icon>
${automation.attributes.friendly_name || ${automation.attributes.friendly_name ||
automation.entity_id} automation.entity_id}
</button> <ha-icon-next slot="meta"></ha-icon-next>
</li> </ha-list-item>
`; `;
})} })}
</ul> </mwc-list>
`
: ""}
${this._related.script_blueprint
? html`
<h3>
${this.hass.localize("ui.components.related-items.blueprint")}:
</h3>
<mwc-list>
${this._related.script_blueprint.map((path) => {
const blueprintMeta = this._blueprints
? this._blueprints.script[path]
: undefined;
return html`<a
href="/config/blueprint/dashboard"
@click=${this._navigateAwayClose}
>
<ha-list-item hasMeta graphic="icon">
<ha-svg-icon
.path=${mdiPaletteSwatch}
slot="graphic"
></ha-svg-icon>
${!blueprintMeta || "error" in blueprintMeta
? path
: blueprintMeta.metadata.name || path}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>`;
})}
</mwc-list>
` `
: ""} : ""}
${this._related.script ${this._related.script
@ -255,7 +365,7 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
<h3> <h3>
${this.hass.localize("ui.components.related-items.script")}: ${this.hass.localize("ui.components.related-items.script")}:
</h3> </h3>
<ul> <mwc-list>
${this._related.script.map((scriptId) => { ${this._related.script.map((scriptId) => {
const script: HassEntity | undefined = const script: HassEntity | undefined =
this.hass.states[scriptId]; this.hass.states[scriptId];
@ -263,18 +373,22 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
return ""; return "";
} }
return html` return html`
<li> <ha-list-item
<button
class="link"
@click=${this._openMoreInfo} @click=${this._openMoreInfo}
.entityId=${scriptId} .entityId=${scriptId}
hasMeta
graphic="icon"
> >
<ha-state-icon
.state=${script}
slot="graphic"
></ha-state-icon>
${script.attributes.friendly_name || script.entity_id} ${script.attributes.friendly_name || script.entity_id}
</button> <ha-icon-next slot="meta"></ha-icon-next>
</li> </ha-list-item>
`; `;
})} })}
</ul> </mwc-list>
` `
: ""} : ""}
`; `;
@ -290,8 +404,12 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
private async _findRelated() { private async _findRelated() {
this._related = await findRelated(this.hass, this.itemType, this.itemId); this._related = await findRelated(this.hass, this.itemType, this.itemId);
await this.updateComplete; if (this._related.config_entry) {
fireEvent(this, "iron-resize"); this._fetchConfigEntries();
}
if (this._related.script_blueprint || this._related.automation_blueprint) {
this._fetchBlueprints();
}
} }
private _openMoreInfo(ev: CustomEvent) { private _openMoreInfo(ev: CustomEvent) {
@ -303,19 +421,10 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
return css` return css`
a { a {
color: var(--primary-color); color: var(--primary-color);
text-decoration: none;
} }
button.link { ha-list-item {
color: var(--primary-color); --mdc-list-side-padding: 24px;
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;
} }
h3 { h3 {
font-family: var(--paper-font-title_-_font-family); 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); letter-spacing: var(--paper-font-title_-_letter-spacing);
line-height: var(--paper-font-title_-_line-height); line-height: var(--paper-font-title_-_line-height);
opacity: var(--dark-primary-opacity); opacity: var(--dark-primary-opacity);
padding: 0 24px;
} }
h3:first-child { h3:first-child {
margin-top: 0; margin-top: 0;
} }
.avatar {
background-position: center center;
background-size: cover;
}
`; `;
} }
} }

View File

@ -3,14 +3,23 @@ import { HomeAssistant } from "../types";
export interface RelatedResult { export interface RelatedResult {
area?: string[]; area?: string[];
automation?: string[]; automation?: string[];
automation_blueprint?: string[];
config_entry?: string[]; config_entry?: string[];
device?: string[]; device?: string[];
entity?: string[]; entity?: string[];
group?: string[]; group?: string[];
scene?: string[]; scene?: string[];
script?: string[]; script?: string[];
script_blueprint?: string[];
} }
export const SearchableDomains = new Set([
"automation",
"script",
"scene",
"group",
]);
export type ItemType = export type ItemType =
| "area" | "area"
| "automation" | "automation"
@ -19,7 +28,9 @@ export type ItemType =
| "entity" | "entity"
| "group" | "group"
| "scene" | "scene"
| "script"; | "script"
| "automation_blueprint"
| "script_blueprint";
export const findRelated = ( export const findRelated = (
hass: HomeAssistant, hass: HomeAssistant,

View File

@ -30,6 +30,7 @@ import {
ExtEntityRegistryEntry, ExtEntityRegistryEntry,
getExtendedEntityRegistryEntry, getExtendedEntityRegistryEntry,
} from "../../data/entity_registry"; } from "../../data/entity_registry";
import { SearchableDomains } from "../../data/search";
import { haStyleDialog } from "../../resources/styles"; import { haStyleDialog } from "../../resources/styles";
import "../../state-summary/state-card-content"; import "../../state-summary/state-card-content";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
@ -406,7 +407,9 @@ export class MoreInfoDialog extends LitElement {
<ha-related-items <ha-related-items
.hass=${this.hass} .hass=${this.hass}
.itemId=${entityId} .itemId=${entityId}
itemType="entity" .itemType=${SearchableDomains.has(domain)
? domain
: "entity"}
></ha-related-items> ></ha-related-items>
` `
: nothing : nothing
@ -464,7 +467,6 @@ export class MoreInfoDialog extends LitElement {
flex: 1; flex: 1;
} }
ha-related-items,
ha-more-info-history-and-logbook { ha-more-info-history-and-logbook {
padding: 8px 24px 24px 24px; padding: 8px 24px 24px 24px;
display: block; display: block;

View File

@ -50,6 +50,8 @@ import { HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url"; import { documentationUrl } from "../../../util/documentation-url";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
import { showNewAutomationDialog } from "./show-dialog-new-automation"; import { showNewAutomationDialog } from "./show-dialog-new-automation";
import { findRelated } from "../../../data/search";
import { fetchBlueprints } from "../../../data/blueprint";
@customElement("ha-automation-picker") @customElement("ha-automation-picker")
class HaAutomationPicker extends LitElement { class HaAutomationPicker extends LitElement {
@ -65,6 +67,8 @@ class HaAutomationPicker extends LitElement {
@property() private _activeFilters?: string[]; @property() private _activeFilters?: string[];
@state() private _searchParms = new URLSearchParams(window.location.search);
@state() private _filteredAutomations?: string[] | null; @state() private _filteredAutomations?: string[] | null;
@state() private _filterValue?; @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) { private _relatedFilterChanged(ev: CustomEvent) {
this._filterValue = ev.detail.value; this._filterValue = ev.detail.value;
if (!this._filterValue) { if (!this._filterValue) {

View File

@ -1,11 +1,13 @@
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import { import {
mdiAlertCircle,
mdiDelete, mdiDelete,
mdiDownload, mdiDownload,
mdiEye,
mdiHelpCircle, mdiHelpCircle,
mdiRobot, mdiPlus,
mdiShareVariant, mdiShareVariant,
} from "@mdi/js"; } from "@mdi/js";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import { import {
CSSResultGroup, CSSResultGroup,
html, html,
@ -15,13 +17,18 @@ import {
} from "lit"; } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; 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 { navigate } from "../../../common/navigate";
import { extractSearchParam } from "../../../common/url/search-params"; 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/entity/ha-entity-toggle";
import "../../../components/ha-fab"; import "../../../components/ha-fab";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { showAutomationEditor } from "../../../data/automation"; import { showAutomationEditor } from "../../../data/automation";
import { import {
@ -31,6 +38,7 @@ import {
deleteBlueprint, deleteBlueprint,
} from "../../../data/blueprint"; } from "../../../data/blueprint";
import { showScriptEditor } from "../../../data/script"; import { showScriptEditor } from "../../../data/script";
import { findRelated } from "../../../data/search";
import { import {
showAlertDialog, showAlertDialog,
showConfirmationDialog, showConfirmationDialog,
@ -73,7 +81,7 @@ class HaBlueprintOverview extends LitElement {
@property({ attribute: false }) public route!: Route; @property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public blueprints!: Record< @property({ attribute: false }) public blueprints!: Record<
string, "automation" | "script",
Blueprints Blueprints
>; >;
@ -104,7 +112,7 @@ class HaBlueprintOverview extends LitElement {
); );
private _columns = memoizeOne( private _columns = memoizeOne(
(narrow, _language): DataTableColumnContainer => ({ (narrow, _language): DataTableColumnContainer<BlueprintMetaDataPath> => ({
name: { name: {
title: this.hass.localize( title: this.hass.localize(
"ui.panel.config.blueprint.overview.headers.name" "ui.panel.config.blueprint.overview.headers.name"
@ -146,64 +154,60 @@ class HaBlueprintOverview extends LitElement {
direction: "asc", direction: "asc",
width: "25%", width: "25%",
}, },
create: { actions: {
title: "", title: "",
width: narrow ? undefined : "20%", width: this.narrow ? undefined : "10%",
type: narrow ? "icon-button" : undefined, type: "overflow-menu",
template: (_, blueprint: BlueprintMetaDataPath) => template: (_: string, blueprint) =>
blueprint.error blueprint.error
? "" ? html`<ha-svg-icon
: narrow style="color: var(--error-color); display: block; margin-inline-end: 12px; margin-inline-start: auto;"
? html`<ha-icon-button .path=${mdiAlertCircle}
.blueprint=${blueprint} ></ha-svg-icon>`
.label=${this.hass.localize( : html`
<ha-icon-overflow-menu
.hass=${this.hass}
narrow
.items=${[
{
path: mdiPlus,
label: this.hass.localize(
`ui.panel.config.blueprint.overview.create_${blueprint.domain}` `ui.panel.config.blueprint.overview.create_${blueprint.domain}`
)} ),
@click=${this._createNew} action: () => this._createNew(blueprint),
.path=${mdiRobot}
>
</ha-icon-button>`
: html`<mwc-button
.blueprint=${blueprint}
@click=${this._createNew}
>
${this.hass.localize(
`ui.panel.config.blueprint.overview.create_${blueprint.domain}`
)}
</mwc-button>`,
}, },
share: { {
title: "", path: mdiEye,
type: "icon-button", label: this.hass.localize(
template: (_, blueprint: any) => `ui.panel.config.blueprint.overview.view_${blueprint.domain}`
blueprint.error ),
? "" action: () => this._showUsed(blueprint),
: html`<ha-icon-button },
.blueprint=${blueprint} {
.disabled=${!blueprint.source_url} path: mdiShareVariant,
.label=${this.hass.localize( disabled: !blueprint.source_url,
label: this.hass.localize(
blueprint.source_url blueprint.source_url
? "ui.panel.config.blueprint.overview.share_blueprint" ? "ui.panel.config.blueprint.overview.share_blueprint"
: "ui.panel.config.blueprint.overview.share_blueprint_no_url" : "ui.panel.config.blueprint.overview.share_blueprint_no_url"
)} ),
.path=${mdiShareVariant} action: () => this._share(blueprint),
@click=${this._share}
></ha-icon-button>`,
}, },
delete: { {
title: "", divider: true,
type: "icon-button", },
template: (_, blueprint: any) => {
blueprint.error label: this.hass.localize(
? ""
: html`<ha-icon-button
.blueprint=${blueprint}
.label=${this.hass.localize(
"ui.panel.config.blueprint.overview.delete_blueprint" "ui.panel.config.blueprint.overview.delete_blueprint"
)} ),
.path=${mdiDelete} path: mdiDelete,
@click=${this._delete} action: () => this._delete(blueprint),
></ha-icon-button>`, warning: true,
},
]}
>
</ha-icon-overflow-menu>
`,
}, },
}) })
); );
@ -229,11 +233,13 @@ class HaBlueprintOverview extends LitElement {
.tabs=${configSections.automations} .tabs=${configSections.automations}
.columns=${this._columns(this.narrow, this.hass.language)} .columns=${this._columns(this.narrow, this.hass.language)}
.data=${this._processedBlueprints(this.blueprints)} .data=${this._processedBlueprints(this.blueprints)}
id="entity_id" id="path"
.noDataText=${this.hass.localize( .noDataText=${this.hass.localize(
"ui.panel.config.blueprint.overview.no_blueprints" "ui.panel.config.blueprint.overview.no_blueprints"
)} )}
hasFab hasFab
clickable
@row-click=${this._handleRowClicked}
.appendRow=${html` <div .appendRow=${html` <div
class="mdc-data-table__cell" class="mdc-data-table__cell"
style="width: 100%; text-align: center;" style="width: 100%; text-align: center;"
@ -310,23 +316,81 @@ class HaBlueprintOverview extends LitElement {
fireEvent(this, "reload-blueprints"); fireEvent(this, "reload-blueprints");
} }
private _createNew = (ev) => { private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
const blueprint = ev.currentTarget.blueprint as BlueprintMetaDataPath; 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); createNewFunctions[blueprint.domain](blueprint);
}; };
private _share = (ev) => { private _share = (blueprint: BlueprintMetaDataPath) => {
const blueprint = ev.currentTarget.blueprint;
const params = new URLSearchParams(); const params = new URLSearchParams();
params.append("redirect", "blueprint_import"); params.append("redirect", "blueprint_import");
params.append("blueprint_url", blueprint.source_url); params.append("blueprint_url", blueprint.source_url!);
window.open( window.open(
`https://my.home-assistant.io/create-link/?${params.toString()}` `https://my.home-assistant.io/create-link/?${params.toString()}`
); );
}; };
private _delete = async (ev) => { private _delete = async (blueprint: BlueprintMetaDataPath) => {
const blueprint = ev.currentTarget.blueprint; 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`<ul>
${[...(related.automation || []), ...(related.script || [])].map(
(item) => {
const state = this.hass.states[item];
return html`<li>
${state ? `${computeStateName(state)} (${item})` : item}
</li>`;
}
)}
</ul>`,
}
),
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 ( if (
!(await showConfirmationDialog(this, { !(await showConfirmationDialog(this, {
title: this.hass.localize( title: this.hass.localize(

View File

@ -45,6 +45,8 @@ import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast"; import { showToast } from "../../../util/toast";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
import { EntityRegistryEntry } from "../../../data/entity_registry"; import { EntityRegistryEntry } from "../../../data/entity_registry";
import { findRelated } from "../../../data/search";
import { fetchBlueprints } from "../../../data/blueprint";
@customElement("ha-script-picker") @customElement("ha-script-picker")
class HaScriptPicker extends LitElement { class HaScriptPicker extends LitElement {
@ -60,6 +62,8 @@ class HaScriptPicker extends LitElement {
@property({ attribute: false }) public entityRegistry!: EntityRegistryEntry[]; @property({ attribute: false }) public entityRegistry!: EntityRegistryEntry[];
@state() private _searchParms = new URLSearchParams(window.location.search);
@state() private _activeFilters?: string[]; @state() private _activeFilters?: string[];
@state() private _filteredScripts?: string[] | null; @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) { private _relatedFilterChanged(ev: CustomEvent) {
this._filterValue = ev.detail.value; this._filterValue = ev.detail.value;
if (!this._filterValue) { if (!this._filterValue) {

View File

@ -540,7 +540,8 @@
"group": "Part of the following groups", "group": "Part of the following groups",
"scene": "Part of the following scenes", "scene": "Part of the following scenes",
"script": "Part of the following scripts", "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": { "data-table": {
"search": "Search", "search": "Search",
@ -2147,6 +2148,7 @@
"delete_confirm_text": "{name} will be permanently deleted.", "delete_confirm_text": "{name} will be permanently deleted.",
"duplicate": "[%key:ui::common::duplicate%]", "duplicate": "[%key:ui::common::duplicate%]",
"disabled": "Disabled", "disabled": "Disabled",
"filtered_by_blueprint": "blueprint: {name}",
"headers": { "headers": {
"toggle": "Enable/disable", "toggle": "Enable/disable",
"name": "Name", "name": "Name",
@ -2622,12 +2624,21 @@
"automation": "Automation", "automation": "Automation",
"script": "Script" "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_title": "Delete blueprint?",
"confirm_delete_text": "{name} will be permanently deleted.", "confirm_delete_text": "{name} will be permanently deleted.",
"add_blueprint": "Import blueprint", "add_blueprint": "Import blueprint",
"no_blueprints": "[%key:ui::panel::config::automation::editor::blueprint::no_blueprints%]", "no_blueprints": "[%key:ui::panel::config::automation::editor::blueprint::no_blueprints%]",
"create_automation": "Create automation", "create_automation": "Create automation",
"create_script": "Create script", "create_script": "Create script",
"view_automation": "Show automations using this blueprint",
"view_script": "Show scripts using this blueprint",
"delete_blueprint": "Delete blueprint", "delete_blueprint": "Delete blueprint",
"share_blueprint": "Share blueprint", "share_blueprint": "Share blueprint",
"share_blueprint_no_url": "Unable to share blueprint: no source url", "share_blueprint_no_url": "Unable to share blueprint: no source url",
@ -2662,6 +2673,7 @@
"run": "[%key:ui::panel::config::automation::editor::actions::run%]", "run": "[%key:ui::panel::config::automation::editor::actions::run%]",
"show_trace": "[%key:ui::panel::config::automation::editor::show_trace%]", "show_trace": "[%key:ui::panel::config::automation::editor::show_trace%]",
"show_info": "[%key:ui::panel::config::automation::editor::show_info%]", "show_info": "[%key:ui::panel::config::automation::editor::show_info%]",
"filtered_by_blueprint": "[%key:ui::panel::config::automation::picker::filtered_by_blueprint%]",
"headers": { "headers": {
"name": "Name", "name": "Name",
"state": "State" "state": "State"