1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-04-02 08:33:31 +01:00

Add configuration to built-in panels (#29572)

Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
This commit is contained in:
Paul Bottein
2026-02-23 16:15:54 +01:00
committed by GitHub
parent 1efd5d26f0
commit 758d955053
20 changed files with 577 additions and 54 deletions

View File

@@ -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,
});

View File

@@ -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")

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -102,11 +102,15 @@ export class DialogLovelaceDashboardDetail extends LitElement {
: html`
<ha-form
autofocus
.schema=${this._schema(this._params)}
.schema=${this._schema(
this._params,
this._data?.require_admin
)}
.data=${this._data}
.hass=${this.hass}
.error=${this._error}
.computeLabel=${this._computeLabel}
.computeHelper=${this._computeHelper}
@value-changed=${this._valueChanged}
></ha-form>
`}
@@ -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<ReturnType<typeof this._schema>>
): 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;

View File

@@ -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<string, string>;
@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`
<ha-dialog
.hass=${this.hass}
.open=${this._open}
prevent-scrim-close
header-title=${this.hass.localize(
"ui.panel.config.lovelace.dashboards.panel_detail.edit_panel"
)}
@closed=${this._dialogClosed}
>
<ha-dropdown slot="headerActionItems" placement="bottom-end">
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-dropdown-item @click=${this._resetPanel}>
<ha-svg-icon slot="icon" .path=${mdiRestart}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.lovelace.dashboards.panel_detail.reset_to_default"
)}
</ha-dropdown-item>
</ha-dropdown>
<ha-form
autofocus
.schema=${this._schema(
this._params.isDefault,
this._data.require_admin
)}
.data=${this._data}
.hass=${this.hass}
.error=${this._error}
.computeLabel=${this._computeLabel}
.computeHelper=${this._computeHelper}
@value-changed=${this._valueChanged}
></ha-form>
<ha-dialog-footer slot="footer">
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._updatePanel}
.disabled=${titleInvalid || this._submitting}
>
${this.hass.localize(
"ui.panel.config.lovelace.dashboards.detail.update"
)}
</ha-button>
</ha-dialog-footer>
</ha-dialog>
`;
}
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<ReturnType<typeof this._schema>>
): string =>
this.hass.localize(
`ui.panel.config.lovelace.dashboards.panel_detail.${entry.name}`
);
private _computeHelper = (
entry: SchemaUnion<ReturnType<typeof this._schema>>
): 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;
}
}

View File

@@ -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<void> {
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(

View File

@@ -8,6 +8,7 @@ import type {
export interface LovelaceDashboardDetailsDialogParams {
dashboard?: LovelaceDashboard;
urlPath?: string;
isDefault?: boolean;
createDashboard?: (values: LovelaceDashboardCreateParams) => Promise<unknown>;
updateDashboard: (
updates: Partial<LovelaceDashboardMutableParams>

View File

@@ -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<unknown>;
}
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,
});
};

View File

@@ -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();

View File

@@ -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();

View File

@@ -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
{

View File

@@ -796,7 +796,7 @@ class HuiEnergyDistrubutionCard
</svg>
</div>
</div>
${this._config.link_dashboard
${this._config.link_dashboard && this.hass.panels.energy
? html`
<div class="card-actions">
<ha-button

View File

@@ -102,10 +102,12 @@ export class HomeAreaViewStrategy extends ReactiveElement {
type: "heading",
heading: getSummaryLabel(hass.localize, "light"),
icon: HOME_SUMMARIES_ICONS.light,
tap_action: {
action: "navigate",
navigation_path: "/light?historyBack=1",
},
tap_action: hass.panels.light
? {
action: "navigate",
navigation_path: "/light?historyBack=1",
}
: undefined,
} satisfies HeadingCardConfig,
...light.map(computeTileCard),
],
@@ -120,10 +122,12 @@ export class HomeAreaViewStrategy extends ReactiveElement {
type: "heading",
heading: getSummaryLabel(hass.localize, "climate"),
icon: HOME_SUMMARIES_ICONS.climate,
tap_action: {
action: "navigate",
navigation_path: "/climate?historyBack=1",
},
tap_action: hass.panels.climate
? {
action: "navigate",
navigation_path: "/climate?historyBack=1",
}
: undefined,
} satisfies HeadingCardConfig,
...climate.map(computeTileCard),
],
@@ -138,10 +142,12 @@ export class HomeAreaViewStrategy extends ReactiveElement {
type: "heading",
heading: getSummaryLabel(hass.localize, "security"),
icon: HOME_SUMMARIES_ICONS.security,
tap_action: {
action: "navigate",
navigation_path: "/security?historyBack=1",
},
tap_action: hass.panels.security
? {
action: "navigate",
navigation_path: "/security?historyBack=1",
}
: undefined,
} satisfies HeadingCardConfig,
...security.map(computeTileCard),
],

View File

@@ -222,11 +222,16 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
generateEntityFilter(hass, filter)
);
const hasLights = findEntities(allEntities, lightsFilters).length > 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[] = [

View File

@@ -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`
<hui-root
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.route=${this.route}
.panel=${this.panel}
no-edit
></hui-root>
`;
}
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;
}
}

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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",

View File

@@ -141,6 +141,7 @@ export interface PanelInfo<T = Record<string, any> | null> {
url_path: string;
config_panel_domain?: string;
default_visible?: boolean;
require_admin?: boolean;
}
export type Panels = Record<string, PanelInfo>;