From 2d5ae78521c4e3f8a8af410aef347b2eebe4d032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 28 May 2021 15:49:40 +0200 Subject: [PATCH] Add selection to snapshot table for mass deletion (#9284) Co-authored-by: Bram Kragten --- hassio/src/snapshots/hassio-snapshots.ts | 138 ++++++++++++++++++++++- src/data/hassio/snapshot.ts | 15 +++ src/translations/en.json | 6 + 3 files changed, 154 insertions(+), 5 deletions(-) diff --git a/hassio/src/snapshots/hassio-snapshots.ts b/hassio/src/snapshots/hassio-snapshots.ts index a13f37c9d2..56c527b667 100644 --- a/hassio/src/snapshots/hassio-snapshots.ts +++ b/hassio/src/snapshots/hassio-snapshots.ts @@ -1,15 +1,17 @@ import "@material/mwc-button"; import { ActionDetail } from "@material/mwc-list"; import "@material/mwc-list/mwc-list-item"; -import { mdiDotsVertical, mdiPlus } from "@mdi/js"; +import { mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js"; import { + css, CSSResultGroup, html, LitElement, PropertyValues, TemplateResult, } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { customElement, property, query, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; import { atLeastVersion } from "../../../src/common/config/version"; import relativeTime from "../../../src/common/datetime/relative_time"; @@ -17,18 +19,25 @@ import { HASSDomEvent } from "../../../src/common/dom/fire_event"; import { DataTableColumnContainer, RowClickedEvent, + SelectionChangedEvent, } from "../../../src/components/data-table/ha-data-table"; import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-fab"; +import { extractApiErrorMessage } from "../../../src/data/hassio/common"; import { fetchHassioSnapshots, friendlyFolderName, HassioSnapshot, reloadHassioSnapshots, + removeSnapshot, } from "../../../src/data/hassio/snapshot"; import { Supervisor } from "../../../src/data/supervisor/supervisor"; -import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../src/dialogs/generic/show-dialog-box"; import "../../../src/layouts/hass-tabs-subpage-data-table"; +import type { HaTabsSubpageDataTable } from "../../../src/layouts/hass-tabs-subpage-data-table"; import { haStyle } from "../../../src/resources/styles"; import { HomeAssistant, Route } from "../../../src/types"; import { showHassioCreateSnapshotDialog } from "../dialogs/snapshot/show-dialog-hassio-create-snapshot"; @@ -49,10 +58,15 @@ export class HassioSnapshots extends LitElement { @property({ type: Boolean }) public isWide!: boolean; - private _firstUpdatedCalled = false; + @state() private _selectedSnapshots: string[] = []; @state() private _snapshots?: HassioSnapshot[] = []; + @query("hass-tabs-subpage-data-table", true) + private _dataTable!: HaTabsSubpageDataTable; + + private _firstUpdatedCalled = false; + public connectedCallback(): void { super.connectedCallback(); if (this.hass && this._firstUpdatedCalled) { @@ -153,7 +167,9 @@ export class HassioSnapshots extends LitElement { .data=${this._snapshotData(this._snapshots || [])} id="slug" @row-click=${this._handleRowClicked} + @selection-changed=${this._handleSelectionChanged} clickable + selectable hasFab main-page supervisor @@ -176,6 +192,45 @@ export class HassioSnapshots extends LitElement { : ""} + ${this._selectedSnapshots.length + ? html`
+

+ ${this.supervisor.localize("snapshot.selected", { + number: this._selectedSnapshots.length, + })} +

+
+ ${!this.narrow + ? html` + + ${this.supervisor.localize("snapshot.delete_selected")} + + ` + : html` + + + + + ${this.supervisor.localize("snapshot.delete_selected")} + + `} +
+
` + : ""} + + ): void { + this._selectedSnapshots = ev.detail.value; + } + private _showUploadSnapshotDialog() { showSnapshotUploadDialog(this, { showSnapshot: (slug: string) => @@ -216,6 +277,35 @@ export class HassioSnapshots extends LitElement { this._snapshots = await fetchHassioSnapshots(this.hass); } + private async _deleteSelected() { + const confirm = await showConfirmationDialog(this, { + title: this.supervisor.localize("snapshot.delete_snapshot_title"), + text: this.supervisor.localize("snapshot.delete_snapshot_text", { + number: this._selectedSnapshots.length, + }), + confirmText: this.supervisor.localize("snapshot.delete_snapshot_confirm"), + }); + + if (!confirm) { + return; + } + + try { + await Promise.all( + this._selectedSnapshots.map((slug) => removeSnapshot(this.hass, slug)) + ); + } catch (err) { + showAlertDialog(this, { + title: this.supervisor.localize("snapshot.failed_to_delete"), + text: extractApiErrorMessage(err), + }); + return; + } + await reloadHassioSnapshots(this.hass); + this._snapshots = await fetchHassioSnapshots(this.hass); + this._dataTable.clearSelection(); + } + private _handleRowClicked(ev: HASSDomEvent) { const slug = ev.detail.id; showHassioSnapshotDialog(this, { @@ -244,7 +334,45 @@ export class HassioSnapshots extends LitElement { } static get styles(): CSSResultGroup { - return [haStyle, hassioStyle]; + return [ + haStyle, + hassioStyle, + css` + .table-header { + display: flex; + justify-content: space-between; + align-items: center; + height: 58px; + border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12); + } + .header-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + color: var(--secondary-text-color); + position: relative; + top: -4px; + } + .selected-txt { + font-weight: bold; + padding-left: 16px; + color: var(--primary-text-color); + } + .table-header .selected-txt { + margin-top: 20px; + } + .header-toolbar .selected-txt { + font-size: 16px; + } + .header-toolbar .header-btns { + margin-right: -12px; + } + .header-btns > mwc-button, + .header-btns > mwc-icon-button { + margin: 8px; + } + `, + ]; } } diff --git a/src/data/hassio/snapshot.ts b/src/data/hassio/snapshot.ts index ba34776228..b2f1c36c25 100644 --- a/src/data/hassio/snapshot.ts +++ b/src/data/hassio/snapshot.ts @@ -130,6 +130,21 @@ export const createHassioFullSnapshot = async ( ); }; +export const removeSnapshot = async (hass: HomeAssistant, slug: string) => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + await hass.callWS({ + type: "supervisor/api", + endpoint: `/snapshots/${slug}/remove`, + method: "post", + }); + return; + } + await hass.callApi>( + "POST", + `hassio/snapshots/${slug}/remove` + ); +}; + export const createHassioPartialSnapshot = async ( hass: HomeAssistant, data: HassioPartialSnapshotCreateParams diff --git a/src/translations/en.json b/src/translations/en.json index 6a700c664a..584e25f33e 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3896,6 +3896,12 @@ "available_snapshots": "Available Snapshots", "no_snapshots": "You don't have any snapshots yet.", "create_blocked_not_running": "Creating a snapshot is not possible right now because the system is in {state} state.", + "delete_selected": "Delete selected snapshots", + "delete_snapshot_title": "Delete snapshot", + "delete_snapshot_text": "Do you want to delete {number} {number, plural,\n one {snapshot}\n other {snapshots}\n}?", + "delete_snapshot_confirm": "delete", + "selected": "{number} selected", + "failed_to_delete": "Failed to delete", "could_not_create": "Could not create snapshot", "upload_snapshot": "Upload snapshot", "create_snapshot": "Create snapshot",