From 758d95505394369d894c2ef03c320ea870f71793 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 23 Feb 2026 16:15:54 +0100 Subject: [PATCH] Add configuration to built-in panels (#29572) Co-authored-by: Norbert Rittel Co-authored-by: Petar Petrov --- src/data/panel.ts | 43 ++- src/layouts/partial-panel-resolver.ts | 1 + src/panels/climate/ha-panel-climate.ts | 3 +- .../strategies/climate-view-strategy.ts | 10 +- .../dialog-lovelace-dashboard-detail.ts | 18 +- .../dashboards/dialog-panel-detail.ts | 247 ++++++++++++++++++ .../ha-config-lovelace-dashboards.ts | 69 ++++- .../show-dialog-lovelace-dashboard-detail.ts | 1 + .../dashboards/show-dialog-panel-detail.ts | 25 ++ src/panels/home/ha-panel-home.ts | 3 +- src/panels/light/ha-panel-light.ts | 3 +- .../light/strategies/light-view-strategy.ts | 10 +- .../energy/hui-energy-distribution-card.ts | 2 +- .../home/home-area-view-strategy.ts | 30 ++- .../home/home-overview-view-strategy.ts | 17 +- src/panels/notfound/ha-panel-notfound.ts | 110 ++++++++ src/panels/security/ha-panel-security.ts | 3 +- .../strategies/security-view-strategy.ts | 10 +- src/translations/en.json | 25 +- src/types.ts | 1 + 20 files changed, 577 insertions(+), 54 deletions(-) create mode 100644 src/panels/config/lovelace/dashboards/dialog-panel-detail.ts create mode 100644 src/panels/config/lovelace/dashboards/show-dialog-panel-detail.ts create mode 100644 src/panels/notfound/ha-panel-notfound.ts diff --git a/src/data/panel.ts b/src/data/panel.ts index fe55d4b719..1784aff83f 100644 --- a/src/data/panel.ts +++ b/src/data/panel.ts @@ -8,12 +8,17 @@ import { mdiPlayBoxMultiple, mdiTooltipAccount, } from "@mdi/js"; -import type { HomeAssistant, PanelInfo } from "../types"; -import type { PageNavigation } from "../layouts/hass-tabs-subpage"; import type { LocalizeKeys } from "../common/translations/localize"; +import type { PageNavigation } from "../layouts/hass-tabs-subpage"; +import type { HomeAssistant, PanelInfo } from "../types"; + +export const HOME_PANEL = "home"; +export const NOT_FOUND_PANEL = "notfound"; +export const PROFILE_PANEL = "profile"; +export const LOVELACE_PANEL = "lovelace"; /** Panel to show when no panel is picked. */ -export const DEFAULT_PANEL = "home"; +export const DEFAULT_PANEL = HOME_PANEL; export const hasLegacyOverviewPanel = (hass: HomeAssistant): boolean => Boolean(hass.panels.lovelace?.config); @@ -30,7 +35,7 @@ export const getDefaultPanelUrlPath = (hass: HomeAssistant): string => { getLegacyDefaultPanelUrlPath() || DEFAULT_PANEL; // If default panel is lovelace and no old overview exists, fall back to home - if (defaultPanel === "lovelace" && !hasLegacyOverviewPanel(hass)) { + if (defaultPanel === LOVELACE_PANEL && !hasLegacyOverviewPanel(hass)) { return DEFAULT_PANEL; } return defaultPanel; @@ -39,12 +44,16 @@ export const getDefaultPanelUrlPath = (hass: HomeAssistant): string => { export const getDefaultPanel = (hass: HomeAssistant): PanelInfo => { const panel = getDefaultPanelUrlPath(hass); - return (panel ? hass.panels[panel] : undefined) ?? hass.panels[DEFAULT_PANEL]; + return ( + (panel ? hass.panels[panel] : undefined) ?? + hass.panels[DEFAULT_PANEL] ?? + hass.panels[NOT_FOUND_PANEL] + ); }; export const getPanelNameTranslationKey = (panel: PanelInfo) => { - if (panel.url_path === "profile") { - return "panel.profile" as const; + if ([PROFILE_PANEL, NOT_FOUND_PANEL].includes(panel.url_path)) { + return `panel.${panel.url_path}` as const; } return `panel.${panel.title}` as const; @@ -137,4 +146,22 @@ export const PANEL_ICON_PATHS = { export const getPanelIconPath = (panel: PanelInfo): string | undefined => PANEL_ICON_PATHS[panel.url_path]; -export const FIXED_PANELS = ["profile", "config"]; +export const FIXED_PANELS = [PROFILE_PANEL, "config", NOT_FOUND_PANEL]; + +export interface PanelMutableParams { + title?: string | null; + icon?: string | null; + require_admin?: boolean | null; + show_in_sidebar?: boolean | null; +} + +export const updatePanel = ( + hass: HomeAssistant, + urlPath: string, + updates: PanelMutableParams +) => + hass.callWS({ + type: "frontend/update_panel", + url_path: urlPath, + ...updates, + }); diff --git a/src/layouts/partial-panel-resolver.ts b/src/layouts/partial-panel-resolver.ts index dc102b67b0..ac8b47ae9b 100644 --- a/src/layouts/partial-panel-resolver.ts +++ b/src/layouts/partial-panel-resolver.ts @@ -35,6 +35,7 @@ const COMPONENTS = { security: () => import("../panels/security/ha-panel-security"), climate: () => import("../panels/climate/ha-panel-climate"), home: () => import("../panels/home/ha-panel-home"), + notfound: () => import("../panels/notfound/ha-panel-notfound"), }; @customElement("partial-panel-resolver") diff --git a/src/panels/climate/ha-panel-climate.ts b/src/panels/climate/ha-panel-climate.ts index f61e98838c..12563a035d 100644 --- a/src/panels/climate/ha-panel-climate.ts +++ b/src/panels/climate/ha-panel-climate.ts @@ -58,7 +58,8 @@ class PanelClimate extends LitElement { oldHass.entities !== this.hass.entities || oldHass.devices !== this.hass.devices || oldHass.areas !== this.hass.areas || - oldHass.floors !== this.hass.floors + oldHass.floors !== this.hass.floors || + oldHass.panels !== this.hass.panels ) { if (this.hass.config.state === "RUNNING") { this._debounceRegistriesChanged(); diff --git a/src/panels/climate/strategies/climate-view-strategy.ts b/src/panels/climate/strategies/climate-view-strategy.ts index dbe3f70051..6ce9711e55 100644 --- a/src/panels/climate/strategies/climate-view-strategy.ts +++ b/src/panels/climate/strategies/climate-view-strategy.ts @@ -103,10 +103,12 @@ const processAreasForClimate = ( heading_style: "subtitle", type: "heading", heading: area.name, - tap_action: { - action: "navigate", - navigation_path: `/home/areas-${area.area_id}`, - }, + tap_action: hass.panels.home + ? { + action: "navigate", + navigation_path: `/home/areas-${area.area_id}`, + } + : undefined, }); cards.push(...areaCards); } diff --git a/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts b/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts index e605d783d2..4527266258 100644 --- a/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts +++ b/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts @@ -102,11 +102,15 @@ export class DialogLovelaceDashboardDetail extends LitElement { : html` `} @@ -155,7 +159,7 @@ export class DialogLovelaceDashboardDetail extends LitElement { } private _schema = memoizeOne( - (params: LovelaceDashboardDetailsDialogParams) => + (params: LovelaceDashboardDetailsDialogParams, requireAdmin?: boolean) => [ { name: "title", @@ -183,6 +187,7 @@ export class DialogLovelaceDashboardDetail extends LitElement { { name: "require_admin", required: true, + disabled: params.isDefault && !requireAdmin, selector: { boolean: {}, }, @@ -210,6 +215,15 @@ export class DialogLovelaceDashboardDetail extends LitElement { }` ); + private _computeHelper = ( + entry: SchemaUnion> + ): string => + entry.name === "require_admin" && entry.disabled + ? this.hass.localize( + "ui.panel.config.lovelace.dashboards.panel_detail.require_admin_helper" + ) + : ""; + private _valueChanged(ev: CustomEvent) { this._error = undefined; const value = ev.detail.value; diff --git a/src/panels/config/lovelace/dashboards/dialog-panel-detail.ts b/src/panels/config/lovelace/dashboards/dialog-panel-detail.ts new file mode 100644 index 0000000000..b918fcb2c5 --- /dev/null +++ b/src/panels/config/lovelace/dashboards/dialog-panel-detail.ts @@ -0,0 +1,247 @@ +import { mdiDotsVertical, mdiRestart } from "@mdi/js"; +import { html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-button"; +import "../../../../components/ha-dialog-footer"; +import "../../../../components/ha-dropdown"; +import "../../../../components/ha-dropdown-item"; +import "../../../../components/ha-form/ha-form"; +import "../../../../components/ha-icon-button"; +import "../../../../components/ha-svg-icon"; +import "../../../../components/ha-dialog"; +import type { SchemaUnion } from "../../../../components/ha-form/types"; +import type { PanelMutableParams } from "../../../../data/panel"; +import { haStyleDialog } from "../../../../resources/styles"; +import type { HomeAssistant } from "../../../../types"; +import type { PanelDetailDialogParams } from "./show-dialog-panel-detail"; + +interface PanelDetailData { + title: string; + icon?: string; + require_admin: boolean; + show_in_sidebar: boolean; +} + +@customElement("dialog-panel-detail") +export class DialogPanelDetail extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _params?: PanelDetailDialogParams; + + @state() private _data?: PanelDetailData; + + @state() private _error?: Record; + + @state() private _submitting = false; + + @state() private _open = false; + + public showDialog(params: PanelDetailDialogParams): void { + this._params = params; + this._error = undefined; + this._data = { + title: params.title, + icon: params.icon, + require_admin: params.requireAdmin, + show_in_sidebar: params.showInSidebar, + }; + this._open = true; + } + + public closeDialog(): void { + this._open = false; + } + + private _dialogClosed(): void { + this._params = undefined; + this._data = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render() { + if (!this._params || !this._data) { + return nothing; + } + + const titleInvalid = !this._data.title || !this._data.title.trim(); + + return html` + + + + + + ${this.hass.localize( + "ui.panel.config.lovelace.dashboards.panel_detail.reset_to_default" + )} + + + + + + + ${this.hass.localize("ui.common.cancel")} + + + ${this.hass.localize( + "ui.panel.config.lovelace.dashboards.detail.update" + )} + + + + `; + } + + private _schema = memoizeOne( + (isDefault: boolean, requireAdmin: boolean) => + [ + { + name: "title", + required: true, + selector: { text: {} }, + }, + { + name: "icon", + required: false, + selector: { icon: {} }, + }, + { + name: "require_admin", + required: true, + disabled: isDefault && !requireAdmin, + selector: { boolean: {} }, + }, + { + name: "show_in_sidebar", + required: true, + selector: { boolean: {} }, + }, + ] as const + ); + + private _computeLabel = ( + entry: SchemaUnion> + ): string => + this.hass.localize( + `ui.panel.config.lovelace.dashboards.panel_detail.${entry.name}` + ); + + private _computeHelper = ( + entry: SchemaUnion> + ): string => + entry.name === "require_admin" && entry.disabled + ? this.hass.localize( + "ui.panel.config.lovelace.dashboards.panel_detail.require_admin_helper" + ) + : ""; + + private _valueChanged(ev: CustomEvent) { + this._error = undefined; + this._data = ev.detail.value; + } + + private async _handleError(err: any) { + let localizedErrorMessage: string | undefined; + if (err?.translation_domain && err?.translation_key) { + const localize = await this.hass.loadBackendTranslation( + "exceptions", + err.translation_domain + ); + localizedErrorMessage = localize( + `component.${err.translation_domain}.exceptions.${err.translation_key}.message`, + err.translation_placeholders + ); + } + this._error = { + base: localizedErrorMessage || err?.message || "Unknown error", + }; + } + + private async _resetPanel() { + this._submitting = true; + try { + await this._params!.updatePanel({ + title: null, + icon: null, + require_admin: null, + show_in_sidebar: null, + }); + this.closeDialog(); + } catch (err: any) { + this._handleError(err); + } finally { + this._submitting = false; + } + } + + private async _updatePanel() { + this._submitting = true; + try { + const updates: PanelMutableParams = {}; + + if (this._data!.title !== this._params!.title) { + updates.title = this._data!.title; + } + if ((this._data!.icon || undefined) !== this._params!.icon) { + updates.icon = this._data!.icon || null; + } + if (this._data!.require_admin !== this._params!.requireAdmin) { + updates.require_admin = this._data!.require_admin; + } + if (this._data!.show_in_sidebar !== this._params!.showInSidebar) { + updates.show_in_sidebar = this._data!.show_in_sidebar; + } + + if (Object.keys(updates).length > 0) { + await this._params!.updatePanel(updates); + } + this.closeDialog(); + } catch (err: any) { + this._handleError(err); + } finally { + this._submitting = false; + } + } + + static styles = haStyleDialog; +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-panel-detail": DialogPanelDetail; + } +} diff --git a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts index 7fda1bbe74..0a23dd0e6c 100644 --- a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts +++ b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts @@ -50,8 +50,12 @@ import { DEFAULT_PANEL, getPanelIcon, getPanelTitle, + updatePanel, } from "../../../../data/panel"; -import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../../dialogs/generic/show-dialog-box"; import "../../../../layouts/hass-loading-screen"; import "../../../../layouts/hass-tabs-subpage-data-table"; import type { HomeAssistant, Route } from "../../../../types"; @@ -60,6 +64,7 @@ import { showNewDashboardDialog } from "../../dashboard/show-dialog-new-dashboar import { lovelaceTabs } from "../ha-config-lovelace"; import { showDashboardConfigureStrategyDialog } from "./show-dialog-lovelace-dashboard-configure-strategy"; import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail"; +import { showPanelDetailDialog } from "./show-dialog-panel-detail"; export const PANEL_DASHBOARDS = [ "home", @@ -282,6 +287,17 @@ export class HaConfigLovelaceDashboards extends LitElement { action: () => this._handleSetAsDefault(dashboard), disabled: dashboard.default, }, + ...(dashboard.type === "built_in" + ? [ + { + path: mdiPencil, + label: this.hass.localize( + "ui.panel.config.lovelace.dashboards.picker.edit" + ), + action: () => this._handleEditPanel(dashboard), + }, + ] + : []), ...(dashboard.type === "user_created" && dashboard.mode === "storage" ? [ @@ -313,23 +329,27 @@ export class HaConfigLovelaceDashboards extends LitElement { ); private _getItems = memoize( - (dashboards: LovelaceDashboard[], defaultUrlPath: string | null) => { + ( + dashboards: LovelaceDashboard[], + defaultUrlPath: string | null, + panels: HomeAssistant["panels"] + ) => { const result: DataTableItem[] = []; PANEL_DASHBOARDS.forEach((panel) => { - const panelInfo = this.hass.panels[panel]; + const panelInfo = panels[panel]; if (!panelInfo) { return; } const item: DataTableItem = { icon: getPanelIcon(panelInfo), title: getPanelTitle(this.hass, panelInfo) || panelInfo.url_path, - show_in_sidebar: true, + show_in_sidebar: panelInfo.title != null, mode: "storage", url_path: panelInfo.url_path, filename: "", default: defaultUrlPath === panelInfo.url_path, - require_admin: false, + require_admin: panelInfo.require_admin || false, type: "built_in", localized_type: this._localizeType("built_in"), }; @@ -381,7 +401,11 @@ export class HaConfigLovelaceDashboards extends LitElement { this._dashboards, this.hass.localize )} - .data=${this._getItems(this._dashboards, defaultPanel)} + .data=${this._getItems( + this._dashboards, + defaultPanel, + this.hass.panels + )} .initialGroupColumn=${this._activeGrouping} .initialCollapsedGroups=${this._activeCollapsed} .initialSorting=${this._activeSorting} @@ -452,11 +476,42 @@ export class HaConfigLovelaceDashboards extends LitElement { this._openDetailDialog(dashboard, urlPath); } + private _handleEditPanel(item: DataTableItem) { + const panelInfo = this.hass.panels[item.url_path]; + if (!panelInfo) { + return; + } + const defaultPanel = this.hass.systemData?.default_panel || DEFAULT_PANEL; + showPanelDetailDialog(this, { + urlPath: panelInfo.url_path, + title: getPanelTitle(this.hass, panelInfo) || panelInfo.url_path, + icon: getPanelIcon(panelInfo), + requireAdmin: panelInfo.require_admin || false, + showInSidebar: panelInfo.title != null, + isDefault: panelInfo.url_path === defaultPanel, + updatePanel: async (values) => { + await updatePanel(this.hass!, panelInfo.url_path, values); + }, + }); + } + private _handleSetAsDefault = async (item: DataTableItem) => { if (item.default) { return; } + if (item.require_admin) { + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.lovelace.dashboards.detail.set_default_admin_only_title" + ), + text: this.hass.localize( + "ui.panel.config.lovelace.dashboards.detail.set_default_admin_only_text" + ), + }); + return; + } + const confirm = await showConfirmationDialog(this, { title: this.hass.localize( "ui.panel.config.lovelace.dashboards.detail.set_default_confirm_title" @@ -524,9 +579,11 @@ export class HaConfigLovelaceDashboards extends LitElement { urlPath?: string, defaultConfig?: LovelaceRawConfig ): Promise { + const defaultPanel = this.hass.systemData?.default_panel || DEFAULT_PANEL; showDashboardDetailDialog(this, { dashboard, urlPath, + isDefault: dashboard?.url_path === defaultPanel, createDashboard: async (values: LovelaceDashboardCreateParams) => { const created = await createDashboard(this.hass!, values); this._dashboards = this._dashboards!.concat(created).sort( diff --git a/src/panels/config/lovelace/dashboards/show-dialog-lovelace-dashboard-detail.ts b/src/panels/config/lovelace/dashboards/show-dialog-lovelace-dashboard-detail.ts index 3f9e6c8fdf..a6d6f18add 100644 --- a/src/panels/config/lovelace/dashboards/show-dialog-lovelace-dashboard-detail.ts +++ b/src/panels/config/lovelace/dashboards/show-dialog-lovelace-dashboard-detail.ts @@ -8,6 +8,7 @@ import type { export interface LovelaceDashboardDetailsDialogParams { dashboard?: LovelaceDashboard; urlPath?: string; + isDefault?: boolean; createDashboard?: (values: LovelaceDashboardCreateParams) => Promise; updateDashboard: ( updates: Partial diff --git a/src/panels/config/lovelace/dashboards/show-dialog-panel-detail.ts b/src/panels/config/lovelace/dashboards/show-dialog-panel-detail.ts new file mode 100644 index 0000000000..5de590a50f --- /dev/null +++ b/src/panels/config/lovelace/dashboards/show-dialog-panel-detail.ts @@ -0,0 +1,25 @@ +import { fireEvent } from "../../../../common/dom/fire_event"; +import type { PanelMutableParams } from "../../../../data/panel"; + +export interface PanelDetailDialogParams { + urlPath: string; + title: string; + icon?: string; + requireAdmin: boolean; + showInSidebar: boolean; + isDefault: boolean; + updatePanel: (updates: PanelMutableParams) => Promise; +} + +export const loadPanelDetailDialog = () => import("./dialog-panel-detail"); + +export const showPanelDetailDialog = ( + element: HTMLElement, + dialogParams: PanelDetailDialogParams +) => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-panel-detail", + dialogImport: loadPanelDetailDialog, + dialogParams, + }); +}; diff --git a/src/panels/home/ha-panel-home.ts b/src/panels/home/ha-panel-home.ts index e15495d13c..26c2b942a4 100644 --- a/src/panels/home/ha-panel-home.ts +++ b/src/panels/home/ha-panel-home.ts @@ -101,7 +101,8 @@ class PanelHome extends LitElement { oldHass.entities !== this.hass.entities || oldHass.devices !== this.hass.devices || oldHass.areas !== this.hass.areas || - oldHass.floors !== this.hass.floors + oldHass.floors !== this.hass.floors || + oldHass.panels !== this.hass.panels ) { if (this.hass.config.state === "RUNNING") { this._debounceRegistriesChanged(); diff --git a/src/panels/light/ha-panel-light.ts b/src/panels/light/ha-panel-light.ts index f2036e3415..251a7be866 100644 --- a/src/panels/light/ha-panel-light.ts +++ b/src/panels/light/ha-panel-light.ts @@ -58,7 +58,8 @@ class PanelLight extends LitElement { oldHass.entities !== this.hass.entities || oldHass.devices !== this.hass.devices || oldHass.areas !== this.hass.areas || - oldHass.floors !== this.hass.floors + oldHass.floors !== this.hass.floors || + oldHass.panels !== this.hass.panels ) { if (this.hass.config.state === "RUNNING") { this._debounceRegistriesChanged(); diff --git a/src/panels/light/strategies/light-view-strategy.ts b/src/panels/light/strategies/light-view-strategy.ts index b9c94269b6..5294d52a18 100644 --- a/src/panels/light/strategies/light-view-strategy.ts +++ b/src/panels/light/strategies/light-view-strategy.ts @@ -64,10 +64,12 @@ const processAreasForLight = ( heading_style: "subtitle", type: "heading", heading: area.name, - tap_action: { - action: "navigate", - navigation_path: `/home/areas-${area.area_id}`, - }, + tap_action: hass.panels.home + ? { + action: "navigate", + navigation_path: `/home/areas-${area.area_id}`, + } + : undefined, badges: [ // Toggle buttons for mobile { diff --git a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts index 05b23294ea..45e429825c 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts @@ -796,7 +796,7 @@ class HuiEnergyDistrubutionCard - ${this._config.link_dashboard + ${this._config.link_dashboard && this.hass.panels.energy ? html`
0; + const hasLights = + hass.panels.light && findEntities(allEntities, lightsFilters).length > 0; const hasMediaPlayers = findEntities(allEntities, mediaPlayerFilter).length > 0; - const hasClimate = findEntities(allEntities, climateFilters).length > 0; - const hasSecurity = findEntities(allEntities, securityFilters).length > 0; + const hasClimate = + hass.panels.climate && + findEntities(allEntities, climateFilters).length > 0; + const hasSecurity = + hass.panels.security && + findEntities(allEntities, securityFilters).length > 0; const weatherFilter = generateEntityFilter(hass, { domain: "weather", @@ -243,9 +248,11 @@ export class HomeOverviewViewStrategy extends ReactiveElement { : undefined; const hasEnergy = - energyPrefs?.energy_sources.some( + hass.panels.energy && + (energyPrefs?.energy_sources.some( (source) => source.type === "grid" && !!source.stat_energy_from - ) ?? false; + ) ?? + false); // Build summary cards (used in both mobile section and sidebar) const summaryCards: LovelaceCardConfig[] = [ diff --git a/src/panels/notfound/ha-panel-notfound.ts b/src/panels/notfound/ha-panel-notfound.ts new file mode 100644 index 0000000000..924119da1f --- /dev/null +++ b/src/panels/notfound/ha-panel-notfound.ts @@ -0,0 +1,110 @@ +import type { PropertyValues } from "lit"; +import { LitElement, html, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import type { LovelaceConfig } from "../../data/lovelace/config/types"; +import { NOT_FOUND_PANEL } from "../../data/panel"; +import type { HomeAssistant, PanelInfo, Route } from "../../types"; +import type { EmptyStateCardConfig } from "../lovelace/cards/types"; +import "../lovelace/hui-root"; +import type { Lovelace } from "../lovelace/types"; + +@customElement("ha-panel-notfound") +class HaPanelNotFound extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public narrow = false; + + @property({ attribute: false }) public route?: Route; + + @property({ attribute: false }) public panel?: PanelInfo; + + @state() private _lovelace?: Lovelace; + + public willUpdate(changedProps: PropertyValues) { + if (!this.hasUpdated) { + this._setup(); + return; + } + + if (changedProps.has("hass")) { + const oldHass = changedProps.get("hass") as this["hass"]; + if (oldHass && oldHass.localize !== this.hass.localize) { + this._setLovelace(); + } + } + } + + private async _setup() { + await this.hass.loadFragmentTranslation("lovelace"); + this._setLovelace(); + } + + protected render() { + if (!this._lovelace) { + return nothing; + } + + return html` + + `; + } + + private _setLovelace() { + const config: LovelaceConfig = { + views: [ + { + type: "panel", + cards: [ + { + type: "empty-state", + content_only: true, + icon: "mdi:lock", + title: this.hass.localize("ui.panel.notfound.no_access_title"), + content: this.hass.localize( + "ui.panel.notfound.no_access_content" + ), + buttons: [ + { + text: this.hass.localize( + "ui.panel.notfound.no_access_go_to_profile" + ), + tap_action: { + action: "navigate", + navigation_path: "/profile/general", + }, + }, + ], + } as EmptyStateCardConfig, + ], + }, + ], + }; + + this._lovelace = { + config: config, + rawConfig: config, + editMode: false, + urlPath: NOT_FOUND_PANEL, + mode: "generated", + locale: this.hass.locale, + enableFullEditMode: () => undefined, + saveConfig: async () => undefined, + deleteConfig: async () => undefined, + setEditMode: () => undefined, + showToast: () => undefined, + }; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-panel-notfound": HaPanelNotFound; + } +} diff --git a/src/panels/security/ha-panel-security.ts b/src/panels/security/ha-panel-security.ts index baeff65736..b6088e7f5a 100644 --- a/src/panels/security/ha-panel-security.ts +++ b/src/panels/security/ha-panel-security.ts @@ -58,7 +58,8 @@ class PanelSecurity extends LitElement { oldHass.entities !== this.hass.entities || oldHass.devices !== this.hass.devices || oldHass.areas !== this.hass.areas || - oldHass.floors !== this.hass.floors + oldHass.floors !== this.hass.floors || + oldHass.panels !== this.hass.panels ) { if (this.hass.config.state === "RUNNING") { this._debounceRegistriesChanged(); diff --git a/src/panels/security/strategies/security-view-strategy.ts b/src/panels/security/strategies/security-view-strategy.ts index 5f88aeb5d8..49a2192c60 100644 --- a/src/panels/security/strategies/security-view-strategy.ts +++ b/src/panels/security/strategies/security-view-strategy.ts @@ -91,10 +91,12 @@ const processAreasForSecurity = ( heading_style: "subtitle", type: "heading", heading: area.name, - tap_action: { - action: "navigate", - navigation_path: `/home/areas-${area.area_id}`, - }, + tap_action: hass.panels.home + ? { + action: "navigate", + navigation_path: `/home/areas-${area.area_id}`, + } + : undefined, }); cards.push(...areaCards); } diff --git a/src/translations/en.json b/src/translations/en.json index 2c95d77b0a..8866f81cb0 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -15,7 +15,8 @@ "light": "Lights", "security": "Security", "climate": "Climate", - "home": "Overview" + "home": "Overview", + "notfound": "Page not found" }, "state": { "default": { @@ -4251,7 +4252,7 @@ "title": "Title", "conf_mode": "Configuration method", "default": "Default", - "require_admin": "Admin only", + "require_admin": "Admin-only", "sidebar": "In sidebar", "filename": "Filename", "url": "Open", @@ -4280,7 +4281,7 @@ "title_required": "Title is required.", "url": "URL", "url_error_msg": "The URL should contain a - and cannot contain spaces or special characters, except for _ and -", - "require_admin": "Admin only", + "require_admin": "Admin-only", "delete": "Delete", "update": "Update", "create": "Create", @@ -4288,7 +4289,18 @@ "remove_default": "Remove as default", "set_default_confirm_title": "Set as default dashboard?", "set_default_confirm_text": "This dashboard will be shown to all users when opening Home Assistant.", - "set_default_confirm_note": "Users who have chosen a specific dashboard in their profile will not be affected. They must set it back to \"Auto (use system settings)\" to use this dashboard." + "set_default_confirm_note": "Users who have chosen a specific dashboard in their profile will not be affected. They must set it back to \"Auto (use system settings)\" to use this dashboard.", + "set_default_admin_only_title": "Can't set as default", + "set_default_admin_only_text": "This dashboard is set to admin-only. Disable this limitation before setting it as default." + }, + "panel_detail": { + "edit_panel": "Edit panel", + "title": "[%key:ui::panel::config::lovelace::dashboards::detail::title%]", + "icon": "[%key:ui::panel::config::lovelace::dashboards::detail::icon%]", + "require_admin": "[%key:ui::panel::config::lovelace::dashboards::detail::require_admin%]", + "show_in_sidebar": "[%key:ui::panel::config::lovelace::dashboards::detail::show_sidebar%]", + "reset_to_default": "Reset to default", + "require_admin_helper": "Can't be enabled because this dashboard is set as default" } }, "resources": { @@ -9618,6 +9630,11 @@ "map": { "edit_zones": "Edit zones" }, + "notfound": { + "no_access_title": "No access", + "no_access_content": "You don't have access to this page. Contact an administrator or change the default dashboard in your profile settings.", + "no_access_go_to_profile": "Go to profile" + }, "profile": { "tabs": { "general": "General", diff --git a/src/types.ts b/src/types.ts index 963b70e779..90cc90de76 100644 --- a/src/types.ts +++ b/src/types.ts @@ -141,6 +141,7 @@ export interface PanelInfo | null> { url_path: string; config_panel_domain?: string; default_visible?: boolean; + require_admin?: boolean; } export type Panels = Record;