1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-02-14 23:18:21 +00:00

Add welcome banner for new overview dashboard (#29223)

This commit is contained in:
Paul Bottein
2026-01-28 18:46:22 +01:00
committed by GitHub
parent 4f3196adb9
commit 60079ce999
11 changed files with 313 additions and 32 deletions

View File

@@ -13,10 +13,13 @@ export interface SidebarFrontendUserData {
export interface CoreFrontendSystemData {
default_panel?: string;
onboarded_version?: string;
onboarded_date?: string;
}
export interface HomeFrontendSystemData {
favorite_entities?: string[];
welcome_banner_dismissed?: boolean;
}
declare global {

View File

@@ -15,16 +15,26 @@ import type { LocalizeKeys } from "../common/translations/localize";
/** Panel to show when no panel is picked. */
export const DEFAULT_PANEL = "home";
export const hasLegacyOverviewPanel = (hass: HomeAssistant): boolean =>
Boolean(hass.panels.lovelace?.config);
export const getLegacyDefaultPanelUrlPath = (): string | null => {
const defaultPanel = window.localStorage.getItem("defaultPanel");
return defaultPanel ? JSON.parse(defaultPanel) : null;
};
export const getDefaultPanelUrlPath = (hass: HomeAssistant): string =>
hass.userData?.default_panel ||
hass.systemData?.default_panel ||
getLegacyDefaultPanelUrlPath() ||
DEFAULT_PANEL;
export const getDefaultPanelUrlPath = (hass: HomeAssistant): string => {
const defaultPanel =
hass.userData?.default_panel ||
hass.systemData?.default_panel ||
getLegacyDefaultPanelUrlPath() ||
DEFAULT_PANEL;
// If default panel is lovelace and no old overview exists, fall back to home
if (defaultPanel === "lovelace" && !hasLegacyOverviewPanel(hass)) {
return DEFAULT_PANEL;
}
return defaultPanel;
};
export const getDefaultPanel = (hass: HomeAssistant): PanelInfo => {
const panel = getDefaultPanelUrlPath(hass);

View File

@@ -25,6 +25,7 @@ import { subscribeOne } from "../common/util/subscribe-one";
import "../components/ha-card";
import type { AuthUrlSearchParams } from "../data/auth";
import { hassUrl } from "../data/auth";
import { saveFrontendSystemData } from "../data/frontend";
import type { OnboardingResponses, OnboardingStep } from "../data/onboarding";
import {
fetchInstallationType,
@@ -406,6 +407,11 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
),
};
await saveFrontendSystemData(this.hass!.connection, "core", {
onboarded_version: this.hass!.config.version,
onboarded_date: new Date().toISOString(),
});
let result: OnboardingResponses["integration"];
try {

View File

@@ -28,15 +28,15 @@ interface Strategy {
const STRATEGIES = [
{
type: "overview",
type: "original-states",
images: {
light:
"/static/images/dashboard-options/light/icon-dashboard-overview.svg",
dark: "/static/images/dashboard-options/dark/icon-dashboard-overview.svg",
"/static/images/dashboard-options/light/icon-dashboard-overview-legacy.svg",
dark: "/static/images/dashboard-options/dark/icon-dashboard-overview-legacy.svg",
},
name: "ui.panel.config.lovelace.dashboards.dialog_new.strategy.overview.title",
name: "ui.panel.config.lovelace.dashboards.dialog_new.strategy.overview-legacy.title",
description:
"ui.panel.config.lovelace.dashboards.dialog_new.strategy.overview.description",
"ui.panel.config.lovelace.dashboards.dialog_new.strategy.overview-legacy.description",
},
{
type: "map",
@@ -244,11 +244,7 @@ class DialogNewDashboard extends LitElement implements HassDialog {
if (target.config) {
config = target.config;
} else if (target.strategy) {
if (target.strategy === "overview") {
config = null;
} else {
config = this._generateStrategyConfig(target.strategy);
}
config = this._generateStrategyConfig(target.strategy);
}
this._params?.selectConfig(config);

View File

@@ -0,0 +1,132 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-button";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-wa-dialog";
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import type { NewOverviewDialogParams } from "./show-dialog-new-overview";
@customElement("dialog-new-overview")
export class DialogNewOverview
extends LitElement
implements HassDialog<NewOverviewDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: NewOverviewDialogParams;
@state() private _open = false;
public showDialog(params: NewOverviewDialogParams): void {
this._params = params;
this._open = true;
}
public closeDialog(): boolean {
this._open = false;
return true;
}
private _dialogClosed(): void {
if (this._params) {
this._params.dismiss();
}
this._params = undefined;
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}
header-title="Welcome to your new overview"
prevent-scrim-close
@closed=${this._dialogClosed}
>
<div class="content">
<p>
The overview dashboard has been redesigned to give you a better
experience managing your smart home.
</p>
<h3>What's new</h3>
<ul>
<li>
<strong>Automatic organization</strong> - Your devices are now
automatically organized by area and floor.
</li>
<li>
<strong>Favorites</strong> - Pin your most used entities to the
top for quick access.
</li>
</ul>
<h3>Your existing dashboards</h3>
<p>
Your manual dashboards are still available in the sidebar. This new
overview works alongside them. You can also create a new dashboard
using the "Overview (legacy)" template in
<a href="/config/lovelace/dashboards" @click=${this.closeDialog}
>dashboard settings</a
>.
</p>
</div>
<ha-dialog-footer slot="footer">
<ha-button slot="primaryAction" @click=${this.closeDialog}>
OK, understood
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}
static styles = [
haStyleDialog,
css`
ha-wa-dialog {
--dialog-content-padding: var(--ha-space-6);
}
.content {
line-height: var(--ha-line-height-normal);
}
p {
margin: 0 0 var(--ha-space-4) 0;
color: var(--secondary-text-color);
}
h3 {
margin: var(--ha-space-4) 0 var(--ha-space-2) 0;
font-size: var(--ha-font-size-l);
font-weight: var(--ha-font-weight-medium);
}
ul {
margin: 0 0 var(--ha-space-4) 0;
padding-left: var(--ha-space-6);
color: var(--secondary-text-color);
}
li {
margin-bottom: var(--ha-space-2);
}
li strong {
color: var(--primary-text-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"dialog-new-overview": DialogNewOverview;
}
}

View File

@@ -0,0 +1,18 @@
import { fireEvent } from "../../../common/dom/fire_event";
export interface NewOverviewDialogParams {
dismiss: () => void;
}
export const loadNewOverviewDialog = () => import("./dialog-new-overview");
export const showNewOverviewDialog = (
element: HTMLElement,
params: NewOverviewDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-new-overview",
dialogImport: loadNewOverviewDialog,
dialogParams: params,
});
};

View File

@@ -1,10 +1,15 @@
import { ResizeController } from "@lit-labs/observers/resize-controller";
import { mdiPencil } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { atLeastVersion } from "../../common/config/version";
import { navigate } from "../../common/navigate";
import { debounce } from "../../common/util/debounce";
import { deepEqual } from "../../common/util/deep-equal";
import "../../components/ha-button";
import "../../components/ha-svg-icon";
import { updateAreaRegistryEntry } from "../../data/area/area_registry";
import { updateDeviceRegistryEntry } from "../../data/device/device_registry";
import {
@@ -13,6 +18,7 @@ import {
type HomeFrontendSystemData,
} from "../../data/frontend";
import type { LovelaceDashboardStrategyConfig } from "../../data/lovelace/config/types";
import { mdiHomeAssistant } from "../../resources/home-assistant-logo-svg";
import type { HomeAssistant, PanelInfo, Route } from "../../types";
import { showToast } from "../../util/toast";
import { showAreaRegistryDetailDialog } from "../config/areas/show-dialog-area-registry-detail";
@@ -23,6 +29,8 @@ import type { ExtraActionItem } from "../lovelace/hui-root";
import { expandLovelaceConfigStrategies } from "../lovelace/strategies/get-strategy";
import type { Lovelace } from "../lovelace/types";
import { showEditHomeDialog } from "./dialogs/show-dialog-edit-home";
import { showNewOverviewDialog } from "./dialogs/show-dialog-new-overview";
import { hasLegacyOverviewPanel } from "../../data/panel";
@customElement("ha-panel-home")
class PanelHome extends LitElement {
@@ -40,6 +48,31 @@ class PanelHome extends LitElement {
@state() private _extraActionItems?: ExtraActionItem[];
private get _showBanner(): boolean {
// Don't show if already dismissed
if (this._config.welcome_banner_dismissed) {
return false;
}
// Don't show if HA is not running
if (this.hass.config.state !== "RUNNING") {
return false;
}
// Show banner only for users who:
// 1. Were onboarded before 2026.2 (or have no onboarded_version)
// 2. Don't have a custom "lovelace" dashboard (old overview)
const onboardedVersion = this.hass.systemData?.onboarded_version;
const isNewInstance =
onboardedVersion && atLeastVersion(onboardedVersion, 2026, 2);
const hasOldOverview = hasLegacyOverviewPanel(this.hass);
return !isNewInstance && !hasOldOverview;
}
private _bannerHeight = new ResizeController(this, {
target: null,
callback: (entries) =>
(entries[0]?.target as HTMLElement | undefined)?.offsetHeight ?? 0,
});
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
// Initial setup
@@ -80,7 +113,7 @@ class PanelHome extends LitElement {
this.hass.config.state === "RUNNING" &&
oldHass.config.state !== "RUNNING"
) {
this._setLovelace();
this._setup();
}
}
}
@@ -211,7 +244,14 @@ class PanelHome extends LitElement {
return nothing;
}
const huiRootStyle = styleMap({
"--view-container-padding-top": this._bannerHeight.value
? `${this._bannerHeight.value}px`
: undefined,
});
return html`
${this._renderBanner()}
<hui-root
.hass=${this.hass}
.narrow=${this.narrow}
@@ -221,10 +261,56 @@ class PanelHome extends LitElement {
no-edit
.extraActionItems=${this._extraActionItems}
@ll-custom=${this._handleLLCustomEvent}
style=${huiRootStyle}
></hui-root>
`;
}
private _renderBanner() {
if (!this._showBanner) {
return nothing;
}
return html`
<div class="banner">
<div class="banner-content">
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
<span class="banner-text">
Welcome to the new overview dashboard.
</span>
</div>
<div class="banner-actions">
<ha-button size="small" appearance="filled" @click=${this._learnMore}>
Learn more
</ha-button>
</div>
</div>
`;
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("_showBanner") || changedProps.has("_lovelace")) {
const banner = this.shadowRoot?.querySelector(".banner");
if (banner) {
this._bannerHeight.observe(banner);
}
}
}
private _learnMore() {
showNewOverviewDialog(this, {
dismiss: async () => {
const newConfig = {
...this._config,
welcome_banner_dismissed: true,
};
this._config = newConfig;
await saveFrontendSystemData(this.hass.connection, "home", newConfig);
},
});
}
private async _setLovelace() {
const strategyConfig: LovelaceDashboardStrategyConfig = {
strategy: {
@@ -282,6 +368,45 @@ class PanelHome extends LitElement {
:host {
display: block;
}
.banner {
display: flex;
align-items: center;
flex-wrap: wrap;
padding: var(--ha-space-2) var(--ha-space-4);
background-color: var(--primary-color);
color: var(--text-primary-color);
gap: var(--ha-space-2);
position: fixed;
top: var(--header-height, 56px);
left: var(--mdc-drawer-width, 0px);
right: 0;
z-index: 5;
}
.banner-content {
display: flex;
align-items: center;
gap: var(--ha-space-2);
flex: 1;
min-width: 200px;
}
.banner ha-svg-icon {
--mdc-icon-size: 24px;
flex-shrink: 0;
}
.banner-text {
font-size: 14px;
font-weight: 500;
}
.banner-actions {
display: flex;
flex: none;
gap: var(--ha-space-2);
align-items: center;
margin-inline-start: auto;
}
.banner-actions ha-button::part(base) {
text-wrap: nowrap;
}
`;
}

View File

@@ -1497,7 +1497,10 @@ class HUIRoot extends LitElement {
display: flex;
min-height: 100vh;
box-sizing: border-box;
padding-top: calc(var(--header-height) + var(--safe-area-inset-top));
padding-top: calc(
var(--header-height) + var(--safe-area-inset-top) +
var(--view-container-padding-top, 0px)
);
padding-right: var(--safe-area-inset-right);
padding-inline-end: var(--safe-area-inset-right);
padding-bottom: calc(

View File

@@ -14,7 +14,7 @@
"light": "Lights",
"security": "Security",
"climate": "Climate",
"home": "Home"
"home": "Overview"
},
"state": {
"default": {
@@ -4155,21 +4155,9 @@
"title": "Webpage",
"description": "Integrate a webpage as a dashboard"
},
"areas": {
"title": "Areas (experimental)",
"description": "Display your devices with a view for each area"
},
"default": {
"title": "Default dashboard",
"description": "Display your devices grouped by area"
},
"overview": {
"title": "Overview",
"overview-legacy": {
"title": "Overview (Legacy)",
"description": "Gives an overview of all your entities and areas they are in"
},
"home": {
"title": "Home (experimental)",
"description": "Global overview of your home"
}
},
"search_dashboards": "Search dashboards",