-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Add panel to Backup integration #11671
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
215fde4
885508b
f658538
c15c39b
39d7a5c
f16827c
56ca6dc
8b363e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import { HomeAssistant } from "../types"; | ||
|
|
||
| export interface BackupContent { | ||
| slug: string; | ||
| date: string; | ||
| name: string; | ||
| size: number; | ||
| path: string; | ||
| } | ||
|
|
||
| export interface BackupData { | ||
| backing_up: boolean; | ||
| backups: BackupContent[]; | ||
| } | ||
|
|
||
| export const getBackupDownloadUrl = (slug: string) => | ||
| `/api/backup/download/${slug}`; | ||
|
|
||
| export const fetchBackupInfo = (hass: HomeAssistant): Promise<BackupData> => | ||
| hass.callWS({ | ||
| type: "backup/info", | ||
| }); | ||
|
|
||
| export const removeBackup = ( | ||
| hass: HomeAssistant, | ||
| slug: string | ||
| ): Promise<void> => | ||
| hass.callWS({ | ||
| type: "backup/remove", | ||
| slug, | ||
| }); | ||
|
|
||
| export const generateBackup = (hass: HomeAssistant): Promise<BackupContent> => | ||
| hass.callWS({ | ||
| type: "backup/generate", | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,224 @@ | ||
| import { mdiDelete, mdiDownload, mdiPlus } from "@mdi/js"; | ||
| import "@polymer/paper-tooltip/paper-tooltip"; | ||
| import { | ||
| css, | ||
| CSSResultGroup, | ||
| html, | ||
| LitElement, | ||
| PropertyValues, | ||
| TemplateResult, | ||
| } from "lit"; | ||
| import { customElement, property, state } from "lit/decorators"; | ||
| import memoize from "memoize-one"; | ||
| import { relativeTime } from "../../../common/datetime/relative_time"; | ||
| import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table"; | ||
| import "../../../components/ha-circular-progress"; | ||
| import "../../../components/ha-fab"; | ||
| import "../../../components/ha-icon"; | ||
| import "../../../components/ha-icon-overflow-menu"; | ||
| import "../../../components/ha-svg-icon"; | ||
| import { getSignedPath } from "../../../data/auth"; | ||
| import { | ||
| BackupContent, | ||
| BackupData, | ||
| fetchBackupInfo, | ||
| generateBackup, | ||
| getBackupDownloadUrl, | ||
| removeBackup, | ||
| } from "../../../data/backup"; | ||
| import { | ||
| showAlertDialog, | ||
| showConfirmationDialog, | ||
| } from "../../../dialogs/generic/show-dialog-box"; | ||
| import "../../../layouts/hass-loading-screen"; | ||
| import "../../../layouts/hass-tabs-subpage-data-table"; | ||
| import { HomeAssistant, Route } from "../../../types"; | ||
| import { fileDownload } from "../../../util/file_download"; | ||
| import { configSections } from "../ha-panel-config"; | ||
|
|
||
| @customElement("ha-config-backup") | ||
| class HaConfigBackup extends LitElement { | ||
| @property({ attribute: false }) public hass!: HomeAssistant; | ||
|
|
||
| @property({ type: Boolean }) public isWide!: boolean; | ||
|
|
||
| @property({ type: Boolean }) public narrow!: boolean; | ||
|
|
||
| @property({ attribute: false }) public route!: Route; | ||
|
|
||
| @state() private _backupData?: BackupData; | ||
|
|
||
| private _columns = memoize( | ||
| (narrow, _language): DataTableColumnContainer => ({ | ||
| name: { | ||
| title: this.hass.localize("ui.panel.config.backup.name"), | ||
| sortable: true, | ||
| filterable: true, | ||
| grows: true, | ||
| template: (entry: string, backup: BackupContent) => | ||
| html`${entry} | ||
| <div class="secondary">${backup.path}</div>`, | ||
| }, | ||
| size: { | ||
| title: this.hass.localize("ui.panel.config.backup.size"), | ||
| width: "15%", | ||
| hidden: narrow, | ||
| filterable: true, | ||
| sortable: true, | ||
| template: (entry: number) => Math.ceil(entry * 10) / 10 + " MB", | ||
| }, | ||
| date: { | ||
| title: this.hass.localize("ui.panel.config.backup.created"), | ||
| width: "15%", | ||
| direction: "desc", | ||
| hidden: narrow, | ||
| filterable: true, | ||
| sortable: true, | ||
| template: (entry: string) => | ||
| relativeTime(new Date(entry), this.hass.locale), | ||
| }, | ||
|
|
||
| actions: { | ||
| title: "", | ||
| width: "15%", | ||
| template: (_: string, backup: BackupContent) => | ||
| html` <ha-icon-overflow-menu | ||
| .hass=${this.hass} | ||
| .narrow=${this.narrow} | ||
| .items=${[ | ||
| // Download Button | ||
| { | ||
| path: mdiDownload, | ||
| label: this.hass.localize( | ||
| "ui.panel.config.backup.download_backup" | ||
| ), | ||
| action: () => this._downloadBackup(backup), | ||
| }, | ||
| // Delete button | ||
| { | ||
| path: mdiDelete, | ||
| label: this.hass.localize( | ||
| "ui.panel.config.backup.remove_backup" | ||
| ), | ||
| action: () => this._removeBackup(backup), | ||
| }, | ||
| ]} | ||
| style="color: var(--secondary-text-color)" | ||
| > | ||
| </ha-icon-overflow-menu>`, | ||
| }, | ||
| }) | ||
| ); | ||
|
|
||
| private _getItems = memoize((backupItems: BackupContent[]) => | ||
| backupItems.map((backup) => ({ | ||
| name: backup.name, | ||
| slug: backup.slug, | ||
| date: backup.date, | ||
| size: backup.size, | ||
| path: backup.path, | ||
| })) | ||
| ); | ||
|
|
||
| protected render(): TemplateResult { | ||
| if (!this.hass || this._backupData === undefined) { | ||
| return html` <hass-loading-screen></hass-loading-screen> `; | ||
|
balloob marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| return html` | ||
| <hass-tabs-subpage-data-table | ||
| .hass=${this.hass} | ||
| .narrow=${this.narrow} | ||
| back-path="/config" | ||
| .route=${this.route} | ||
| .tabs=${configSections.backup} | ||
| .columns=${this._columns(this.narrow, this.hass.language)} | ||
| .data=${this._getItems(this._backupData.backups)} | ||
| .noDataText=${this.hass.localize("ui.panel.config.backup.no_bakcups")} | ||
| > | ||
| <ha-fab | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we disable the fab and change the text while a backup is being generated? Otherwise the user will not know a backup is in progress.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, a user should always be able to click and fail even if we know it will never work.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not disable plus change text that were making a backup ? |
||
| slot="fab" | ||
| ?disabled=${this._backupData.backing_up} | ||
| .label=${this._backupData.backing_up | ||
| ? this.hass.localize("ui.panel.config.backup.creating_backup") | ||
| : this.hass.localize("ui.panel.config.backup.create_backup")} | ||
| extended | ||
| @click=${this._generateBackup} | ||
| > | ||
| ${this._backupData.backing_up | ||
| ? html`<ha-circular-progress | ||
| slot="icon" | ||
| active | ||
| ></ha-circular-progress>` | ||
| : html`<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>`} | ||
| </ha-fab> | ||
| </hass-tabs-subpage-data-table> | ||
| `; | ||
| } | ||
|
|
||
| protected firstUpdated(changedProps: PropertyValues) { | ||
| super.firstUpdated(changedProps); | ||
| this._getBackups(); | ||
| } | ||
|
|
||
| private async _getBackups(): Promise<void> { | ||
| this._backupData = await fetchBackupInfo(this.hass); | ||
| } | ||
|
|
||
| private async _downloadBackup(backup: BackupContent): Promise<void> { | ||
| const signedUrl = await getSignedPath( | ||
| this.hass, | ||
| getBackupDownloadUrl(backup.slug) | ||
| ); | ||
| fileDownload(signedUrl.path); | ||
| } | ||
|
|
||
| private async _generateBackup(): Promise<void> { | ||
| const confirm = await showConfirmationDialog(this, { | ||
| title: this.hass.localize("ui.panel.config.backup.create.title"), | ||
| text: this.hass.localize("ui.panel.config.backup.create.description"), | ||
| confirmText: this.hass.localize("ui.panel.config.backup.create.confirm"), | ||
| }); | ||
| if (!confirm) { | ||
| return; | ||
| } | ||
|
|
||
| generateBackup(this.hass) | ||
| .then(() => this._getBackups()) | ||
| .catch((err) => showAlertDialog(this, { text: (err as Error).message })); | ||
|
|
||
| await this._getBackups(); | ||
| } | ||
|
|
||
| private async _removeBackup(backup: BackupContent): Promise<void> { | ||
| const confirm = await showConfirmationDialog(this, { | ||
| title: this.hass.localize("ui.panel.config.backup.remove.title"), | ||
| text: this.hass.localize("ui.panel.config.backup.remove.description", { | ||
| name: backup.name, | ||
| }), | ||
| confirmText: this.hass.localize("ui.panel.config.backup.remove.confirm"), | ||
| }); | ||
| if (!confirm) { | ||
| return; | ||
| } | ||
|
|
||
| await removeBackup(this.hass, backup.slug); | ||
| await this._getBackups(); | ||
| } | ||
|
|
||
| static get styles(): CSSResultGroup { | ||
| return [ | ||
| css` | ||
| ha-fab[disabled] { | ||
| --mdc-theme-secondary: var(--disabled-text-color) !important; | ||
| } | ||
| `, | ||
| ]; | ||
| } | ||
| } | ||
|
|
||
| declare global { | ||
| interface HTMLElementTagNameMap { | ||
| "ha-config-backup": HaConfigBackup; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.