Compare commits

..

10 Commits

Author SHA1 Message Date
Paul Bottein
6a99558237 Add other panels to profile 2025-11-21 15:32:54 +01:00
Paul Bottein
c6b65cf4cd Simplify navigation picker 2025-11-21 15:32:54 +01:00
Paul Bottein
000e1101c4 Simplify lovelace dashboard logic 2025-11-21 15:32:54 +01:00
Paul Bottein
785e6e4e86 Improve sidebar logic with default panel 2025-11-21 15:32:54 +01:00
Paul Bottein
0735d3614c Allow to set every dashboard as default 2025-11-21 15:32:54 +01:00
Paul Bottein
e3f243eb6b Refactor panel configuration 2025-11-21 15:32:54 +01:00
Paul Bottein
e09f69eeb3 Add reset to default sidebar button 2025-11-21 15:32:54 +01:00
Paul Bottein
fb15c7f87e Simplify sidebar logic 2025-11-21 15:32:54 +01:00
Paul Bottein
0595f722f3 Add basic editor to edit favorites entities for home panel (#28028)
* Add basic editor to edit favorites entities for home panel

* Rename favorites

* Rename favorites

* Feedbacks
2025-11-21 16:19:39 +02:00
Petar Petrov
1c0315854a Hide echarts toolbox better (#28030) 2025-11-21 14:52:20 +02:00
17 changed files with 571 additions and 334 deletions

View File

@@ -597,10 +597,15 @@ export class HaChartBase extends LitElement {
aria: { show: true },
dataZoom: this._getDataZoomConfig(),
toolbox: {
top: Infinity,
left: Infinity,
top: Number.MAX_SAFE_INTEGER,
left: Number.MAX_SAFE_INTEGER,
feature: {
dataZoom: { show: true, yAxisIndex: false, filterMode: "none" },
dataZoom: {
show: true,
yAxisIndex: false,
filterMode: "none",
showTitle: false,
},
},
iconStyle: { opacity: 0 },
},

View File

@@ -66,7 +66,7 @@ export class HaIconOverflowMenu extends LitElement {
.path=${item.path}
></ha-svg-icon>
${item.label}
</ha-md-menu-item> `
</ha-md-menu-item>`
)}
</ha-md-button-menu>`
: html`
@@ -103,6 +103,7 @@ export class HaIconOverflowMenu extends LitElement {
:host {
display: flex;
justify-content: flex-end;
cursor: initial;
}
div[role="separator"] {
border-right: 1px solid var(--divider-color);

View File

@@ -27,6 +27,7 @@ export interface DisplayItem {
label: string;
description?: string;
disableSorting?: boolean;
disableHiding?: boolean;
}
export interface DisplayValue {
@@ -101,6 +102,7 @@ export class HaItemDisplayEditor extends LitElement {
icon,
iconPath,
disableSorting,
disableHiding,
} = item;
return html`
<ha-md-list-item
@@ -155,18 +157,21 @@ export class HaItemDisplayEditor extends LitElement {
</div>
`
: nothing}
<ha-icon-button
.path=${isVisible ? mdiEye : mdiEyeOff}
slot="end"
.label=${this.hass.localize(
`ui.components.items-display-editor.${isVisible ? "hide" : "show"}`,
{
label: label,
}
)}
.value=${value}
@click=${this._toggle}
></ha-icon-button>
${isVisible && !disableHiding
? html`<ha-icon-button
.path=${isVisible ? mdiEye : mdiEyeOff}
slot="end"
.label=${this.hass.localize(
`ui.components.items-display-editor.${isVisible ? "hide" : "show"}`,
{
label: label,
}
)}
.value=${value}
@click=${this._toggle}
.disabled=${disableHiding || false}
></ha-icon-button>`
: nothing}
${isVisible && !disableSorting
? html`
<ha-svg-icon

View File

@@ -36,6 +36,11 @@ export class HaMdMenuItem extends MenuItemEl {
::slotted([slot="headline"]) {
text-wrap: nowrap;
}
:host([disabled]) {
opacity: 1;
--md-menu-item-label-text-color: var(--disabled-text-color);
--md-menu-item-leading-icon-color: var(--disabled-text-color);
}
`,
];
}

View File

@@ -6,7 +6,7 @@ import { fireEvent } from "../common/dom/fire_event";
import { titleCase } from "../common/string/title-case";
import { fetchConfig } from "../data/lovelace/config/types";
import type { LovelaceViewRawConfig } from "../data/lovelace/config/view";
import { getDefaultPanelUrlPath } from "../data/panel";
import { getPanelIcon, getPanelTitle } from "../data/panel";
import type { HomeAssistant, PanelInfo, ValueChangedEvent } from "../types";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
@@ -43,13 +43,8 @@ const createViewNavigationItem = (
const createPanelNavigationItem = (hass: HomeAssistant, panel: PanelInfo) => ({
path: `/${panel.url_path}`,
icon: panel.icon ?? "mdi:view-dashboard",
title:
panel.url_path === getDefaultPanelUrlPath(hass)
? hass.localize("panel.states")
: hass.localize(`panel.${panel.title}`) ||
panel.title ||
(panel.url_path ? titleCase(panel.url_path) : ""),
icon: getPanelIcon(panel) || "mdi:view-dashboard",
title: getPanelTitle(hass, panel) || "",
});
@customElement("ha-navigation-picker")

View File

@@ -16,7 +16,7 @@ import {
} from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { css, html, LitElement, nothing } from "lit";
import {
customElement,
eventOptions,
@@ -33,7 +33,14 @@ import { computeRTL } from "../common/util/compute_rtl";
import { throttle } from "../common/util/throttle";
import { subscribeFrontendUserData } from "../data/frontend";
import type { ActionHandlerDetail } from "../data/lovelace/action_handler";
import { getDefaultPanelUrlPath } from "../data/panel";
import {
FIXED_PANELS,
getDefaultPanelUrlPath,
getPanelIcon,
getPanelIconPath,
getPanelTitle,
SHOW_AFTER_SPACER_PANELS,
} from "../data/panel";
import type { PersistentNotification } from "../data/persistent_notification";
import { subscribeNotifications } from "../data/persistent_notification";
import { subscribeRepairsIssueRegistry } from "../data/repairs";
@@ -54,8 +61,6 @@ import "./ha-spinner";
import "./ha-svg-icon";
import "./user/ha-user-badge";
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
const SORT_VALUE_URL_PATHS = {
@@ -67,7 +72,7 @@ const SORT_VALUE_URL_PATHS = {
config: 11,
};
export const PANEL_ICONS = {
export const PANEL_ICON_PATHS = {
calendar: mdiCalendar,
"developer-tools": mdiHammer,
energy: mdiLightningBolt,
@@ -155,16 +160,23 @@ export const computePanels = memoizeOne(
const beforeSpacer: PanelInfo[] = [];
const afterSpacer: PanelInfo[] = [];
Object.values(panels).forEach((panel) => {
const allPanels = Object.values(panels).filter(
(panel) => !FIXED_PANELS.includes(panel.url_path)
);
allPanels.forEach((panel) => {
const isDefaultPanel = panel.url_path === defaultPanel;
if (
hiddenPanels.includes(panel.url_path) ||
(!panel.title && panel.url_path !== defaultPanel) ||
(panel.default_visible === false &&
!panelsOrder.includes(panel.url_path))
!isDefaultPanel &&
(!panel.title ||
hiddenPanels.includes(panel.url_path) ||
(panel.default_visible === false &&
!panelsOrder.includes(panel.url_path)))
) {
return;
}
(SHOW_AFTER_SPACER.includes(panel.url_path)
(SHOW_AFTER_SPACER_PANELS.includes(panel.url_path)
? afterSpacer
: beforeSpacer
).push(panel);
@@ -252,9 +264,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
}
// Show the supervisor as being part of configuration
const selectedPanel = this.route.path?.startsWith("/hassio/")
? "config"
: this.hass.panelUrl;
const selectedPanel = this.hass.panelUrl;
// prettier-ignore
return html`
@@ -397,9 +407,9 @@ class HaSidebar extends SubscribeMixin(LitElement) {
private _renderAllPanels(selectedPanel: string) {
if (!this._panelOrder || !this._hiddenPanels) {
return html`
<ha-fade-in .delay=${500}
><ha-spinner size="small"></ha-spinner
></ha-fade-in>
<ha-fade-in .delay=${500}>
<ha-spinner size="small"></ha-spinner>
</ha-fade-in>
`;
}
@@ -413,7 +423,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
this.hass.locale
);
// prettier-ignore
return html`
<ha-md-list
class="ha-scrollbar"
@@ -422,61 +431,42 @@ class HaSidebar extends SubscribeMixin(LitElement) {
@scroll=${this._listboxScroll}
@keydown=${this._listboxKeydown}
>
${this._renderPanels(beforeSpacer, selectedPanel, defaultPanel)}
${this._renderPanels(beforeSpacer, selectedPanel)}
${this._renderSpacer()}
${this._renderPanels(afterSpacer, selectedPanel, defaultPanel)}
${this._renderExternalConfiguration()}
${this._renderPanels(afterSpacer, selectedPanel)}
${this.hass.user?.is_admin
? this._renderConfiguration(selectedPanel)
: this._renderExternalConfiguration()}
</ha-md-list>
`;
}
private _renderPanels(
panels: PanelInfo[],
selectedPanel: string,
defaultPanel: string
) {
private _renderPanels(panels: PanelInfo[], selectedPanel: string) {
return panels.map((panel) =>
this._renderPanel(
panel.url_path,
panel.url_path === defaultPanel
? panel.title || this.hass.localize("panel.states")
: this.hass.localize(`panel.${panel.title}`) || panel.title,
panel.icon,
panel.url_path === defaultPanel && !panel.icon
? PANEL_ICONS.lovelace
: panel.url_path in PANEL_ICONS
? PANEL_ICONS[panel.url_path]
: undefined,
selectedPanel
)
this._renderPanel(panel, panel.url_path === selectedPanel)
);
}
private _renderPanel(
urlPath: string,
title: string | null,
icon: string | null | undefined,
iconPath: string | null | undefined,
selectedPanel: string
) {
return urlPath === "config"
? this._renderConfiguration(title, selectedPanel)
: html`
<ha-md-list-item
.href=${`/${urlPath}`}
type="link"
class=${classMap({
selected: selectedPanel === urlPath,
})}
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
${iconPath
? html`<ha-svg-icon slot="start" .path=${iconPath}></ha-svg-icon>`
: html`<ha-icon slot="start" .icon=${icon}></ha-icon>`}
<span class="item-text" slot="headline">${title}</span>
</ha-md-list-item>
`;
private _renderPanel(panel: PanelInfo, isSelected: boolean) {
const title = getPanelTitle(this.hass, panel);
const urlPath = panel.url_path;
const icon = getPanelIcon(panel);
const iconPath = getPanelIconPath(panel);
return html`
<ha-md-list-item
.href=${`/${urlPath}`}
type="link"
class=${classMap({ selected: isSelected })}
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
${iconPath
? html`<ha-svg-icon slot="start" .path=${iconPath}></ha-svg-icon>`
: html`<ha-icon slot="start" .icon=${icon}></ha-icon>`}
<span class="item-text" slot="headline">${title}</span>
</ha-md-list-item>
`;
}
private _renderDivider() {
@@ -487,10 +477,15 @@ class HaSidebar extends SubscribeMixin(LitElement) {
return html`<div class="spacer" disabled></div>`;
}
private _renderConfiguration(title: string | null, selectedPanel: string) {
private _renderConfiguration(selectedPanel: string) {
if (!this.hass.user?.is_admin) {
return nothing;
}
const isSelected =
selectedPanel === "config" || this.route.path?.startsWith("/hassio/");
return html`
<ha-md-list-item
class="configuration${selectedPanel === "config" ? " selected" : ""}"
class="configuration ${classMap({ selected: isSelected })}"
type="button"
href="/config"
@mouseenter=${this._itemMouseEnter}
@@ -504,15 +499,17 @@ class HaSidebar extends SubscribeMixin(LitElement) {
${this._updatesCount + this._issuesCount}
</span>
`
: ""}
<span class="item-text" slot="headline">${title}</span>
: nothing}
<span class="item-text" slot="headline"
>${this.hass.localize("panel.config")}</span
>
${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
? html`
<span class="badge" slot="end"
>${this._updatesCount + this._issuesCount}</span
>
`
: ""}
: nothing}
</ha-md-list-item>
`;
}
@@ -535,19 +532,20 @@ class HaSidebar extends SubscribeMixin(LitElement) {
? html`
<span class="badge" slot="start"> ${notificationCount} </span>
`
: ""}
: nothing}
<span class="item-text" slot="headline"
>${this.hass.localize("ui.notification_drawer.title")}</span
>
${this.alwaysExpand && notificationCount > 0
? html`<span class="badge" slot="end">${notificationCount}</span>`
: ""}
: nothing}
</ha-md-list-item>
`;
}
private _renderUserItem(selectedPanel: string) {
const isRTL = computeRTL(this.hass);
const isSelected = selectedPanel === "profile";
return html`
<ha-md-list-item
@@ -555,7 +553,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
type="link"
class=${classMap({
user: true,
selected: selectedPanel === "profile",
selected: isSelected,
rtl: isRTL,
})}
@mouseenter=${this._itemMouseEnter}
@@ -566,31 +564,30 @@ class HaSidebar extends SubscribeMixin(LitElement) {
.user=${this.hass.user}
.hass=${this.hass}
></ha-user-badge>
<span class="item-text" slot="headline"
>${this.hass.user ? this.hass.user.name : ""}</span
>
<span class="item-text" slot="headline">
${this.hass.user ? this.hass.user.name : ""}
</span>
</ha-md-list-item>
`;
}
private _renderExternalConfiguration() {
return html`${!this.hass.user?.is_admin &&
this.hass.auth.external?.config.hasSettingsScreen
? html`
<ha-md-list-item
@click=${this._handleExternalAppConfiguration}
type="button"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<ha-svg-icon slot="start" .path=${mdiCellphoneCog}></ha-svg-icon>
<span class="item-text" slot="headline">
${this.hass.localize("ui.sidebar.external_app_configuration")}
</span>
</ha-md-list-item>
`
: ""}`;
if (!this.hass.auth.external?.config.hasSettingsScreen) {
return nothing;
}
return html`
<ha-md-list-item
@click=${this._handleExternalAppConfiguration}
type="button"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<ha-svg-icon slot="start" .path=${mdiCellphoneCog}></ha-svg-icon>
<span class="item-text" slot="headline">
${this.hass.localize("ui.sidebar.external_app_configuration")}
</span>
</ha-md-list-item>
`;
}
private _handleExternalAppConfiguration(ev: Event) {

View File

@@ -7,14 +7,18 @@ export interface CoreFrontendUserData {
}
export interface SidebarFrontendUserData {
panelOrder: string[];
hiddenPanels: string[];
panelOrder?: string[];
hiddenPanels?: string[];
}
export interface CoreFrontendSystemData {
defaultPanel?: string;
}
export interface HomeFrontendSystemData {
favorite_entities?: string[];
}
declare global {
interface FrontendUserData {
core: CoreFrontendUserData;
@@ -22,6 +26,7 @@ declare global {
}
interface FrontendSystemData {
core: CoreFrontendSystemData;
home: HomeFrontendSystemData;
}
}

View File

@@ -1,3 +1,15 @@
import {
mdiAccount,
mdiCalendar,
mdiChartBox,
mdiClipboardList,
mdiFormatListBulletedType,
mdiHammer,
mdiLightningBolt,
mdiPlayBoxMultiple,
mdiTooltipAccount,
mdiViewDashboard,
} from "@mdi/js";
import type { HomeAssistant, PanelInfo } from "../types";
/** Panel to show when no panel is picked. */
@@ -60,7 +72,7 @@ export const getPanelTitleFromUrlPath = (
return getPanelTitle(hass, panel);
};
export const getPanelIcon = (panel: PanelInfo): string | null => {
export const getPanelIcon = (panel: PanelInfo): string | undefined => {
if (!panel.icon) {
switch (panel.component_name) {
case "profile":
@@ -70,5 +82,24 @@ export const getPanelIcon = (panel: PanelInfo): string | null => {
}
}
return panel.icon;
return panel.icon || undefined;
};
export const PANEL_ICON_PATHS = {
calendar: mdiCalendar,
"developer-tools": mdiHammer,
energy: mdiLightningBolt,
history: mdiChartBox,
logbook: mdiFormatListBulletedType,
lovelace: mdiViewDashboard,
profile: mdiAccount,
map: mdiTooltipAccount,
"media-browser": mdiPlayBoxMultiple,
todo: mdiClipboardList,
};
export const getPanelIconPath = (panel: PanelInfo): string | undefined =>
PANEL_ICON_PATHS[panel.url_path];
export const FIXED_PANELS = ["profile", "config"];
export const SHOW_AFTER_SPACER_PANELS = ["developer-tools"];

View File

@@ -1,5 +1,5 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import { mdiClose } from "@mdi/js";
import { mdiClose, mdiDotsVertical, mdiRestart } from "@mdi/js";
import { css, html, LitElement, nothing, type TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -9,18 +9,30 @@ import "../../components/ha-dialog-header";
import "../../components/ha-fade-in";
import "../../components/ha-icon-button";
import "../../components/ha-items-display-editor";
import type { DisplayValue } from "../../components/ha-items-display-editor";
import type {
DisplayItem,
DisplayValue,
} from "../../components/ha-items-display-editor";
import "../../components/ha-md-button-menu";
import "../../components/ha-md-dialog";
import type { HaMdDialog } from "../../components/ha-md-dialog";
import { computePanels, PANEL_ICONS } from "../../components/ha-sidebar";
import "../../components/ha-md-menu-item";
import { computePanels } from "../../components/ha-sidebar";
import "../../components/ha-spinner";
import "../../components/ha-svg-icon";
import {
fetchFrontendUserData,
saveFrontendUserData,
} from "../../data/frontend";
import {
getDefaultPanelUrlPath,
getPanelIcon,
getPanelIconPath,
getPanelTitle,
SHOW_AFTER_SPACER_PANELS,
} from "../../data/panel";
import type { HomeAssistant } from "../../types";
import { showConfirmationDialog } from "../generic/show-dialog-box";
import { getDefaultPanelUrlPath } from "../../data/panel";
@customElement("dialog-edit-sidebar")
class DialogEditSidebar extends LitElement {
@@ -105,48 +117,53 @@ class DialogEditSidebar extends LitElement {
this.hass.locale
);
// Add default hidden panels that are missing in hidden
const orderSet = new Set(this._order);
const hiddenSet = new Set(this._hidden);
for (const panel of panels) {
if (
panel.default_visible === false &&
!this._order.includes(panel.url_path) &&
!this._hidden.includes(panel.url_path)
!orderSet.has(panel.url_path) &&
!hiddenSet.has(panel.url_path)
) {
this._hidden.push(panel.url_path);
hiddenSet.add(panel.url_path);
}
}
if (hiddenSet.has(defaultPanel)) {
hiddenSet.delete(defaultPanel);
}
const hiddenPanels = Array.from(hiddenSet);
const items = [
...beforeSpacer,
...panels.filter((panel) => this._hidden!.includes(panel.url_path)),
...afterSpacer.filter((panel) => panel.url_path !== "config"),
].map((panel) => ({
...panels.filter((panel) => hiddenPanels.includes(panel.url_path)),
...afterSpacer,
].map<DisplayItem>((panel) => ({
value: panel.url_path,
label:
panel.url_path === defaultPanel
? panel.title || this.hass.localize("panel.states")
: this.hass.localize(`panel.${panel.title}`) || panel.title || "?",
icon: panel.icon || undefined,
iconPath:
panel.url_path === defaultPanel && !panel.icon
? PANEL_ICONS.lovelace
: panel.url_path in PANEL_ICONS
? PANEL_ICONS[panel.url_path]
: undefined,
disableSorting: panel.url_path === "developer-tools",
(getPanelTitle(this.hass, panel) || panel.url_path) +
`${defaultPanel === panel.url_path ? " (default)" : ""}`,
icon: getPanelIcon(panel),
iconPath: getPanelIconPath(panel),
disableSorting: SHOW_AFTER_SPACER_PANELS.includes(panel.url_path),
disableHiding: panel.url_path === defaultPanel,
}));
return html`<ha-items-display-editor
.hass=${this.hass}
.value=${{
order: this._order,
hidden: this._hidden,
}}
.items=${items}
@value-changed=${this._changed}
dont-sort-visible
>
</ha-items-display-editor>`;
return html`
<ha-items-display-editor
.hass=${this.hass}
.value=${{
order: this._order,
hidden: hiddenPanels,
}}
.items=${items}
@value-changed=${this._changed}
dont-sort-visible
>
</ha-items-display-editor>
`;
}
protected render() {
@@ -171,6 +188,22 @@ class DialogEditSidebar extends LitElement {
>${this.hass.localize("ui.sidebar.edit_subtitle")}</span
>`
: nothing}
<ha-md-button-menu
slot="actionItems"
positioning="popover"
anchor-corner="end-end"
menu-corner="start-end"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-md-menu-item .clickAction=${this._resetToDefaults}>
<ha-svg-icon slot="start" .path=${mdiRestart}></ha-svg-icon>
${this.hass.localize("ui.sidebar.reset_to_defaults")}
</ha-md-menu-item>
</ha-md-button-menu>
</ha-dialog-header>
<div slot="content" class="content">${this._renderContent()}</div>
<div slot="actions">
@@ -194,6 +227,26 @@ class DialogEditSidebar extends LitElement {
this._hidden = [...hidden];
}
private _resetToDefaults = async () => {
const confirmation = await showConfirmationDialog(this, {
text: this.hass.localize("ui.sidebar.reset_confirmation"),
confirmText: this.hass.localize("ui.common.reset"),
});
if (!confirmation) {
return;
}
this._order = [];
this._hidden = [];
try {
await saveFrontendUserData(this.hass.connection, "sidebar", {});
} catch (err: any) {
this._error = err.message || err;
}
this.closeDialog();
};
private async _save() {
if (this._migrateToUserData) {
const confirmation = await showConfirmationDialog(this, {

View File

@@ -295,7 +295,7 @@ export class ZHANetworkVisualizationPage extends LitElement {
color:
route.route_status === "Active"
? primaryColor
: style.getPropertyValue("--dark-primary-color"),
: style.getPropertyValue("--disabled-color"),
type: ["Child", "Parent"].includes(neighbor.relationship)
? "solid"
: "dotted",
@@ -335,7 +335,7 @@ export class ZHANetworkVisualizationPage extends LitElement {
symbolSize: 5,
lineStyle: {
width: 1,
color: style.getPropertyValue("--dark-primary-color"),
color: style.getPropertyValue("--disabled-color"),
type: "dotted",
},
ignoreForceLayout: true,

View File

@@ -8,16 +8,13 @@ import "../../../../components/ha-button";
import { createCloseHeading } from "../../../../components/ha-dialog";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import { saveFrontendSystemData } from "../../../../data/frontend";
import type {
LovelaceDashboard,
LovelaceDashboardCreateParams,
LovelaceDashboardMutableParams,
} from "../../../../data/lovelace/dashboard";
import { DEFAULT_PANEL } from "../../../../data/panel";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { showConfirmationDialog } from "../../../lovelace/custom-card-helpers";
import type { LovelaceDashboardDetailsDialogParams } from "./show-dialog-lovelace-dashboard-detail";
@customElement("dialog-lovelace-dashboard-detail")
@@ -61,9 +58,8 @@ export class DialogLovelaceDashboardDetail extends LitElement {
if (!this._params || !this._data) {
return nothing;
}
const defaultPanelUrlPath =
this.hass.systemData?.defaultPanel || DEFAULT_PANEL;
const titleInvalid = !this._data.title || !this._data.title.trim();
const isLovelaceDashboard = this._params.urlPath === "lovelace";
return html`
<ha-dialog
@@ -88,9 +84,9 @@ export class DialogLovelaceDashboardDetail extends LitElement {
? this.hass.localize(
"ui.panel.config.lovelace.dashboards.cant_edit_yaml"
)
: this._params.urlPath === "lovelace"
: isLovelaceDashboard
? this.hass.localize(
"ui.panel.config.lovelace.dashboards.cant_edit_default"
"ui.panel.config.lovelace.dashboards.cant_edit_lovelace"
)
: html`
<ha-form
@@ -119,24 +115,9 @@ export class DialogLovelaceDashboardDetail extends LitElement {
)}
</ha-button>
`
: ""}
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this._toggleDefault}
.disabled=${this._params.urlPath === "lovelace" &&
defaultPanelUrlPath === "lovelace"}
>
${this._params.urlPath === defaultPanelUrlPath
? this.hass.localize(
"ui.panel.config.lovelace.dashboards.detail.remove_default"
)
: this.hass.localize(
"ui.panel.config.lovelace.dashboards.detail.set_default"
)}
</ha-button>
: nothing}
`
: ""}
: nothing}
<ha-button
slot="primaryAction"
@click=${this._updateDashboard}
@@ -254,40 +235,6 @@ export class DialogLovelaceDashboardDetail extends LitElement {
};
}
private async _toggleDefault() {
const urlPath = this._params?.urlPath;
if (!urlPath) {
return;
}
const defaultPanel = this.hass.systemData?.defaultPanel || DEFAULT_PANEL;
// Add warning dialog to saying that this will change the default dashboard for all users
const confirm = await showConfirmationDialog(this, {
title: this.hass.localize(
urlPath === defaultPanel
? "ui.panel.config.lovelace.dashboards.detail.remove_default_confirm_title"
: "ui.panel.config.lovelace.dashboards.detail.set_default_confirm_title"
),
text: this.hass.localize(
urlPath === defaultPanel
? "ui.panel.config.lovelace.dashboards.detail.remove_default_confirm_text"
: "ui.panel.config.lovelace.dashboards.detail.set_default_confirm_text"
),
confirmText: this.hass.localize("ui.common.ok"),
dismissText: this.hass.localize("ui.common.cancel"),
destructive: false,
});
if (!confirm) {
return;
}
saveFrontendSystemData(this.hass.connection, "core", {
...this.hass.systemData,
defaultPanel: urlPath === defaultPanel ? undefined : urlPath,
});
}
private async _updateDashboard() {
if (this._params?.urlPath && !this._params.dashboard?.id) {
this.closeDialog();

View File

@@ -1,8 +1,9 @@
import {
mdiCheck,
mdiCheckCircleOutline,
mdiDelete,
mdiDotsVertical,
mdiHomeCircleOutline,
mdiHomeEdit,
mdiPencil,
mdiPlus,
} from "@mdi/js";
@@ -10,7 +11,6 @@ import type { PropertyValues } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoize from "memoize-one";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import { storage } from "../../../../common/decorators/storage";
import { navigate } from "../../../../common/navigate";
import { stringCompare } from "../../../../common/string/compare";
@@ -29,6 +29,7 @@ import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-tooltip";
import { saveFrontendSystemData } from "../../../../data/frontend";
import type { LovelacePanelConfig } from "../../../../data/lovelace";
import type { LovelaceRawConfig } from "../../../../data/lovelace/config/types";
import {
@@ -45,7 +46,11 @@ import {
fetchDashboards,
updateDashboard,
} from "../../../../data/lovelace/dashboard";
import { DEFAULT_PANEL } from "../../../../data/panel";
import {
DEFAULT_PANEL,
getPanelIcon,
getPanelTitle,
} from "../../../../data/panel";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import "../../../../layouts/hass-loading-screen";
import "../../../../layouts/hass-tabs-subpage-data-table";
@@ -56,12 +61,21 @@ import { lovelaceTabs } from "../ha-config-lovelace";
import { showDashboardConfigureStrategyDialog } from "./show-dialog-lovelace-dashboard-configure-strategy";
import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail";
export const PANEL_DASHBOARDS = [
"home",
"light",
"security",
"climate",
"energy",
] as string[];
type DataTableItem = Pick<
LovelaceDashboard,
"icon" | "title" | "show_in_sidebar" | "require_admin" | "mode" | "url_path"
> & {
default: boolean;
filename: string;
localized_type: string;
type: string;
};
@@ -112,7 +126,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
state: false,
subscribe: false,
})
private _activeGrouping?: string = "type";
private _activeGrouping?: string = "localized_type";
@storage({
key: "lovelace-dashboards-table-collapsed",
@@ -167,7 +181,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
<ha-svg-icon
.id="default-icon-${dashboard.title}"
style="padding-left: 10px; padding-inline-start: 10px; padding-inline-end: initial; direction: var(--direction);"
.path=${mdiCheckCircleOutline}
.path=${mdiHomeCircleOutline}
></ha-svg-icon>
<ha-tooltip
.for="default-icon-${dashboard.title}"
@@ -183,7 +197,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
},
};
columns.type = {
columns.localized_type = {
title: localize(
"ui.panel.config.lovelace.dashboards.picker.headers.type"
),
@@ -253,7 +267,15 @@ export class HaConfigLovelaceDashboards extends LitElement {
.hass=${this.hass}
narrow
.items=${[
...(this._canEdit(dashboard.url_path)
{
path: mdiHomeEdit,
label: localize(
"ui.panel.config.lovelace.dashboards.picker.set_as_default"
),
action: () => this._handleSetAsDefault(dashboard),
disabled: dashboard.default,
},
...(dashboard.type === "user_created"
? [
{
path: mdiPencil,
@@ -262,10 +284,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
),
action: () => this._handleEdit(dashboard),
},
]
: []),
...(this._canDelete(dashboard.url_path)
? [
{
label: this.hass.localize(
"ui.panel.config.lovelace.dashboards.picker.delete"
@@ -288,92 +306,43 @@ export class HaConfigLovelaceDashboards extends LitElement {
private _getItems = memoize(
(dashboards: LovelaceDashboard[], defaultUrlPath: string | null) => {
const defaultMode = (
this.hass.panels?.lovelace?.config as LovelacePanelConfig
).mode;
const mode = (this.hass.panels?.lovelace?.config as LovelacePanelConfig)
.mode;
const isDefault = defaultUrlPath === "lovelace";
const result: DataTableItem[] = [
{
icon: "mdi:view-dashboard",
title: this.hass.localize("panel.states"),
default: isDefault,
show_in_sidebar: isDefault,
show_in_sidebar: true,
require_admin: false,
url_path: "lovelace",
mode: defaultMode,
filename: defaultMode === "yaml" ? "ui-lovelace.yaml" : "",
type: this._localizeType("built_in"),
mode: mode,
filename: mode === "yaml" ? "ui-lovelace.yaml" : "",
type: "built_in",
localized_type: this._localizeType("built_in"),
},
];
if (isComponentLoaded(this.hass, "energy")) {
result.push({
icon: "mdi:lightning-bolt",
title: this.hass.localize(`ui.panel.config.dashboard.energy.main`),
show_in_sidebar: true,
mode: "storage",
url_path: "energy",
filename: "",
default: false,
require_admin: false,
type: this._localizeType("built_in"),
});
}
if (this.hass.panels.light) {
result.push({
icon: this.hass.panels.light.icon || "mdi:lamps",
title: this.hass.localize("panel.light"),
PANEL_DASHBOARDS.forEach((panel) => {
const panelInfo = this.hass.panels[panel];
if (!panel) {
return;
}
const item: DataTableItem = {
icon: getPanelIcon(panelInfo),
title: getPanelTitle(this.hass, panelInfo) || panelInfo.url_path,
show_in_sidebar: true,
mode: "storage",
url_path: "light",
url_path: panelInfo.url_path,
filename: "",
default: false,
default: defaultUrlPath === panelInfo.url_path,
require_admin: false,
type: this._localizeType("built_in"),
});
}
if (this.hass.panels.security) {
result.push({
icon: this.hass.panels.security.icon || "mdi:security",
title: this.hass.localize("panel.security"),
show_in_sidebar: true,
mode: "storage",
url_path: "security",
filename: "",
default: false,
require_admin: false,
type: this._localizeType("built_in"),
});
}
if (this.hass.panels.climate) {
result.push({
icon: this.hass.panels.climate.icon || "mdi:home-thermometer",
title: this.hass.localize("panel.climate"),
show_in_sidebar: true,
mode: "storage",
url_path: "climate",
filename: "",
default: false,
require_admin: false,
type: this._localizeType("built_in"),
});
}
if (this.hass.panels.home) {
result.push({
icon: this.hass.panels.home.icon || "mdi:home",
title: this.hass.localize("panel.home"),
show_in_sidebar: true,
mode: "storage",
url_path: "home",
filename: "",
default: false,
require_admin: false,
type: this._localizeType("built_in"),
});
}
type: "built_in",
localized_type: this._localizeType("built_in"),
};
result.push(item);
});
result.push(
...dashboards
@@ -386,7 +355,8 @@ export class HaConfigLovelaceDashboards extends LitElement {
filename: "",
...dashboard,
default: defaultUrlPath === dashboard.url_path,
type: this._localizeType("user_created"),
type: "user_created",
localized_type: this._localizeType("user_created"),
}) satisfies DataTableItem
)
);
@@ -486,20 +456,15 @@ export class HaConfigLovelaceDashboards extends LitElement {
this._openDetailDialog(dashboard, urlPath);
}
private _canDelete(urlPath: string) {
return ![
"lovelace",
"energy",
"light",
"security",
"climate",
"home",
].includes(urlPath);
}
private _canEdit(urlPath: string) {
return !["light", "security", "climate", "home"].includes(urlPath);
}
private _handleSetAsDefault = async (item: DataTableItem) => {
if (item.default) {
return;
}
await saveFrontendSystemData(this.hass.connection, "core", {
...this.hass.systemData,
defaultPanel: item.url_path === DEFAULT_PANEL ? undefined : item.url_path,
});
};
private _handleDelete = async (item: DataTableItem) => {
const dashboard = this._dashboards.find(
@@ -581,10 +546,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
private async _deleteDashboard(
dashboard: LovelaceDashboard
): Promise<boolean> {
if (!this._canDelete(dashboard.url_path)) {
return false;
}
const confirm = await showConfirmationDialog(this, {
title: this.hass!.localize(
"ui.panel.config.lovelace.dashboards.confirm_delete_title",

View File

@@ -0,0 +1,151 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/entity/ha-entities-picker";
import "../../../components/ha-button";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-wa-dialog";
import type { HomeFrontendSystemData } from "../../../data/frontend";
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import type { EditHomeDialogParams } from "./show-dialog-edit-home";
@customElement("dialog-edit-home")
export class DialogEditHome
extends LitElement
implements HassDialog<EditHomeDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: EditHomeDialogParams;
@state() private _config?: HomeFrontendSystemData;
@state() private _open = false;
@state() private _submitting = false;
public showDialog(params: EditHomeDialogParams): void {
this._params = params;
this._config = { ...params.config };
this._open = true;
}
public closeDialog(): boolean {
this._open = false;
return true;
}
private _dialogClosed(): void {
this._params = undefined;
this._config = undefined;
this._submitting = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._params) {
return nothing;
}
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
.headerTitle=${this.hass.localize("ui.panel.home.editor.title")}
@closed=${this._dialogClosed}
>
<p class="description">
${this.hass.localize("ui.panel.home.editor.description")}
</p>
<ha-entities-picker
autofocus
.hass=${this.hass}
.value=${this._config?.favorite_entities || []}
.label=${this.hass.localize(
"ui.panel.lovelace.editor.strategy.home.favorite_entities"
)}
.placeholder=${this.hass.localize(
"ui.panel.lovelace.editor.strategy.home.add_favorite_entity"
)}
.helper=${this.hass.localize(
"ui.panel.home.editor.favorite_entities_helper"
)}
reorder
allow-custom-entity
@value-changed=${this._favoriteEntitiesChanged}
></ha-entities-picker>
<ha-dialog-footer slot="footer">
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
.disabled=${this._submitting}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._save}
.disabled=${this._submitting}
>
${this.hass.localize("ui.common.save")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}
private _favoriteEntitiesChanged(ev: CustomEvent): void {
const entities = ev.detail.value as string[];
this._config = {
...this._config,
favorite_entities: entities.length > 0 ? entities : undefined,
};
}
private async _save(): Promise<void> {
if (!this._params || !this._config) {
return;
}
this._submitting = true;
try {
await this._params.saveConfig(this._config);
this.closeDialog();
} catch (err: any) {
// eslint-disable-next-line no-console
console.error("Failed to save home configuration:", err);
} finally {
this._submitting = false;
}
}
static styles = [
haStyleDialog,
css`
ha-wa-dialog {
--dialog-content-padding: var(--ha-space-6);
}
.description {
margin: 0 0 var(--ha-space-4) 0;
color: var(--secondary-text-color);
}
ha-entities-picker {
display: block;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"dialog-edit-home": DialogEditHome;
}
}

View File

@@ -0,0 +1,20 @@
import { fireEvent } from "../../../common/dom/fire_event";
import type { HomeFrontendSystemData } from "../../../data/frontend";
export interface EditHomeDialogParams {
config: HomeFrontendSystemData;
saveConfig: (config: HomeFrontendSystemData) => Promise<void>;
}
export const loadEditHomeDialog = () => import("./dialog-edit-home");
export const showEditHomeDialog = (
element: HTMLElement,
params: EditHomeDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-edit-home",
dialogImport: loadEditHomeDialog,
dialogParams: params,
});
};

View File

@@ -3,18 +3,18 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { debounce } from "../../common/util/debounce";
import { deepEqual } from "../../common/util/deep-equal";
import {
fetchFrontendSystemData,
saveFrontendSystemData,
type HomeFrontendSystemData,
} from "../../data/frontend";
import type { LovelaceDashboardStrategyConfig } from "../../data/lovelace/config/types";
import type { HomeAssistant, PanelInfo, Route } from "../../types";
import { showToast } from "../../util/toast";
import "../lovelace/hui-root";
import { generateLovelaceDashboardStrategy } from "../lovelace/strategies/get-strategy";
import type { Lovelace } from "../lovelace/types";
import { showAlertDialog } from "../lovelace/custom-card-helpers";
const HOME_LOVELACE_CONFIG: LovelaceDashboardStrategyConfig = {
strategy: {
type: "home",
},
};
import { showEditHomeDialog } from "./dialogs/show-dialog-edit-home";
@customElement("ha-panel-home")
class PanelHome extends LitElement {
@@ -28,12 +28,14 @@ class PanelHome extends LitElement {
@state() private _lovelace?: Lovelace;
@state() private _config: FrontendSystemData["home"] = {};
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
// Initial setup
if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace");
this._setLovelace();
this._loadConfig();
return;
}
@@ -95,9 +97,28 @@ class PanelHome extends LitElement {
`;
}
private async _loadConfig() {
try {
const data = await fetchFrontendSystemData(this.hass.connection, "home");
this._config = data || {};
} catch (err) {
// eslint-disable-next-line no-console
console.error("Failed to load favorites:", err);
this._config = {};
}
this._setLovelace();
}
private async _setLovelace() {
const strategyConfig: LovelaceDashboardStrategyConfig = {
strategy: {
type: "home",
favorite_entities: this._config.favorite_entities,
},
};
const config = await generateLovelaceDashboardStrategy(
HOME_LOVELACE_CONFIG,
strategyConfig,
this.hass
);
@@ -121,15 +142,34 @@ class PanelHome extends LitElement {
}
private _setEditMode = () => {
// For now, we just show an alert that edit mode is not supported.
// This will be expanded in the future.
showAlertDialog(this, {
title: "Edit mode not available",
text: "The Home panel does not support edit mode.",
confirmText: this.hass.localize("ui.common.ok"),
showEditHomeDialog(this, {
config: this._config,
saveConfig: async (config) => {
await this._saveConfig(config);
},
});
};
private async _saveConfig(config: HomeFrontendSystemData): Promise<void> {
try {
await saveFrontendSystemData(this.hass.connection, "home", config);
this._config = config || {};
} catch (err: any) {
// eslint-disable-next-line no-console
console.error("Failed to save home configuration:", err);
showToast(this, {
message: this.hass.localize("ui.panel.home.editor.save_failed"),
duration: 0,
dismissable: true,
});
return;
}
showToast(this, {
message: this.hass.localize("ui.common.successfully_saved"),
});
this._setLovelace();
}
static readonly styles: CSSResultGroup = css`
:host {
display: block;

View File

@@ -8,6 +8,9 @@ import type { LovelaceDashboard } from "../../data/lovelace/dashboard";
import { fetchDashboards } from "../../data/lovelace/dashboard";
import type { HomeAssistant } from "../../types";
import { saveFrontendUserData } from "../../data/frontend";
import { PANEL_DASHBOARDS } from "../config/lovelace/dashboards/ha-config-lovelace-dashboards";
import { getPanelTitle } from "../../data/panel";
import "../../components/ha-divider";
const USE_SYSTEM_VALUE = "___use_system___";
@@ -47,12 +50,19 @@ class HaPickDashboardRow extends LitElement {
<ha-list-item .value=${USE_SYSTEM_VALUE}>
${this.hass.localize("ui.panel.profile.dashboard.system")}
</ha-list-item>
<ha-divider></ha-divider>
<ha-list-item value="lovelace">
${this.hass.localize("ui.panel.profile.dashboard.lovelace")}
</ha-list-item>
<ha-list-item value="home">
${this.hass.localize("ui.panel.profile.dashboard.home")}
</ha-list-item>
${PANEL_DASHBOARDS.map((panel) => {
const panelInfo = this.hass.panels[panel];
return html`
<ha-list-item value="lovelace">
${panelInfo ? getPanelTitle(this.hass, panelInfo) : panel}
</ha-list-item>
`;
})}
<ha-divider></ha-divider>
${this._dashboards.map((dashboard) => {
if (!this.hass.user!.is_admin && dashboard.require_admin) {
return "";

View File

@@ -2217,9 +2217,19 @@
"sidebar_toggle": "Sidebar toggle",
"edit_sidebar": "Edit sidebar",
"edit_subtitle": "Synced on all devices",
"migrate_to_user_data": "This will change the sidebar on all the devices you are logged in to. To create a sidebar per device, you should use a different user for that device."
"migrate_to_user_data": "This will change the sidebar on all the devices you are logged in to. To create a sidebar per device, you should use a different user for that device.",
"reset_to_defaults": "Reset to defaults",
"reset_confirmation": "Are you sure you want to reset the sidebar to its default configuration? This will restore the original order and visibility of all panels."
},
"panel": {
"home": {
"editor": {
"title": "Edit home page",
"description": "Configure your home page display preferences.",
"favorite_entities_helper": "Display your favorite entities. Home Assistant will still suggest based on commonly used up to 8 slots.",
"save_failed": "Failed to save home page configuration"
}
},
"my": {
"not_supported": "This redirect is not supported by your Home Assistant instance. Check the {link} for the supported redirects and the version they where introduced.",
"component_not_loaded": "This redirect is not supported by your Home Assistant instance. You need the integration {integration} to use this redirect.",
@@ -3497,6 +3507,7 @@
"edit": "Edit",
"delete": "Delete",
"add_dashboard": "Add dashboard",
"set_as_default": "Set as default",
"type": {
"user_created": "User created",
"built_in": "Built-in"
@@ -3505,7 +3516,7 @@
"confirm_delete_title": "Delete {dashboard_title}?",
"confirm_delete_text": "This dashboard will be permanently deleted.",
"cant_edit_yaml": "Dashboards created in YAML cannot be edited from the UI. Change them in configuration.yaml.",
"cant_edit_default": "The default dashboard, Overview, cannot be edited from the UI. You can hide it by setting another dashboard as default.",
"cant_edit_lovelace": "The Overview dashboard title and icon cannot be changed. You can create a new dashboard to get more customization options.",
"detail": {
"edit_dashboard": "Edit dashboard",
"new_dashboard": "Add new dashboard",