mirror of
https://github.com/home-assistant/frontend.git
synced 2026-02-15 07:25:54 +00:00
Add repairs and updates cards to home dashboard overview (#29552)
* Add repairs and updates cards to home dashboard overview Add two new cards to the "For You" section of the home dashboard that display links to repairs and updates when there are active issues or available updates. Both cards are only visible to admin users and hide when empty. https://claude.ai/code/session_013NTgs1U9x59uaEJs1smy8i * Fix navigation and visibility * Reorder --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,8 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
@state() private _showSkipped = false;
|
||||
|
||||
@state() private _supervisorInfo?: HassioSupervisorInfo;
|
||||
@@ -65,7 +67,9 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/system"
|
||||
.backPath=${this._searchParms.has("historyBack")
|
||||
? undefined
|
||||
: "/config/system"}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.updates.caption")}
|
||||
|
||||
@@ -32,6 +32,8 @@ class HaConfigRepairsDashboard extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _repairsIssues: RepairsIssue[] = [];
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
@state() private _showIgnored = false;
|
||||
|
||||
private _getFilteredIssues = memoizeOne(
|
||||
@@ -75,7 +77,9 @@ class HaConfigRepairsDashboard extends SubscribeMixin(LitElement) {
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/system"
|
||||
.backPath=${this._searchParms.has("historyBack")
|
||||
? undefined
|
||||
: "/config/system"}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.repairs.caption")}
|
||||
|
||||
@@ -29,6 +29,8 @@ export class HuiDiscoveredDevicesCard
|
||||
extends SubscribeMixin(LitElement)
|
||||
implements LovelaceCard
|
||||
{
|
||||
public connectedWhileHidden = true;
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _config?: DiscoveredDevicesCardConfig;
|
||||
@@ -181,7 +183,7 @@ export class HuiDiscoveredDevicesCard
|
||||
tileCardStyle,
|
||||
css`
|
||||
:host {
|
||||
--tile-color: var(--primary-color);
|
||||
--tile-color: var(--info-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
162
src/panels/lovelace/cards/hui-repairs-card.ts
Normal file
162
src/panels/lovelace/cards/hui-repairs-card.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { mdiWrench } from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/tile/ha-tile-container";
|
||||
import "../../../components/tile/ha-tile-icon";
|
||||
import "../../../components/tile/ha-tile-info";
|
||||
import type { RepairsIssue } from "../../../data/repairs";
|
||||
import { subscribeRepairsIssueRegistry } from "../../../data/repairs";
|
||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import type { LovelaceCard, LovelaceGridOptions } from "../types";
|
||||
import { tileCardStyle } from "./tile/tile-card-style";
|
||||
import type { RepairsCardConfig } from "./types";
|
||||
|
||||
@customElement("hui-repairs-card")
|
||||
export class HuiRepairsCard
|
||||
extends SubscribeMixin(LitElement)
|
||||
implements LovelaceCard
|
||||
{
|
||||
public connectedWhileHidden = true;
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _config?: RepairsCardConfig;
|
||||
|
||||
@state() private _repairsIssues: RepairsIssue[] = [];
|
||||
|
||||
public hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
return [
|
||||
subscribeRepairsIssueRegistry(
|
||||
this.hass!.connection,
|
||||
(repairs: { issues: RepairsIssue[] }) => {
|
||||
// Filter to only active and non-ignored issues
|
||||
this._repairsIssues = repairs.issues.filter(
|
||||
(issue) => issue.active !== false && !issue.ignored
|
||||
);
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
public setConfig(config: RepairsCardConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return this._config?.vertical ? 2 : 1;
|
||||
}
|
||||
|
||||
public getGridOptions(): LovelaceGridOptions {
|
||||
const columns = 6;
|
||||
let min_columns = 6;
|
||||
let rows = 1;
|
||||
|
||||
if (this._config?.vertical) {
|
||||
rows++;
|
||||
min_columns = 3;
|
||||
}
|
||||
return {
|
||||
columns,
|
||||
rows,
|
||||
min_columns,
|
||||
min_rows: rows,
|
||||
};
|
||||
}
|
||||
|
||||
private async _handleAction(ev: ActionHandlerEvent) {
|
||||
if (ev.detail.action === "tap" && !hasAction(this._config?.tap_action)) {
|
||||
navigate("/config/repairs");
|
||||
return;
|
||||
}
|
||||
handleAction(this, this.hass!, this._config!, ev.detail.action!);
|
||||
}
|
||||
|
||||
private get _hasCardAction() {
|
||||
return (
|
||||
!this._config?.tap_action ||
|
||||
hasAction(this._config?.tap_action) ||
|
||||
hasAction(this._config?.hold_action) ||
|
||||
hasAction(this._config?.double_tap_action)
|
||||
);
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update visibility based on admin status and repairs count
|
||||
const shouldBeHidden =
|
||||
!this.hass.user?.is_admin ||
|
||||
(this._config.hide_empty && this._repairsIssues.length === 0);
|
||||
|
||||
if (shouldBeHidden !== this.hidden) {
|
||||
this.style.display = shouldBeHidden ? "none" : "";
|
||||
this.toggleAttribute("hidden", shouldBeHidden);
|
||||
fireEvent(this, "card-visibility-changed", { value: !shouldBeHidden });
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (!this._config || !this.hass || this.hidden) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const count = this._repairsIssues.length;
|
||||
|
||||
const label = this.hass.localize("ui.card.repairs.title");
|
||||
const secondary =
|
||||
count > 0
|
||||
? this.hass.localize("ui.card.repairs.count_issues", {
|
||||
count,
|
||||
})
|
||||
: this.hass.localize("ui.card.repairs.no_issues");
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<ha-tile-container
|
||||
.vertical=${Boolean(this._config.vertical)}
|
||||
.interactive=${this._hasCardAction}
|
||||
.actionHandlerOptions=${{
|
||||
hasHold: hasAction(this._config!.hold_action),
|
||||
hasDoubleClick: hasAction(this._config!.double_tap_action),
|
||||
}}
|
||||
@action=${this._handleAction}
|
||||
>
|
||||
<ha-tile-icon slot="icon" .iconPath=${mdiWrench}></ha-tile-icon>
|
||||
<ha-tile-info
|
||||
slot="info"
|
||||
.primary=${label}
|
||||
.secondary=${secondary}
|
||||
></ha-tile-info>
|
||||
</ha-tile-container>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
tileCardStyle,
|
||||
css`
|
||||
:host {
|
||||
--tile-color: var(--warning-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-repairs-card": HuiRepairsCard;
|
||||
}
|
||||
}
|
||||
157
src/panels/lovelace/cards/hui-updates-card.ts
Normal file
157
src/panels/lovelace/cards/hui-updates-card.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { mdiPackageUp } from "@mdi/js";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/tile/ha-tile-container";
|
||||
import "../../../components/tile/ha-tile-icon";
|
||||
import "../../../components/tile/ha-tile-info";
|
||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import {
|
||||
filterUpdateEntities,
|
||||
updateCanInstall,
|
||||
type UpdateEntity,
|
||||
} from "../../../data/update";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import type { LovelaceCard, LovelaceGridOptions } from "../types";
|
||||
import { tileCardStyle } from "./tile/tile-card-style";
|
||||
import type { UpdatesCardConfig } from "./types";
|
||||
|
||||
@customElement("hui-updates-card")
|
||||
export class HuiUpdatesCard extends LitElement implements LovelaceCard {
|
||||
public connectedWhileHidden = true;
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _config?: UpdatesCardConfig;
|
||||
|
||||
public setConfig(config: UpdatesCardConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return this._config?.vertical ? 2 : 1;
|
||||
}
|
||||
|
||||
public getGridOptions(): LovelaceGridOptions {
|
||||
const columns = 6;
|
||||
let min_columns = 6;
|
||||
let rows = 1;
|
||||
|
||||
if (this._config?.vertical) {
|
||||
rows++;
|
||||
min_columns = 3;
|
||||
}
|
||||
return {
|
||||
columns,
|
||||
rows,
|
||||
min_columns,
|
||||
min_rows: rows,
|
||||
};
|
||||
}
|
||||
|
||||
private _getUpdateEntities(): UpdateEntity[] {
|
||||
if (!this.hass) {
|
||||
return [];
|
||||
}
|
||||
return filterUpdateEntities(
|
||||
this.hass.states,
|
||||
this.hass.locale.language
|
||||
).filter((entity) => updateCanInstall(entity, false));
|
||||
}
|
||||
|
||||
private async _handleAction(ev: ActionHandlerEvent) {
|
||||
if (ev.detail.action === "tap" && !hasAction(this._config?.tap_action)) {
|
||||
navigate("/config/updates");
|
||||
return;
|
||||
}
|
||||
handleAction(this, this.hass!, this._config!, ev.detail.action!);
|
||||
}
|
||||
|
||||
private get _hasCardAction() {
|
||||
return (
|
||||
!this._config?.tap_action ||
|
||||
hasAction(this._config?.tap_action) ||
|
||||
hasAction(this._config?.hold_action) ||
|
||||
hasAction(this._config?.double_tap_action)
|
||||
);
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateEntities = this._getUpdateEntities();
|
||||
|
||||
// Update visibility based on admin status and updates count
|
||||
const shouldBeHidden =
|
||||
!this.hass.user?.is_admin ||
|
||||
(this._config.hide_empty && updateEntities.length === 0);
|
||||
|
||||
if (shouldBeHidden !== this.hidden) {
|
||||
this.style.display = shouldBeHidden ? "none" : "";
|
||||
this.toggleAttribute("hidden", shouldBeHidden);
|
||||
fireEvent(this, "card-visibility-changed", { value: !shouldBeHidden });
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (!this._config || !this.hass || this.hidden) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const updateEntities = this._getUpdateEntities();
|
||||
const count = updateEntities.length;
|
||||
|
||||
const label = this.hass.localize("ui.card.updates.title");
|
||||
const secondary =
|
||||
count > 0
|
||||
? this.hass.localize("ui.card.updates.count_updates", {
|
||||
count,
|
||||
})
|
||||
: this.hass.localize("ui.card.updates.no_updates");
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<ha-tile-container
|
||||
.vertical=${Boolean(this._config.vertical)}
|
||||
.interactive=${this._hasCardAction}
|
||||
.actionHandlerOptions=${{
|
||||
hasHold: hasAction(this._config!.hold_action),
|
||||
hasDoubleClick: hasAction(this._config!.double_tap_action),
|
||||
}}
|
||||
@action=${this._handleAction}
|
||||
>
|
||||
<ha-tile-icon slot="icon" .iconPath=${mdiPackageUp}></ha-tile-icon>
|
||||
<ha-tile-info
|
||||
slot="info"
|
||||
.primary=${label}
|
||||
.secondary=${secondary}
|
||||
></ha-tile-info>
|
||||
</ha-tile-container>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
tileCardStyle,
|
||||
css`
|
||||
:host {
|
||||
--tile-color: var(--info-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-updates-card": HuiUpdatesCard;
|
||||
}
|
||||
}
|
||||
@@ -682,3 +682,19 @@ export interface DiscoveredDevicesCardConfig extends LovelaceCardConfig {
|
||||
hold_action?: ActionConfig;
|
||||
double_tap_action?: ActionConfig;
|
||||
}
|
||||
|
||||
export interface RepairsCardConfig extends LovelaceCardConfig {
|
||||
hide_empty?: boolean;
|
||||
vertical?: boolean;
|
||||
tap_action?: ActionConfig;
|
||||
hold_action?: ActionConfig;
|
||||
double_tap_action?: ActionConfig;
|
||||
}
|
||||
|
||||
export interface UpdatesCardConfig extends LovelaceCardConfig {
|
||||
hide_empty?: boolean;
|
||||
vertical?: boolean;
|
||||
tap_action?: ActionConfig;
|
||||
hold_action?: ActionConfig;
|
||||
double_tap_action?: ActionConfig;
|
||||
}
|
||||
|
||||
@@ -74,6 +74,8 @@ const LAZY_LOAD_TYPES = {
|
||||
error: () => import("../cards/hui-error-card"),
|
||||
"home-summary": () => import("../cards/hui-home-summary-card"),
|
||||
"discovered-devices": () => import("../cards/hui-discovered-devices-card"),
|
||||
repairs: () => import("../cards/hui-repairs-card"),
|
||||
updates: () => import("../cards/hui-updates-card"),
|
||||
gauge: () => import("../cards/hui-gauge-card"),
|
||||
"history-graph": () => import("../cards/hui-history-graph-card"),
|
||||
"horizontal-stack": () => import("../cards/hui-horizontal-stack-card"),
|
||||
|
||||
@@ -23,7 +23,9 @@ import type {
|
||||
EmptyStateCardConfig,
|
||||
HomeSummaryCard,
|
||||
MarkdownCardConfig,
|
||||
RepairsCardConfig,
|
||||
TileCardConfig,
|
||||
UpdatesCardConfig,
|
||||
} from "../../cards/types";
|
||||
import type { Condition } from "../../common/validate-condition";
|
||||
import type { CommonControlSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy";
|
||||
@@ -252,6 +254,24 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
|
||||
// Build summary cards (used in both mobile section and sidebar)
|
||||
const summaryCards: LovelaceCardConfig[] = [
|
||||
// Repairs card - only visible to admins, hides when empty
|
||||
{
|
||||
type: "repairs",
|
||||
hide_empty: true,
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/config/repairs?historyBack=1",
|
||||
},
|
||||
} satisfies RepairsCardConfig,
|
||||
// Updates card - only visible to admins, hides when empty
|
||||
{
|
||||
type: "updates",
|
||||
hide_empty: true,
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/config/updates?historyBack=1",
|
||||
},
|
||||
} satisfies UpdatesCardConfig,
|
||||
// Discovered devices card - only visible to admins, hides when empty
|
||||
{
|
||||
type: "discovered-devices",
|
||||
|
||||
@@ -220,6 +220,16 @@
|
||||
"count_devices": "{count} {count, plural,\n one {device to add}\n other {devices to add}\n}",
|
||||
"no_devices": "No devices to add"
|
||||
},
|
||||
"repairs": {
|
||||
"title": "Repairs",
|
||||
"count_issues": "{count} {count, plural,\n one {issue}\n other {issues}\n}",
|
||||
"no_issues": "No issues"
|
||||
},
|
||||
"updates": {
|
||||
"title": "Updates",
|
||||
"count_updates": "{count} {count, plural,\n one {update available}\n other {updates available}\n}",
|
||||
"no_updates": "Up to date"
|
||||
},
|
||||
"media_player": {
|
||||
"source": "Source",
|
||||
"sound_mode": "Sound mode",
|
||||
|
||||
Reference in New Issue
Block a user