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:
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
132
src/panels/home/dialogs/dialog-new-overview.ts
Normal file
132
src/panels/home/dialogs/dialog-new-overview.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
18
src/panels/home/dialogs/show-dialog-new-overview.ts
Normal file
18
src/panels/home/dialogs/show-dialog-new-overview.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user