From f7532f34762a610ece3d0fb9ae508ac4537f5a67 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Wed, 2 Oct 2024 04:21:04 -0700 Subject: [PATCH 1/2] Devtools statistics - new style, multi-select, & multi-delete (#21813) * feat: auto-fix statistics * statistics multi-select and multi-fix * unused css * Change multi action to clear, fixes * Update developer-tools-statistics.ts * update translations * Add select all issues option * Update en.json * Update developer-tools-statistics.ts --------- Co-authored-by: Muka Schultze Co-authored-by: Bram Kragten --- src/components/data-table/ha-data-table.ts | 23 + .../statistics/developer-tools-statistics.ts | 598 +++++++++++++++++- src/translations/en.json | 8 + 3 files changed, 610 insertions(+), 19 deletions(-) diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index e0ab768d5b34..fa1a0e449d87 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -204,6 +204,29 @@ export class HaDataTable extends LitElement { this._checkedRowsChanged(); } + public select(ids: string[], clear?: boolean): void { + if (clear) { + this._checkedRows = []; + } + ids.forEach((id) => { + const row = this._filteredData.find((data) => data[this.id] === id); + if (row?.selectable !== false && !this._checkedRows.includes(id)) { + this._checkedRows.push(id); + } + }); + this._checkedRowsChanged(); + } + + public unselect(ids: string[]): void { + ids.forEach((id) => { + const index = this._checkedRows.indexOf(id); + if (index > -1) { + this._checkedRows.splice(index, 1); + } + }); + this._checkedRowsChanged(); + } + public connectedCallback() { super.connectedCallback(); if (this._filteredData.length) { diff --git a/src/panels/developer-tools/statistics/developer-tools-statistics.ts b/src/panels/developer-tools/statistics/developer-tools-statistics.ts index 3e4bb2526fd1..3169b3e85113 100644 --- a/src/panels/developer-tools/statistics/developer-tools-statistics.ts +++ b/src/panels/developer-tools/statistics/developer-tools-statistics.ts @@ -1,24 +1,50 @@ import "@material/mwc-button/mwc-button"; -import { mdiSlopeUphill } from "@mdi/js"; +import { + mdiArrowDown, + mdiArrowUp, + mdiClose, + mdiCog, + mdiFormatListChecks, + mdiMenuDown, + mdiSlopeUphill, + mdiUnfoldLessHorizontal, + mdiUnfoldMoreHorizontal, +} from "@mdi/js"; + import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; -import { CSSResultGroup, html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; -import { fireEvent } from "../../../common/dom/fire_event"; +import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { LocalizeFunc } from "../../../common/translations/localize"; +import "../../../components/chips/ha-assist-chip"; import "../../../components/data-table/ha-data-table"; -import type { DataTableColumnContainer } from "../../../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-md-button-menu"; +import "../../../components/ha-dialog"; +import { HaMenu } from "../../../components/ha-menu"; +import "../../../components/ha-md-menu-item"; +import "../../../components/search-input-outlined"; import { subscribeEntityRegistry } from "../../../data/entity_registry"; import { - getStatisticIds, StatisticsMetaData, StatisticsValidationResult, + clearStatistics, + getStatisticIds, validateStatistics, } from "../../../data/recorder"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; +import { showConfirmationDialog } from "../../lovelace/custom-card-helpers"; import { fixStatisticsIssue } from "./fix-statistics"; import { showStatisticsAdjustSumDialog } from "./show-dialog-statistics-adjust-sum"; @@ -30,9 +56,17 @@ const FIX_ISSUES_ORDER = { units_changed: 3, }; +const FIXABLE_ISSUES = [ + "no_state", + "entity_no_longer_recorded", + "unsupported_state_class", + "units_changed", +]; + type StatisticData = StatisticsMetaData & { issues?: StatisticsValidationResult[]; state?: HassEntity; + selectable?: boolean; }; type DisplayedStatisticData = StatisticData & { @@ -48,8 +82,40 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) { @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("#group-by-menu") private _groupByMenu!: HaMenu; + + @query("#sort-by-menu") private _sortByMenu!: HaMenu; + private _disabledEntities = new Set(); + private _toggleGroupBy() { + this._groupByMenu.open = !this._groupByMenu.open; + } + + private _toggleSortBy() { + this._sortByMenu.open = !this._sortByMenu.open; + } + protected firstUpdated() { this._validateStatistics(); } @@ -108,6 +174,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) { ), sortable: true, filterable: true, + groupable: true, }, issues_string: { title: localize( @@ -115,6 +182,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) { ), sortable: true, filterable: true, + groupable: true, direction: "asc", flex: 2, template: (statistic) => @@ -133,7 +201,11 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) { .data=${statistic.issues} > ${localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.fix" + statistic.issues.some((issue) => + FIXABLE_ISSUES.includes(issue.type) + ) + ? "ui.panel.developer-tools.tabs.statistics.fix_issue.fix" + : "ui.panel.developer-tools.tabs.statistics.fix_issue.info" )} ` : "—"}`, @@ -164,22 +236,367 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) { ); 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` + + + + ` + : nothing; + + const groupByMenu = Object.values(columns).find((col) => col.groupable) + ? html` + + + ` + : nothing; + + const settingsButton = html` + + `; + return html` - + ${this._selectMode + ? html`
+
+ + + + + + +
+ ${localize("ui.components.subpage-data-table.select_all")} +
+
+ +
+ ${localize( + "ui.panel.developer-tools.tabs.statistics.data_table.select_all_issues" + )} +
+
+ +
+ ${localize( + "ui.components.subpage-data-table.select_none" + )} +
+
+ + +
+ ${localize( + "ui.components.subpage-data-table.close_select_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} +
`} +
+ + + ${Object.entries(columns).map(([id, column]) => + column.groupable + ? html` + + ${column.title || column.label} + + ` + : nothing )} - .narrow=${this.narrow} - id="statistic_id" - clickable - @row-click=${this._rowClicked} - >
+ + ${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")} + + + + ${Object.entries(columns).map(([id, column]) => + column.sortable + ? html` + + ${this._sortColumn === id + ? html` + + ` + : nothing} + ${column.title || column.label} + + ` + : nothing + )} + `; } + private _handleSearchChange(ev: CustomEvent) { + if (this.filter === ev.detail.value) { + return; + } + this.filter = ev.detail.value; + } + + private _handleSelectionChanged( + ev: HASSDomEvent + ): void { + this._selected = ev.detail.value; + } + + private _handleSortBy(ev) { + const columnId = ev.currentTarget.value; + if (!this._sortDirection || this._sortColumn !== columnId) { + this._sortDirection = "asc"; + } else if (this._sortDirection === "asc") { + this._sortDirection = "desc"; + } else { + this._sortDirection = null; + } + this._sortColumn = this._sortDirection === null ? undefined : columnId; + } + + private _handleGroupBy(ev) { + this._setGroupColumn(ev.currentTarget.value); + } + + 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, { @@ -253,6 +670,31 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) { }); } + private _clearSelected = async () => { + if (!this._selected.length) { + return; + } + + const deletableIds = this._selected; + + await showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.developer-tools.tabs.statistics.multi_delete.title" + ), + text: html`${this.hass.localize( + "ui.panel.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) => @@ -265,7 +707,125 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) { }; static get styles(): CSSResultGroup { - return haStyle; + return [ + haStyle, + css` + :host { + display: block; + height: 100%; + } + + ha-data-table { + width: 100%; + height: 100%; + --data-table-border-width: 0; + } + :host(:not([narrow])) ha-data-table { + height: calc(100vh - 1px - var(--header-height)); + display: block; + } + + :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 16px; + gap: 16px; + box-sizing: border-box; + background: var(--primary-background-color); + border-bottom: 1px solid var(--divider-color); + } + search-input-outlined { + flex: 1; + } + .search-toolbar { + display: flex; + align-items: center; + color: var(--secondary-text-color); + } + + .narrow-header-row { + display: flex; + align-items: center; + gap: 16px; + padding: 0 16px; + overflow-x: scroll; + -ms-overflow-style: none; + scrollbar-width: none; + } + + .selection-bar { + background: rgba(var(--rgb-primary-color), 0.1); + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + box-sizing: border-box; + font-size: 14px; + --ha-assist-chip-container-color: var(--card-background-color); + } + + .selection-controls { + display: flex; + align-items: center; + gap: 8px; + } + + .selection-controls p { + margin-left: 8px; + margin-inline-start: 8px; + margin-inline-end: initial; + } + + .center-vertical { + display: flex; + align-items: center; + gap: 8px; + } + + .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: calc( + 100vw - env(safe-area-inset-right) - env(safe-area-inset-left) + ); + --mdc-dialog-max-width: calc( + 100vw - env(safe-area-inset-right) - env(safe-area-inset-left) + ); + --mdc-dialog-min-height: 100%; + --mdc-dialog-max-height: 100%; + --vertical-align-dialog: flex-end; + --ha-dialog-border-radius: 0; + --dialog-content-padding: 0; + } + + #sort-by-anchor, + #group-by-anchor, + ha-button-menu-new ha-assist-chip { + --md-assist-chip-trailing-space: 8px; + } + `, + ]; } } diff --git a/src/translations/en.json b/src/translations/en.json index 06c27f4a9371..32c55ddef724 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -6966,11 +6966,18 @@ "entity_no_longer_recorded": "This entity is no longer being recorded.", "no_state": "There is no state available for this entity." }, + "delete_selected": "Delete selected statistics", + "multi_delete": { + "title": "Delete selected statistics", + "info_text": "Do you want to permanently delete the long term statistics {statistic_count, plural,\n one {of this entity}\n other {of {statistic_count} entities}\n} from your database?" + }, "fix_issue": { "fix": "Fix issue", "clearing_failed": "Clearing the statistics failed", "clearing_timeout_title": "Clearing not done yet", "clearing_timeout_text": "The clearing of the statistics took longer than expected, it might take longer for the issue to disappear.", + "fix_all": "Fix all", + "info": "Info", "no_support": { "title": "Fix issue", "info_text_1": "Fixing this issue is not supported yet." @@ -7029,6 +7036,7 @@ }, "adjust_sum": "Adjust sum", "data_table": { + "select_all_issues": "Select all with issues", "name": "Name", "statistic_id": "Statistic id", "statistics_unit": "Statistics unit", From cdd29295e54511646b1b4f09f896dafd7e43fe15 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 2 Oct 2024 13:37:47 +0200 Subject: [PATCH 2/2] Bumped version to 20241002.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c15c7b81b6a7..8e3b81412d0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20241002.0" +version = "20241002.1" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md"