import { mdiArrowDown, mdiArrowUp, mdiClose, mdiFormatListChecks, mdiMenuDown, mdiSlopeUphill, mdiTableCog, mdiUnfoldLessHorizontal, mdiUnfoldMoreHorizontal, } from "@mdi/js"; import "@home-assistant/webawesome/dist/components/divider/divider"; import type { HassEntity } from "home-assistant-js-websocket"; import { css, type CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import type { HASSDomEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event"; import { computeStateName } from "../../../../common/entity/compute_state_name"; import type { LocalizeFunc } from "../../../../common/translations/localize"; import "../../../../components/chips/ha-assist-chip"; import "../../../../components/data-table/ha-data-table"; import type { DataTableColumnContainer, HaDataTable, SelectionChangedEvent, SortingDirection, } from "../../../../components/data-table/ha-data-table"; import { showDataTableSettingsDialog } from "../../../../components/data-table/show-dialog-data-table-settings"; import "../../../../components/ha-button"; import "../../../../components/ha-dropdown"; import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown"; import "../../../../components/ha-dropdown-item"; import "../../../../components/input/ha-input-search"; import type { HaInputSearch } from "../../../../components/input/ha-input-search"; import type { StatisticsMetaData, StatisticsValidationResult, } from "../../../../data/recorder"; import { clearStatistics, getStatisticIds, StatisticMeanType, updateStatisticsIssues, validateStatistics, } from "../../../../data/recorder"; import { KeyboardShortcutMixin } from "../../../../mixins/keyboard-shortcut-mixin"; import { haStyle } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; import { showConfirmationDialog } from "../../../lovelace/custom-card-helpers"; import { fixStatisticsIssue } from "./fix-statistics"; import { showStatisticsAdjustSumDialog } from "./show-dialog-statistics-adjust-sum"; const FIX_ISSUES_ORDER: Record = { no_state: 0, entity_no_longer_recorded: 1, entity_not_recorded: 1, state_class_removed: 2, units_changed: 3, mean_type_changed: 4, }; const FIXABLE_ISSUES: StatisticsValidationResult["type"][] = [ "no_state", "entity_no_longer_recorded", "state_class_removed", "units_changed", "mean_type_changed", ]; type StatisticData = StatisticsMetaData & { issues?: StatisticsValidationResult[]; state?: HassEntity; selectable?: boolean; }; type DisplayedStatisticData = StatisticData & { displayName: string; issues_string?: string; }; @customElement("developer-tools-statistics") class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @property({ type: Boolean, reflect: true }) public narrow = false; @state() private _data: StatisticData[] = [] as StatisticsMetaData[]; @state() private filter = ""; @state() private _selected: string[] = []; @state() private groupOrder?: string[]; @state() private columnOrder?: string[]; @state() private hiddenColumns?: string[]; @state() private _sortColumn?: string; @state() private _sortDirection: SortingDirection = null; @state() private _groupColumn?: string; @state() private _selectMode = false; @query("ha-data-table", true) private _dataTable!: HaDataTable; @query("ha-input-search") private _searchInput!: HaInputSearch; protected firstUpdated() { this._validateStatistics(); } private _displayData = memoizeOne( (data: StatisticData[], localize: LocalizeFunc): DisplayedStatisticData[] => data.map((item) => ({ ...item, displayName: item.state ? computeStateName(item.state) : item.name || item.statistic_id, issues_string: item.issues ?.map( (issue) => localize( `ui.panel.config.developer-tools.tabs.statistics.issues.${issue.type}`, issue.data ) || issue.type ) .join(" "), })) ); private _columns = memoizeOne( ( localize: LocalizeFunc ): DataTableColumnContainer => ({ displayName: { title: localize( "ui.panel.config.developer-tools.tabs.statistics.data_table.name" ), main: true, sortable: true, filterable: true, flex: 2, }, statistic_id: { title: localize( "ui.panel.config.developer-tools.tabs.statistics.data_table.statistic_id" ), sortable: true, filterable: true, hidden: this.narrow, }, statistics_unit_of_measurement: { title: localize( "ui.panel.config.developer-tools.tabs.statistics.data_table.statistics_unit" ), sortable: true, filterable: true, forceLTR: true, }, source: { title: localize( "ui.panel.config.developer-tools.tabs.statistics.data_table.source" ), sortable: true, filterable: true, groupable: true, }, issues_string: { title: localize( "ui.panel.config.developer-tools.tabs.statistics.data_table.issue" ), sortable: true, filterable: true, groupable: true, direction: "asc", flex: 2, template: (statistic) => html`${statistic.issues_string ?? localize( "ui.panel.config.developer-tools.tabs.statistics.no_issue" )}`, }, fix: { title: "", label: this.hass.localize( "ui.panel.config.developer-tools.tabs.statistics.fix_issue.fix" ), type: "icon", template: (statistic) => html`${statistic.issues ? html` ${localize( statistic.issues.some((issue) => FIXABLE_ISSUES.includes(issue.type) ) ? "ui.panel.config.developer-tools.tabs.statistics.fix_issue.fix" : "ui.panel.config.developer-tools.tabs.statistics.fix_issue.info" )} ` : "—"}`, minWidth: "113px", maxWidth: "113px", showNarrow: true, }, actions: { title: "", label: localize( "ui.panel.config.developer-tools.tabs.statistics.adjust_sum" ), type: "icon-button", showNarrow: true, template: (statistic) => statistic.has_sum ? html` ` : "", }, }) ); protected render() { const localize = this.hass.localize; const columns = this._columns(this.hass.localize); const selectModeBtn = !this._selectMode ? html` ` : nothing; const searchBar = html` `; const sortByMenu = Object.values(columns).find((col) => col.sortable) ? html` ${Object.entries(columns).map(([id, column]) => column.sortable ? html` ${this._sortColumn === id ? html` ` : nothing} ${column.title || column.label} ` : nothing )} ` : nothing; const groupByMenu = Object.values(columns).find((col) => col.groupable) ? html` ${Object.entries(columns).map(([id, column]) => column.groupable ? html` ${column.title || column.label} ` : nothing )} ${localize("ui.components.subpage-data-table.dont_group_by")} ${localize( "ui.components.subpage-data-table.collapse_all_groups" )} ${localize("ui.components.subpage-data-table.expand_all_groups")} ` : nothing; const settingsButton = html` `; return html`
${this._selectMode ? html`
${localize("ui.components.subpage-data-table.select_all")} ${localize( "ui.panel.config.developer-tools.tabs.statistics.data_table.select_all_issues" )} ${localize("ui.components.subpage-data-table.select_none")} ${localize( "ui.components.subpage-data-table.exit_selection_mode" )}

${localize("ui.components.subpage-data-table.selected", { selected: this._selected.length, })}

` : nothing}
${this.narrow ? html`
${searchBar}
` : ""} ${!this.narrow ? html`
${selectModeBtn}${searchBar}${groupByMenu}${sortByMenu}${settingsButton}
` : html`
${selectModeBtn}${groupByMenu}${sortByMenu}${settingsButton}
`}
`; } private _handleSearchChange(ev: InputEvent) { if (this.filter === (ev.target as HaInputSearch).value) { return; } this.filter = (ev.target as HaInputSearch).value ?? ""; } private _handleSelectionChanged( ev: HASSDomEvent ): void { this._selected = ev.detail.value; } private _handleTableSortingChanged( ev: CustomEvent<{ column: string; direction: SortingDirection }> ) { const { column, direction } = ev.detail; this._sortColumn = column; this._sortDirection = direction; } private _handleSortBy(ev: HaDropdownSelectEvent) { ev.preventDefault(); // keep dropdown open const columnId = ev.detail.item.value; if (!this._sortDirection || this._sortColumn !== columnId) { this._sortDirection = "asc"; } else if (this._sortDirection === "asc") { this._sortDirection = "desc"; } else { this._sortDirection = "asc"; } this._sortColumn = columnId; } private _handleOverflowGroupBy = (ev: HaDropdownSelectEvent) => { const action = ev.detail.item.value; if (!action) { return; } switch (action) { case "collapse_all": this._collapseAllGroups(); return; case "expand_all": this._expandAllGroups(); return; case "none": this._setGroupColumn(); return; default: this._setGroupColumn(action); } }; private _setGroupColumn(columnId?: string) { this._groupColumn = columnId; } private _openSettings() { showDataTableSettingsDialog(this, { columns: this._columns(this.hass.localize), hiddenColumns: this.hiddenColumns, columnOrder: this.columnOrder, onUpdate: ( columnOrder: string[] | undefined, hiddenColumns: string[] | undefined ) => { this.columnOrder = columnOrder; this.hiddenColumns = hiddenColumns; }, localizeFunc: this.hass.localize, }); } private _collapseAllGroups() { this._dataTable.collapseAllGroups(); } private _expandAllGroups() { this._dataTable.expandAllGroups(); } private _enableSelectMode() { this._selectMode = true; } private _disableSelectMode() { this._selectMode = false; this._dataTable.clearSelection(); } private _selectAll() { this._dataTable.selectAll(); } private _selectNone() { this._dataTable.clearSelection(); } private _selectAllIssues() { this._dataTable.select( this._data .filter((statistic) => statistic.issues) .map((statistic) => statistic.statistic_id), true ); } private _showStatisticsAdjustSumDialog(ev) { ev.stopPropagation(); showStatisticsAdjustSumDialog(this, { statistic: ev.currentTarget.statistic, }); } private _rowClicked(ev) { const id = ev.detail.id; if (id in this.hass.states) { fireEvent(this, "hass-more-info", { entityId: id }); } } private async _validateStatistics() { const [statisticIds, issues] = await Promise.all([ getStatisticIds(this.hass), validateStatistics(this.hass), ]); updateStatisticsIssues(this.hass); const statsIds = new Set(); this._data = statisticIds.map((statistic) => { statsIds.add(statistic.statistic_id); return { ...statistic, state: this.hass.states[statistic.statistic_id], issues: issues[statistic.statistic_id], }; }); Object.keys(issues).forEach((statisticId) => { if (!statsIds.has(statisticId)) { this._data.push({ statistic_id: statisticId, statistics_unit_of_measurement: "", source: "", state: this.hass.states[statisticId], issues: issues[statisticId], mean_type: StatisticMeanType.NONE, has_sum: false, unit_class: null, }); } }); } private _clearSelected = async () => { if (!this._selected.length) { return; } const deletableIds = this._selected; await showConfirmationDialog(this, { title: this.hass.localize( "ui.panel.config.developer-tools.tabs.statistics.multi_delete.title" ), text: html`${this.hass.localize( "ui.panel.config.developer-tools.tabs.statistics.multi_delete.info_text", { statistic_count: deletableIds.length } )}`, confirmText: this.hass.localize("ui.common.delete"), destructive: true, confirm: async () => { await clearStatistics(this.hass, deletableIds); this._validateStatistics(); this._dataTable.clearSelection(); }, }); }; private _fixIssue = async (ev) => { const issues = (ev.currentTarget.data as StatisticsValidationResult[]).sort( (itemA, itemB) => (FIX_ISSUES_ORDER[itemA.type] ?? 99) - (FIX_ISSUES_ORDER[itemB.type] ?? 99) ); const issue = issues[0]; await fixStatisticsIssue(this, issue); this._validateStatistics(); }; protected supportedShortcuts(): SupportedShortcuts { return { f: () => this._searchInput.focus(), }; } static get styles(): CSSResultGroup { return [ haStyle, css` :host { display: block; height: 100%; } .table-with-toolbars { height: 100%; display: flex; flex-direction: column; gap: var(--ha-space-2); } ha-data-table { width: 100%; flex-grow: 1; --data-table-border-width: 0; } :host([narrow]) { --expansion-panel-summary-padding: 0 16px; } .table-header { display: flex; align-items: center; --mdc-shape-small: 0; height: 56px; width: 100%; justify-content: space-between; padding: 0 var(--ha-space-4); gap: var(--ha-space-4); box-sizing: border-box; background: var(--primary-background-color); border-bottom: 1px solid var(--divider-color); } ha-input-search { flex: 1; } .search-toolbar { display: flex; align-items: center; color: var(--secondary-text-color); } .narrow-header-row { display: flex; align-items: center; gap: var(--ha-space-4); padding: 0 var(--ha-space-4); overflow-x: scroll; -ms-overflow-style: none; scrollbar-width: none; } .selection-bar { background: rgba(var(--rgb-primary-color), 0.1); width: 100%; display: flex; align-items: center; justify-content: space-between; padding: var(--ha-space-2) var(--ha-space-3); box-sizing: border-box; font-size: var(--ha-font-size-m); --ha-assist-chip-container-color: var(--card-background-color); } .selection-controls { display: flex; align-items: center; gap: var(--ha-space-2); } .selection-controls p { margin-left: var(--ha-space-2); margin-inline-start: var(--ha-space-2); margin-inline-end: initial; } .center-vertical { display: flex; align-items: center; gap: var(--ha-space-2); } .relative { position: relative; } ha-assist-chip { --ha-assist-chip-container-shape: 10px; --ha-assist-chip-container-color: var(--card-background-color); } .select-mode-chip { --md-assist-chip-icon-label-space: 0; --md-assist-chip-trailing-space: 8px; } ha-dialog { --mdc-dialog-min-width: 100vw; --mdc-dialog-max-width: 100vw; --mdc-dialog-min-height: 100%; --mdc-dialog-max-height: 100%; --vertical-align-dialog: flex-end; --ha-dialog-border-radius: var(--ha-border-radius-square); --dialog-content-padding: 0; } ha-dropdown ha-assist-chip { --md-assist-chip-trailing-space: 8px; } `, ]; } } declare global { interface HTMLElementTagNameMap { "developer-tools-statistics": HaPanelDevStatistics; } }