mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-28 12:17:23 +00:00
Compare commits
15 Commits
20251126.0
...
fix_downlo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67a5152c36 | ||
|
|
918fca4d0a | ||
|
|
258a19028b | ||
|
|
7b4536564e | ||
|
|
d9cd428bf4 | ||
|
|
be6ecefb9e | ||
|
|
99bde50c01 | ||
|
|
a2471f82a3 | ||
|
|
c90e820c7f | ||
|
|
0c0b657c79 | ||
|
|
8941837697 | ||
|
|
01adef6d9f | ||
|
|
4a1adf42b8 | ||
|
|
0c2e62ec91 | ||
|
|
2218a7121b |
@@ -8,7 +8,7 @@ export class HaCircularProgress extends MdCircularProgress {
|
|||||||
@property({ attribute: "aria-label", type: String }) public ariaLabel =
|
@property({ attribute: "aria-label", type: String }) public ariaLabel =
|
||||||
"Loading";
|
"Loading";
|
||||||
|
|
||||||
@property() public size: "tiny" | "small" | "medium" | "large" = "medium";
|
@property() public size?: "tiny" | "small" | "medium" | "large";
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues) {
|
protected updated(changedProps: PropertyValues) {
|
||||||
super.updated(changedProps);
|
super.updated(changedProps);
|
||||||
@@ -21,7 +21,6 @@ export class HaCircularProgress extends MdCircularProgress {
|
|||||||
case "small":
|
case "small":
|
||||||
this.style.setProperty("--md-circular-progress-size", "28px");
|
this.style.setProperty("--md-circular-progress-size", "28px");
|
||||||
break;
|
break;
|
||||||
// medium is default size
|
|
||||||
case "medium":
|
case "medium":
|
||||||
this.style.setProperty("--md-circular-progress-size", "48px");
|
this.style.setProperty("--md-circular-progress-size", "48px");
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,36 +1,98 @@
|
|||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
|
|
||||||
|
export interface BackupAgent {
|
||||||
|
agent_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface BackupContent {
|
export interface BackupContent {
|
||||||
slug: string;
|
backup_id: string;
|
||||||
date: string;
|
date: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
protected: boolean;
|
||||||
size: number;
|
size: number;
|
||||||
path: string;
|
agent_ids?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BackupData {
|
export interface BackupInfo {
|
||||||
backing_up: boolean;
|
|
||||||
backups: BackupContent[];
|
backups: BackupContent[];
|
||||||
|
backing_up: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getBackupDownloadUrl = (slug: string) =>
|
export interface BackupDetails {
|
||||||
`/api/backup/download/${slug}`;
|
backup: BackupContent;
|
||||||
|
}
|
||||||
|
|
||||||
export const fetchBackupInfo = (hass: HomeAssistant): Promise<BackupData> =>
|
export interface BackupAgentsInfo {
|
||||||
|
agents: BackupAgent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GenerateBackupParams = {
|
||||||
|
agent_ids: string[];
|
||||||
|
database_included?: boolean;
|
||||||
|
folders_included?: string[];
|
||||||
|
addons_included?: string[];
|
||||||
|
name?: string;
|
||||||
|
password?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBackupDownloadUrl = (id: string, agentId: string) =>
|
||||||
|
`/api/backup/download/${id}?agent_id=${agentId}`;
|
||||||
|
|
||||||
|
export const fetchBackupInfo = (hass: HomeAssistant): Promise<BackupInfo> =>
|
||||||
hass.callWS({
|
hass.callWS({
|
||||||
type: "backup/info",
|
type: "backup/info",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const removeBackup = (
|
export const fetchBackupDetails = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
slug: string
|
id: string
|
||||||
): Promise<void> =>
|
): Promise<BackupDetails> =>
|
||||||
hass.callWS({
|
hass.callWS({
|
||||||
type: "backup/remove",
|
type: "backup/details",
|
||||||
slug,
|
backup_id: id,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const generateBackup = (hass: HomeAssistant): Promise<BackupContent> =>
|
export const fetchBackupAgentsInfo = (
|
||||||
|
hass: HomeAssistant
|
||||||
|
): Promise<BackupAgentsInfo> =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "backup/agents/info",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const removeBackup = (hass: HomeAssistant, id: string): Promise<void> =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "backup/remove",
|
||||||
|
backup_id: id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const generateBackup = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
params: GenerateBackupParams
|
||||||
|
): Promise<{ backup_id: string }> =>
|
||||||
hass.callWS({
|
hass.callWS({
|
||||||
type: "backup/generate",
|
type: "backup/generate",
|
||||||
|
...params,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const uploadBackup = async (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
file: File
|
||||||
|
): Promise<void> => {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append("file", file);
|
||||||
|
const resp = await hass.fetchWithAuth("/api/backup/upload", {
|
||||||
|
method: "POST",
|
||||||
|
body: fd,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
throw new Error(`${resp.status} ${resp.statusText}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPreferredAgentForDownload = (agents: string[]) => {
|
||||||
|
const localAgents = agents.filter(
|
||||||
|
(agent) => agent.split(".")[0] === "backup"
|
||||||
|
);
|
||||||
|
return localAgents[0] || agents[0];
|
||||||
|
};
|
||||||
|
|||||||
136
src/dialogs/backup/dialog-backup-upload.ts
Normal file
136
src/dialogs/backup/dialog-backup-upload.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { mdiClose, mdiFolderUpload } from "@mdi/js";
|
||||||
|
import type { CSSResultGroup } from "lit";
|
||||||
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import "../../components/ha-alert";
|
||||||
|
import "../../components/ha-file-upload";
|
||||||
|
import "../../components/ha-header-bar";
|
||||||
|
import "../../components/ha-dialog";
|
||||||
|
import "../../components/ha-icon-button";
|
||||||
|
import { uploadBackup } from "../../data/backup";
|
||||||
|
import { haStyleDialog } from "../../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../../types";
|
||||||
|
import { showAlertDialog } from "../generic/show-dialog-box";
|
||||||
|
import type { HassDialog } from "../make-dialog-manager";
|
||||||
|
import type { BackupUploadDialogParams } from "./show-dialog-backup-upload";
|
||||||
|
|
||||||
|
const SUPPORTED_FORMAT = "application/x-tar";
|
||||||
|
|
||||||
|
@customElement("dialog-backup-upload")
|
||||||
|
export class DialogBackupUpload
|
||||||
|
extends LitElement
|
||||||
|
implements HassDialog<BackupUploadDialogParams>
|
||||||
|
{
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _dialogParams?: BackupUploadDialogParams;
|
||||||
|
|
||||||
|
@state() private _uploading = false;
|
||||||
|
|
||||||
|
@state() private _error?: string;
|
||||||
|
|
||||||
|
public async showDialog(
|
||||||
|
dialogParams: BackupUploadDialogParams
|
||||||
|
): Promise<void> {
|
||||||
|
this._dialogParams = dialogParams;
|
||||||
|
await this.updateComplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog(): void {
|
||||||
|
this._dialogParams = undefined;
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this._dialogParams || !this.hass) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-dialog
|
||||||
|
open
|
||||||
|
scrimClickAction
|
||||||
|
escapeKeyAction
|
||||||
|
hideActions
|
||||||
|
heading="Upload backup"
|
||||||
|
@closed=${this.closeDialog}
|
||||||
|
>
|
||||||
|
<div slot="heading">
|
||||||
|
<ha-header-bar>
|
||||||
|
<span slot="title"> Upload backup </span>
|
||||||
|
<ha-icon-button
|
||||||
|
.label=${this.hass.localize("ui.common.close")}
|
||||||
|
.path=${mdiClose}
|
||||||
|
slot="actionItems"
|
||||||
|
dialogAction="cancel"
|
||||||
|
dialogInitialFocus
|
||||||
|
></ha-icon-button>
|
||||||
|
</ha-header-bar>
|
||||||
|
</div>
|
||||||
|
<ha-file-upload
|
||||||
|
.hass=${this.hass}
|
||||||
|
.uploading=${this._uploading}
|
||||||
|
.icon=${mdiFolderUpload}
|
||||||
|
accept=${SUPPORTED_FORMAT}
|
||||||
|
label="Upload a backup"
|
||||||
|
supports="Supports .tar files"
|
||||||
|
@file-picked=${this._uploadFile}
|
||||||
|
></ha-file-upload>
|
||||||
|
${this._error
|
||||||
|
? html`<ha-alert alertType="error">${this._error}</ha-alert>`
|
||||||
|
: nothing}
|
||||||
|
</ha-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _uploadFile(ev: CustomEvent<{ files: File[] }>): Promise<void> {
|
||||||
|
this._error = undefined;
|
||||||
|
const file = ev.detail.files[0];
|
||||||
|
|
||||||
|
if (file.type !== SUPPORTED_FORMAT) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: "Unsupported file format",
|
||||||
|
text: "Please choose a Home Assistant backup file (.tar)",
|
||||||
|
confirmText: "ok",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._uploading = true;
|
||||||
|
try {
|
||||||
|
await uploadBackup(this.hass!, file);
|
||||||
|
this._dialogParams!.onUploadComplete();
|
||||||
|
this.closeDialog();
|
||||||
|
} catch (err: any) {
|
||||||
|
this._error = err.message;
|
||||||
|
} finally {
|
||||||
|
this._uploading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyleDialog,
|
||||||
|
css`
|
||||||
|
ha-header-bar {
|
||||||
|
--mdc-theme-on-primary: var(--primary-text-color);
|
||||||
|
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
/* overrule the ha-style-dialog max-height on small screens */
|
||||||
|
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||||
|
ha-header-bar {
|
||||||
|
--mdc-theme-primary: var(--app-header-background-color);
|
||||||
|
--mdc-theme-on-primary: var(--app-header-text-color, white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"dialog-backup-upload": DialogBackupUpload;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/dialogs/backup/show-dialog-backup-upload.ts
Normal file
17
src/dialogs/backup/show-dialog-backup-upload.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import "./dialog-backup-upload";
|
||||||
|
|
||||||
|
export interface BackupUploadDialogParams {
|
||||||
|
onUploadComplete: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const showBackupUploadDialog = (
|
||||||
|
element: HTMLElement,
|
||||||
|
dialogParams: BackupUploadDialogParams
|
||||||
|
): void => {
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "dialog-backup-upload",
|
||||||
|
dialogImport: () => import("./dialog-backup-upload"),
|
||||||
|
dialogParams,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -456,6 +456,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
${!this.narrow
|
${!this.narrow
|
||||||
? html`
|
? html`
|
||||||
<div slot="header">
|
<div slot="header">
|
||||||
|
<slot name="top_header"></slot>
|
||||||
<slot name="header">
|
<slot name="header">
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
${this.hasFilters && !this.showFilters
|
${this.hasFilters && !this.showFilters
|
||||||
|
|||||||
104
src/panels/config/backup/components/ha-backup-agents-select.ts
Normal file
104
src/panels/config/backup/components/ha-backup-agents-select.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { css, html, LitElement } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import "../../../../components/ha-checkbox";
|
||||||
|
import "../../../../components/ha-formfield";
|
||||||
|
import type { BackupAgent } from "../../../../data/backup";
|
||||||
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import { brandsUrl } from "../../../../util/brands-url";
|
||||||
|
import { domainToName } from "../../../../data/integration";
|
||||||
|
|
||||||
|
@customElement("ha-backup-agents-select")
|
||||||
|
class HaBackupAgentsSelect extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
|
public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public disabled = false;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public agents!: BackupAgent[];
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public disabledAgents?: string[];
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public value!: string[];
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<div class="agents">
|
||||||
|
${this.agents.map((agent) => this._renderAgent(agent))}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderAgent(agent: BackupAgent) {
|
||||||
|
const [domain, name] = agent.agent_id.split(".");
|
||||||
|
const domainName = domainToName(this.hass.localize, domain);
|
||||||
|
return html`
|
||||||
|
<ha-formfield>
|
||||||
|
<span class="label" slot="label">
|
||||||
|
<img
|
||||||
|
.src=${brandsUrl({
|
||||||
|
domain,
|
||||||
|
type: "icon",
|
||||||
|
useFallback: true,
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
})}
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
alt=""
|
||||||
|
slot="start"
|
||||||
|
/>
|
||||||
|
${domainName}: ${name}</span
|
||||||
|
>
|
||||||
|
<ha-checkbox
|
||||||
|
.checked=${this.value.includes(agent.agent_id)}
|
||||||
|
.value=${agent.agent_id}
|
||||||
|
.disabled=${this.disabled ||
|
||||||
|
this.disabledAgents?.includes(agent.agent_id)}
|
||||||
|
@change=${this._checkboxChanged}
|
||||||
|
></ha-checkbox>
|
||||||
|
</ha-formfield>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _checkboxChanged(ev: Event) {
|
||||||
|
const checkbox = ev.target as HTMLInputElement;
|
||||||
|
const value = checkbox.value;
|
||||||
|
const index = this.value.indexOf(value);
|
||||||
|
if (checkbox.checked && index === -1) {
|
||||||
|
this.value = [...this.value, value];
|
||||||
|
} else if (!checkbox.checked && index !== -1) {
|
||||||
|
this.value = [
|
||||||
|
...this.value.slice(0, index),
|
||||||
|
...this.value.slice(index + 1),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
img {
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
.agents {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-backup-agents-select": HaBackupAgentsSelect;
|
||||||
|
}
|
||||||
|
}
|
||||||
149
src/panels/config/backup/components/ha-backup-summary-card.ts
Normal file
149
src/panels/config/backup/components/ha-backup-summary-card.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import {
|
||||||
|
mdiAlertCircleCheckOutline,
|
||||||
|
mdiAlertOutline,
|
||||||
|
mdiCheck,
|
||||||
|
mdiInformationOutline,
|
||||||
|
mdiSync,
|
||||||
|
} from "@mdi/js";
|
||||||
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import "../../../../components/ha-button";
|
||||||
|
import "../../../../components/ha-card";
|
||||||
|
import "../../../../components/ha-circular-progress";
|
||||||
|
import "../../../../components/ha-icon";
|
||||||
|
|
||||||
|
type SummaryStatus = "success" | "error" | "info" | "warning" | "loading";
|
||||||
|
|
||||||
|
const ICONS: Record<SummaryStatus, string> = {
|
||||||
|
success: mdiCheck,
|
||||||
|
error: mdiAlertCircleCheckOutline,
|
||||||
|
warning: mdiAlertOutline,
|
||||||
|
info: mdiInformationOutline,
|
||||||
|
loading: mdiSync,
|
||||||
|
};
|
||||||
|
|
||||||
|
@customElement("ha-backup-summary-card")
|
||||||
|
class HaBackupSummaryCard extends LitElement {
|
||||||
|
@property()
|
||||||
|
public title!: string;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
public description!: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "has-action" })
|
||||||
|
public hasAction = false;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
public status: SummaryStatus = "info";
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<ha-card outlined>
|
||||||
|
<div class="summary">
|
||||||
|
${this.status === "loading"
|
||||||
|
? html`<ha-circular-progress indeterminate></ha-circular-progress>`
|
||||||
|
: html`
|
||||||
|
<div class="icon ${this.status}">
|
||||||
|
<ha-svg-icon .path=${ICONS[this.status]}></ha-svg-icon>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<p class="title">${this.title}</p>
|
||||||
|
<p class="description">${this.description}</p>
|
||||||
|
</div>
|
||||||
|
${this.hasAction
|
||||||
|
? html`
|
||||||
|
<div class="action">
|
||||||
|
<slot name="action"></slot>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
.summary {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 20px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
--icon-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
.icon.success {
|
||||||
|
--icon-color: var(--success-color);
|
||||||
|
}
|
||||||
|
.icon.warning {
|
||||||
|
--icon-color: var(--warning-color);
|
||||||
|
}
|
||||||
|
.icon.error {
|
||||||
|
--icon-color: var(--error-color);
|
||||||
|
}
|
||||||
|
.icon::before {
|
||||||
|
display: block;
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-color: var(--icon-color, var(--primary-color));
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
.icon ha-svg-icon {
|
||||||
|
color: var(--icon-color, var(--primary-color));
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
ha-circular-progress {
|
||||||
|
--md-circular-progress-size: 40px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: 22px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 28px;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
margin: 0;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.description {
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px;
|
||||||
|
letter-spacing: 0.25px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
margin: 0;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-backup-summary-card": HaBackupSummaryCard;
|
||||||
|
}
|
||||||
|
}
|
||||||
375
src/panels/config/backup/dialogs/dialog-generate-backup.ts
Normal file
375
src/panels/config/backup/dialogs/dialog-generate-backup.ts
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
import {
|
||||||
|
mdiChartBox,
|
||||||
|
mdiClose,
|
||||||
|
mdiCog,
|
||||||
|
mdiFolder,
|
||||||
|
mdiPlayBoxMultiple,
|
||||||
|
} from "@mdi/js";
|
||||||
|
import type { CSSResultGroup } from "lit";
|
||||||
|
import { LitElement, css, html, nothing } from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import "../../../../components/ha-button";
|
||||||
|
import "../../../../components/ha-dialog-header";
|
||||||
|
import "../../../../components/ha-expansion-panel";
|
||||||
|
import "../../../../components/ha-icon-button";
|
||||||
|
import "../../../../components/ha-icon-button-prev";
|
||||||
|
import "../../../../components/ha-md-dialog";
|
||||||
|
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||||
|
import "../../../../components/ha-md-select";
|
||||||
|
import "../../../../components/ha-md-select-option";
|
||||||
|
import "../../../../components/ha-settings-row";
|
||||||
|
import "../../../../components/ha-svg-icon";
|
||||||
|
import "../../../../components/ha-switch";
|
||||||
|
import "../../../../components/ha-textfield";
|
||||||
|
import type { BackupAgent } from "../../../../data/backup";
|
||||||
|
import { fetchBackupAgentsInfo, generateBackup } from "../../../../data/backup";
|
||||||
|
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||||
|
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import "../components/ha-backup-agents-select";
|
||||||
|
import type { GenerateBackupDialogParams } from "./show-dialog-generate-backup";
|
||||||
|
|
||||||
|
type FormData = {
|
||||||
|
name: string;
|
||||||
|
history: boolean;
|
||||||
|
media: boolean;
|
||||||
|
share: boolean;
|
||||||
|
addons_mode: "all" | "custom";
|
||||||
|
addons: string[];
|
||||||
|
agents_mode: "all" | "custom";
|
||||||
|
agents: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const INITIAL_FORM_DATA: FormData = {
|
||||||
|
name: "",
|
||||||
|
history: true,
|
||||||
|
media: false,
|
||||||
|
share: false,
|
||||||
|
addons_mode: "all",
|
||||||
|
addons: [],
|
||||||
|
agents_mode: "all",
|
||||||
|
agents: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const STEPS = ["data", "sync"] as const;
|
||||||
|
|
||||||
|
@customElement("ha-dialog-generate-backup")
|
||||||
|
class DialogGenerateBackup extends LitElement implements HassDialog {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _formData?: FormData;
|
||||||
|
|
||||||
|
@state() private _step?: "data" | "sync";
|
||||||
|
|
||||||
|
@state() private _agents: BackupAgent[] = [];
|
||||||
|
|
||||||
|
@state() private _params?: GenerateBackupDialogParams;
|
||||||
|
|
||||||
|
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||||
|
|
||||||
|
public showDialog(_params: GenerateBackupDialogParams): void {
|
||||||
|
this._step = STEPS[0];
|
||||||
|
this._formData = INITIAL_FORM_DATA;
|
||||||
|
this._params = _params;
|
||||||
|
this._fetchAgents();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _dialogClosed() {
|
||||||
|
if (this._params!.cancel) {
|
||||||
|
this._params!.cancel();
|
||||||
|
}
|
||||||
|
this._step = undefined;
|
||||||
|
this._formData = undefined;
|
||||||
|
this._agents = [];
|
||||||
|
this._params = undefined;
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _fetchAgents() {
|
||||||
|
const { agents } = await fetchBackupAgentsInfo(this.hass);
|
||||||
|
this._agents = agents;
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog() {
|
||||||
|
this._dialog?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _previousStep() {
|
||||||
|
const index = STEPS.indexOf(this._step!);
|
||||||
|
if (index === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._step = STEPS[index - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _nextStep() {
|
||||||
|
const index = STEPS.indexOf(this._step!);
|
||||||
|
if (index === STEPS.length - 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._step = STEPS[index + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this._step || !this._formData) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogTitle =
|
||||||
|
this._step === "sync" ? "Synchronization" : "Backup data";
|
||||||
|
|
||||||
|
const isFirstStep = this._step === STEPS[0];
|
||||||
|
const isLastStep = this._step === STEPS[STEPS.length - 1];
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-md-dialog open disable-cancel-action @closed=${this._dialogClosed}>
|
||||||
|
<ha-dialog-header slot="headline">
|
||||||
|
${isFirstStep
|
||||||
|
? html`
|
||||||
|
<ha-icon-button
|
||||||
|
slot="navigationIcon"
|
||||||
|
.label=${this.hass.localize("ui.dialogs.generic.close")}
|
||||||
|
.path=${mdiClose}
|
||||||
|
@click=${this.closeDialog}
|
||||||
|
></ha-icon-button>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<ha-icon-button-prev
|
||||||
|
slot="navigationIcon"
|
||||||
|
@click=${this._previousStep}
|
||||||
|
></ha-icon-button-prev>
|
||||||
|
`}
|
||||||
|
<span slot="title" .title=${dialogTitle}> ${dialogTitle} </span>
|
||||||
|
</ha-dialog-header>
|
||||||
|
<div slot="content" class="content">
|
||||||
|
${this._step === "data" ? this._renderData() : this._renderSync()}
|
||||||
|
</div>
|
||||||
|
<div slot="actions">
|
||||||
|
${isFirstStep
|
||||||
|
? html`<ha-button @click=${this.closeDialog}>Cancel</ha-button>`
|
||||||
|
: nothing}
|
||||||
|
${isLastStep
|
||||||
|
? html`<ha-button @click=${this._submit}>Create backup</ha-button>`
|
||||||
|
: html`<ha-button @click=${this._nextStep}>Next</ha-button>`}
|
||||||
|
</div>
|
||||||
|
</ha-md-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderData() {
|
||||||
|
if (!this._formData) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<ha-settings-row>
|
||||||
|
<ha-svg-icon slot="prefix" .path=${mdiCog}></ha-svg-icon>
|
||||||
|
<span slot="heading">Home Assistant settings</span>
|
||||||
|
<span slot="description">
|
||||||
|
With these settings you are able to restore your system.
|
||||||
|
</span>
|
||||||
|
<ha-switch disabled checked></ha-switch>
|
||||||
|
</ha-settings-row>
|
||||||
|
<ha-settings-row>
|
||||||
|
<ha-svg-icon slot="prefix" .path=${mdiChartBox}></ha-svg-icon>
|
||||||
|
<span slot="heading">History</span>
|
||||||
|
<span slot="description">For example of your energy dashboard.</span>
|
||||||
|
<ha-switch
|
||||||
|
id="history"
|
||||||
|
name="history"
|
||||||
|
@change=${this._switchChanged}
|
||||||
|
.checked=${this._formData.history}
|
||||||
|
></ha-switch>
|
||||||
|
</ha-settings-row>
|
||||||
|
<ha-settings-row>
|
||||||
|
<ha-svg-icon slot="prefix" .path=${mdiPlayBoxMultiple}></ha-svg-icon>
|
||||||
|
<span slot="heading">Media</span>
|
||||||
|
<span slot="description">
|
||||||
|
Folder that is often used for advanced or older configurations.
|
||||||
|
</span>
|
||||||
|
<ha-switch
|
||||||
|
id="media"
|
||||||
|
name="media"
|
||||||
|
@change=${this._switchChanged}
|
||||||
|
.checked=${this._formData.media}
|
||||||
|
></ha-switch>
|
||||||
|
</ha-settings-row>
|
||||||
|
<ha-settings-row>
|
||||||
|
<ha-svg-icon slot="prefix" .path=${mdiFolder}></ha-svg-icon>
|
||||||
|
<span slot="heading">Share folder</span>
|
||||||
|
<span slot="description">
|
||||||
|
Folder that is often used for advanced or older configurations.
|
||||||
|
</span>
|
||||||
|
<ha-switch
|
||||||
|
id="share"
|
||||||
|
name="share"
|
||||||
|
@change=${this._switchChanged}
|
||||||
|
.checked=${this._formData.share}
|
||||||
|
></ha-switch>
|
||||||
|
</ha-settings-row>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderSync() {
|
||||||
|
if (!this._formData) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<ha-textfield
|
||||||
|
name="name"
|
||||||
|
.label=${"Backup name"}
|
||||||
|
.value=${this._formData.name}
|
||||||
|
@change=${this._nameChanged}
|
||||||
|
>
|
||||||
|
</ha-textfield>
|
||||||
|
<ha-settings-row>
|
||||||
|
<span slot="heading">Locations</span>
|
||||||
|
<span slot="description">
|
||||||
|
What locations you want to automatically backup to.
|
||||||
|
</span>
|
||||||
|
<ha-md-select
|
||||||
|
@change=${this._agentModeChanged}
|
||||||
|
.value=${this._formData.agents_mode}
|
||||||
|
>
|
||||||
|
<ha-md-select-option value="all">
|
||||||
|
<div slot="headline">All (${this._agents.length})</div>
|
||||||
|
</ha-md-select-option>
|
||||||
|
<ha-md-select-option value="custom">
|
||||||
|
<div slot="headline">Custom</div>
|
||||||
|
</ha-md-select-option>
|
||||||
|
</ha-md-select>
|
||||||
|
</ha-settings-row>
|
||||||
|
${this._formData.agents_mode === "custom"
|
||||||
|
? html`
|
||||||
|
<ha-expansion-panel .header=${"Location"} outlined expanded>
|
||||||
|
<ha-backup-agents-select
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${this._formData.agents}
|
||||||
|
@value-changed=${this._agentsChanged}
|
||||||
|
.agents=${this._agents}
|
||||||
|
></ha-backup-agents-select>
|
||||||
|
</ha-expansion-panel>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _agentModeChanged(ev) {
|
||||||
|
const select = ev.currentTarget;
|
||||||
|
this._formData = {
|
||||||
|
...this._formData!,
|
||||||
|
agents_mode: select.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private _agentsChanged(ev) {
|
||||||
|
this._formData = {
|
||||||
|
...this._formData!,
|
||||||
|
agents: ev.detail.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private _switchChanged(ev) {
|
||||||
|
const _switch = ev.currentTarget;
|
||||||
|
this._formData = {
|
||||||
|
...this._formData!,
|
||||||
|
[_switch.id]: _switch.checked,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private _nameChanged(ev) {
|
||||||
|
this._formData = {
|
||||||
|
...this._formData!,
|
||||||
|
name: ev.target.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _submit() {
|
||||||
|
if (!this._formData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
addons,
|
||||||
|
addons_mode,
|
||||||
|
agents,
|
||||||
|
agents_mode,
|
||||||
|
history,
|
||||||
|
media,
|
||||||
|
name,
|
||||||
|
share,
|
||||||
|
} = this._formData;
|
||||||
|
|
||||||
|
const folders: string[] = [];
|
||||||
|
if (media) {
|
||||||
|
folders.push("media");
|
||||||
|
}
|
||||||
|
if (share) {
|
||||||
|
folders.push("share");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Fetch all addons
|
||||||
|
const ALL_ADDONS = [];
|
||||||
|
const { backup_id } = await generateBackup(this.hass, {
|
||||||
|
name,
|
||||||
|
agent_ids:
|
||||||
|
agents_mode === "all"
|
||||||
|
? this._agents.map((agent) => agent.agent_id)
|
||||||
|
: agents,
|
||||||
|
database_included: history,
|
||||||
|
folders_included: folders,
|
||||||
|
addons_included: addons_mode === "all" ? ALL_ADDONS : addons,
|
||||||
|
});
|
||||||
|
|
||||||
|
this._params!.submit?.({ backup_id });
|
||||||
|
this.closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
haStyleDialog,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
--dialog-content-overflow: visible;
|
||||||
|
}
|
||||||
|
ha-md-dialog {
|
||||||
|
--dialog-content-padding: 24px;
|
||||||
|
}
|
||||||
|
ha-settings-row {
|
||||||
|
--settings-row-prefix-display: flex;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
ha-settings-row > ha-svg-icon {
|
||||||
|
align-self: center;
|
||||||
|
margin-inline-end: 16px;
|
||||||
|
}
|
||||||
|
ha-settings-row > ha-md-select {
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
ha-settings-row > ha-md-select > span {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
ha-settings-row > ha-md-select-option {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
ha-textfield {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-dialog-generate-backup": DialogGenerateBackup;
|
||||||
|
}
|
||||||
|
}
|
||||||
147
src/panels/config/backup/dialogs/dialog-new-backup.ts
Normal file
147
src/panels/config/backup/dialogs/dialog-new-backup.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { mdiBackupRestore, mdiClose, mdiCogs } from "@mdi/js";
|
||||||
|
import type { CSSResultGroup } from "lit";
|
||||||
|
import { LitElement, css, html, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import "../../../../components/ha-dialog-header";
|
||||||
|
import "../../../../components/ha-icon-button";
|
||||||
|
import "../../../../components/ha-icon-next";
|
||||||
|
import "../../../../components/ha-md-dialog";
|
||||||
|
import "../../../../components/ha-md-list";
|
||||||
|
import "../../../../components/ha-md-list-item";
|
||||||
|
import "../../../../components/ha-svg-icon";
|
||||||
|
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||||
|
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import type { NewBackupDialogParams } from "./show-dialog-new-backup";
|
||||||
|
|
||||||
|
@customElement("ha-dialog-new-backup")
|
||||||
|
class DialogNewBackup extends LitElement implements HassDialog {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _opened = false;
|
||||||
|
|
||||||
|
@state() private _params?: NewBackupDialogParams;
|
||||||
|
|
||||||
|
public showDialog(params: NewBackupDialogParams): void {
|
||||||
|
this._opened = true;
|
||||||
|
this._params = params;
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog(): void {
|
||||||
|
if (this._params!.cancel) {
|
||||||
|
this._params!.cancel();
|
||||||
|
}
|
||||||
|
if (this._opened) {
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
}
|
||||||
|
this._opened = false;
|
||||||
|
this._params = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this._opened || !this._params) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const heading = "New backup";
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-md-dialog
|
||||||
|
open
|
||||||
|
@closed=${this.closeDialog}
|
||||||
|
aria-labelledby="dialog-box-title"
|
||||||
|
aria-describedby="dialog-box-description"
|
||||||
|
>
|
||||||
|
<ha-dialog-header slot="headline">
|
||||||
|
<ha-icon-button
|
||||||
|
slot="navigationIcon"
|
||||||
|
@click=${this.closeDialog}
|
||||||
|
.label=${this.hass.localize("ui.common.close")}
|
||||||
|
.path=${mdiClose}
|
||||||
|
></ha-icon-button>
|
||||||
|
<span slot="title" id="dialog-light-color-favorite-title">
|
||||||
|
${heading}
|
||||||
|
</span>
|
||||||
|
</ha-dialog-header>
|
||||||
|
<div slot="content">
|
||||||
|
<ha-md-list
|
||||||
|
innerRole="listbox"
|
||||||
|
itemRoles="option"
|
||||||
|
innerAriaLabel=${heading}
|
||||||
|
rootTabbable
|
||||||
|
dialogInitialFocus
|
||||||
|
>
|
||||||
|
<ha-md-list-item @click=${this._automatic} type="button">
|
||||||
|
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
|
||||||
|
<span slot="headline">Use automatic backup settings</span>
|
||||||
|
<span slot="supporting-text">
|
||||||
|
Trigger a backup using the configured settings for automatic backups
|
||||||
|
</span>
|
||||||
|
<ha-icon-next slot="end"></ha-icon-next>
|
||||||
|
</ha-md-list-item>
|
||||||
|
<ha-md-list-item @click=${this._manual} type="button">
|
||||||
|
<ha-svg-icon slot="start" .path=${mdiCogs}></ha-svg-icon>
|
||||||
|
|
||||||
|
<span slot="headline"> Create a manual backup</span>
|
||||||
|
<span slot="supporting-text">
|
||||||
|
Create a backup with custom settings (e.g. specific add-ons,
|
||||||
|
database, etc.)
|
||||||
|
</span>
|
||||||
|
<ha-icon-next slot="end"></ha-icon-next>
|
||||||
|
</ha-md-list-item>
|
||||||
|
</ha-md-list>
|
||||||
|
</div>
|
||||||
|
</ha-md-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _manual() {
|
||||||
|
this._params!.submit?.("manual");
|
||||||
|
this.closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _automatic() {
|
||||||
|
this._params!.submit?.("automatic");
|
||||||
|
this.closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
haStyleDialog,
|
||||||
|
css`
|
||||||
|
ha-md-dialog {
|
||||||
|
--dialog-content-padding: 0;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
div[slot="content"] {
|
||||||
|
margin-top: -16px;
|
||||||
|
}
|
||||||
|
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||||
|
ha-md-dialog {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
div[slot="content"] {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-md-list {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
ha-md-list-item {
|
||||||
|
}
|
||||||
|
ha-icon-next {
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-dialog-new-backup": DialogNewBackup;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
|
||||||
|
export interface GenerateBackupDialogParams {
|
||||||
|
submit?: (response: { backup_id: string }) => void;
|
||||||
|
cancel?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadGenerateBackupDialog = () =>
|
||||||
|
import("./dialog-generate-backup");
|
||||||
|
|
||||||
|
export const showGenerateBackupDialog = (
|
||||||
|
element: HTMLElement,
|
||||||
|
params: GenerateBackupDialogParams
|
||||||
|
) =>
|
||||||
|
new Promise<{ backup_id: string } | null>((resolve) => {
|
||||||
|
const origCancel = params.cancel;
|
||||||
|
const origSubmit = params.submit;
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "ha-dialog-generate-backup",
|
||||||
|
dialogImport: loadGenerateBackupDialog,
|
||||||
|
dialogParams: {
|
||||||
|
...params,
|
||||||
|
cancel: () => {
|
||||||
|
resolve(null);
|
||||||
|
if (origCancel) {
|
||||||
|
origCancel();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
submit: (response) => {
|
||||||
|
resolve(response);
|
||||||
|
if (origSubmit) {
|
||||||
|
origSubmit(response);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
37
src/panels/config/backup/dialogs/show-dialog-new-backup.ts
Normal file
37
src/panels/config/backup/dialogs/show-dialog-new-backup.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
|
||||||
|
export type NewBackupType = "automatic" | "manual";
|
||||||
|
export interface NewBackupDialogParams {
|
||||||
|
submit?: (type: NewBackupType) => void;
|
||||||
|
cancel?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadNewBackupDialog = () => import("./dialog-new-backup");
|
||||||
|
|
||||||
|
export const showNewBackupDialog = (
|
||||||
|
element: HTMLElement,
|
||||||
|
params: NewBackupDialogParams
|
||||||
|
) =>
|
||||||
|
new Promise<NewBackupType | null>((resolve) => {
|
||||||
|
const origCancel = params.cancel;
|
||||||
|
const origSubmit = params.submit;
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "ha-dialog-new-backup",
|
||||||
|
dialogImport: loadNewBackupDialog,
|
||||||
|
dialogParams: {
|
||||||
|
...params,
|
||||||
|
cancel: () => {
|
||||||
|
resolve(null);
|
||||||
|
if (origCancel) {
|
||||||
|
origCancel();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
submit: (response) => {
|
||||||
|
resolve(response);
|
||||||
|
if (origSubmit) {
|
||||||
|
origSubmit(response);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
132
src/panels/config/backup/ha-config-backup-automatic-config.ts
Normal file
132
src/panels/config/backup/ha-config-backup-automatic-config.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import type { TemplateResult } from "lit";
|
||||||
|
import { css, html, LitElement } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import "../../../components/ha-card";
|
||||||
|
import "../../../components/ha-settings-row";
|
||||||
|
import "../../../components/ha-switch";
|
||||||
|
import "../../../components/ha-select";
|
||||||
|
import "../../../components/ha-button";
|
||||||
|
import "../../../components/ha-list-item";
|
||||||
|
import "../../../layouts/hass-subpage";
|
||||||
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
|
||||||
|
@customElement("ha-config-backup-automatic-config")
|
||||||
|
class HaConfigBackupAutomaticConfig extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<hass-subpage
|
||||||
|
back-path="/config/backup"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.narrow=${this.narrow}
|
||||||
|
.header=${"Automatic backups"}
|
||||||
|
>
|
||||||
|
<div class="content">
|
||||||
|
<ha-card>
|
||||||
|
<div class="card-header">Automation</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<ha-settings-row>
|
||||||
|
<span slot="heading">Schedule</span>
|
||||||
|
<span slot="description">
|
||||||
|
How often you want to create a backup.
|
||||||
|
</span>
|
||||||
|
<ha-select naturalMenuWidth>
|
||||||
|
<ha-list-item>Daily at 02:00</ha-list-item>
|
||||||
|
</ha-select>
|
||||||
|
</ha-settings-row>
|
||||||
|
<ha-settings-row>
|
||||||
|
<span slot="heading">Maximum copies</span>
|
||||||
|
<span slot="description">
|
||||||
|
The number of backups that are saved
|
||||||
|
</span>
|
||||||
|
<ha-select naturalMenuWidth>
|
||||||
|
<ha-list-item>Latest 3 copies</ha-list-item>
|
||||||
|
</ha-select>
|
||||||
|
</ha-settings-row>
|
||||||
|
<ha-settings-row>
|
||||||
|
<span slot="heading">Locations</span>
|
||||||
|
<span slot="description">
|
||||||
|
What locations you want to automatically backup to.
|
||||||
|
</span>
|
||||||
|
<ha-button> Configure </ha-button>
|
||||||
|
</ha-settings-row>
|
||||||
|
<ha-settings-row>
|
||||||
|
<span slot="heading">Password</span>
|
||||||
|
<span slot="description">
|
||||||
|
Automatic backups are protected with this password
|
||||||
|
</span>
|
||||||
|
<ha-switch></ha-switch>
|
||||||
|
</ha-settings-row>
|
||||||
|
<ha-settings-row>
|
||||||
|
<span slot="heading">Custom backup name</span>
|
||||||
|
<span slot="description">
|
||||||
|
By default it will use the date and description (2024-07-05
|
||||||
|
Automatic backup).
|
||||||
|
</span>
|
||||||
|
<ha-switch></ha-switch>
|
||||||
|
</ha-settings-row>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
<ha-card>
|
||||||
|
<div class="card-header">Backup data</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<ha-settings-row>
|
||||||
|
<span slot="heading">
|
||||||
|
Home Assistant settings is always included
|
||||||
|
</span>
|
||||||
|
<span slot="description">
|
||||||
|
With these settings you are able to restore your system.
|
||||||
|
</span>
|
||||||
|
<ha-button>Learn more</ha-button>
|
||||||
|
</ha-settings-row>
|
||||||
|
<ha-settings-row>
|
||||||
|
<span slot="heading">History</span>
|
||||||
|
<span slot="description">
|
||||||
|
For example of your energy dashboard.
|
||||||
|
</span>
|
||||||
|
<ha-switch></ha-switch>
|
||||||
|
</ha-settings-row>
|
||||||
|
<ha-settings-row>
|
||||||
|
<span slot="heading">Media</span>
|
||||||
|
<span slot="description">For example camera recordings.</span>
|
||||||
|
<ha-switch></ha-switch>
|
||||||
|
</ha-settings-row>
|
||||||
|
<ha-settings-row>
|
||||||
|
<span slot="heading">Add-ons</span>
|
||||||
|
<span slot="description">
|
||||||
|
Select what add-ons you want to backup.
|
||||||
|
</span>
|
||||||
|
<ha-select naturalMenuWidth>
|
||||||
|
<ha-list-item>All, including new (4)</ha-list-item>
|
||||||
|
</ha-select>
|
||||||
|
</ha-settings-row>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
</div>
|
||||||
|
</hass-subpage>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
.content {
|
||||||
|
padding: 28px 20px 0;
|
||||||
|
max-width: 690px;
|
||||||
|
margin: 0 auto;
|
||||||
|
gap: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.card-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-config-backup-automatic-config": HaConfigBackupAutomaticConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
424
src/panels/config/backup/ha-config-backup-dashboard.ts
Normal file
424
src/panels/config/backup/ha-config-backup-dashboard.ts
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
import { mdiDelete, mdiDownload, mdiPlus } from "@mdi/js";
|
||||||
|
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||||
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||||
|
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||||
|
import { navigate } from "../../../common/navigate";
|
||||||
|
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||||
|
import type {
|
||||||
|
DataTableColumnContainer,
|
||||||
|
RowClickedEvent,
|
||||||
|
SelectionChangedEvent,
|
||||||
|
} from "../../../components/data-table/ha-data-table";
|
||||||
|
import "../../../components/ha-button";
|
||||||
|
import "../../../components/ha-card";
|
||||||
|
import "../../../components/ha-fab";
|
||||||
|
import "../../../components/ha-icon";
|
||||||
|
import "../../../components/ha-icon-next";
|
||||||
|
import "../../../components/ha-icon-overflow-menu";
|
||||||
|
import "../../../components/ha-svg-icon";
|
||||||
|
import { getSignedPath } from "../../../data/auth";
|
||||||
|
import {
|
||||||
|
fetchBackupInfo,
|
||||||
|
getBackupDownloadUrl,
|
||||||
|
removeBackup,
|
||||||
|
type BackupContent,
|
||||||
|
getPreferredAgentForDownload,
|
||||||
|
} from "../../../data/backup";
|
||||||
|
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||||
|
import {
|
||||||
|
showAlertDialog,
|
||||||
|
showConfirmationDialog,
|
||||||
|
} from "../../../dialogs/generic/show-dialog-box";
|
||||||
|
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||||
|
import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table";
|
||||||
|
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||||
|
import { haStyle } from "../../../resources/styles";
|
||||||
|
import type { HomeAssistant, Route } from "../../../types";
|
||||||
|
import { brandsUrl } from "../../../util/brands-url";
|
||||||
|
import { fileDownload } from "../../../util/file_download";
|
||||||
|
import "./components/ha-backup-summary-card";
|
||||||
|
import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup";
|
||||||
|
import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup";
|
||||||
|
|
||||||
|
@customElement("ha-config-backup-dashboard")
|
||||||
|
class HaConfigBackupDashboard extends SubscribeMixin(LitElement) {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public route!: Route;
|
||||||
|
|
||||||
|
@state() private _backingUp = false;
|
||||||
|
|
||||||
|
@state() private _backups: BackupContent[] = [];
|
||||||
|
|
||||||
|
@state() private _selected: string[] = [];
|
||||||
|
|
||||||
|
@query("hass-tabs-subpage-data-table", true)
|
||||||
|
private _dataTable!: HaTabsSubpageDataTable;
|
||||||
|
|
||||||
|
private _columns = memoizeOne(
|
||||||
|
(localize: LocalizeFunc): DataTableColumnContainer<BackupContent> => ({
|
||||||
|
name: {
|
||||||
|
title: localize("ui.panel.config.backup.name"),
|
||||||
|
main: true,
|
||||||
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
|
flex: 2,
|
||||||
|
template: (backup) => backup.name,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
title: localize("ui.panel.config.backup.size"),
|
||||||
|
filterable: true,
|
||||||
|
sortable: true,
|
||||||
|
template: (backup) => Math.ceil(backup.size * 10) / 10 + " MB",
|
||||||
|
},
|
||||||
|
date: {
|
||||||
|
title: localize("ui.panel.config.backup.created"),
|
||||||
|
direction: "desc",
|
||||||
|
filterable: true,
|
||||||
|
sortable: true,
|
||||||
|
template: (backup) =>
|
||||||
|
relativeTime(new Date(backup.date), this.hass.locale),
|
||||||
|
},
|
||||||
|
locations: {
|
||||||
|
title: "Locations",
|
||||||
|
template: (backup) =>
|
||||||
|
html`${(backup.agent_ids || []).map((agent) => {
|
||||||
|
const [domain, name] = agent.split(".");
|
||||||
|
return html`
|
||||||
|
<img
|
||||||
|
title=${name}
|
||||||
|
.src=${brandsUrl({
|
||||||
|
domain,
|
||||||
|
type: "icon",
|
||||||
|
useFallback: true,
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
})}
|
||||||
|
height="24"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
alt=${name}
|
||||||
|
slot="graphic"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
})}`,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
title: "",
|
||||||
|
label: localize("ui.panel.config.generic.headers.actions"),
|
||||||
|
showNarrow: true,
|
||||||
|
moveable: false,
|
||||||
|
hideable: false,
|
||||||
|
type: "overflow-menu",
|
||||||
|
template: (backup) => html`
|
||||||
|
<ha-icon-overflow-menu
|
||||||
|
.hass=${this.hass}
|
||||||
|
narrow
|
||||||
|
.items=${[
|
||||||
|
{
|
||||||
|
label: this.hass.localize("ui.common.download"),
|
||||||
|
path: mdiDownload,
|
||||||
|
action: () => this._downloadBackup(backup),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.hass.localize("ui.common.delete"),
|
||||||
|
path: mdiDelete,
|
||||||
|
action: () => this._deleteBackup(backup),
|
||||||
|
warning: true,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
</ha-icon-overflow-menu>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
private _handleSelectionChanged(
|
||||||
|
ev: HASSDomEvent<SelectionChangedEvent>
|
||||||
|
): void {
|
||||||
|
this._selected = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<hass-tabs-subpage-data-table
|
||||||
|
hasFab
|
||||||
|
.tabs=${[
|
||||||
|
{
|
||||||
|
translationKey: "ui.panel.config.backup.caption",
|
||||||
|
path: `/config/backup/list`,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
.hass=${this.hass}
|
||||||
|
.narrow=${this.narrow}
|
||||||
|
back-path="/config/system"
|
||||||
|
clickable
|
||||||
|
id="backup_id"
|
||||||
|
selectable
|
||||||
|
.selected=${this._selected.length}
|
||||||
|
@selection-changed=${this._handleSelectionChanged}
|
||||||
|
.route=${this.route}
|
||||||
|
@row-click=${this._showBackupDetails}
|
||||||
|
.columns=${this._columns(this.hass.localize)}
|
||||||
|
.data=${this._backups ?? []}
|
||||||
|
.noDataText=${this.hass.localize("ui.panel.config.backup.no_backups")}
|
||||||
|
.searchLabel=${this.hass.localize(
|
||||||
|
"ui.panel.config.backup.picker.search"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div slot="top_header" class="header">
|
||||||
|
<ha-backup-summary-card
|
||||||
|
title="Automatically backed up"
|
||||||
|
description="Your configuration has been backed up."
|
||||||
|
has-action
|
||||||
|
.status=${this._backingUp ? "loading" : "success"}
|
||||||
|
>
|
||||||
|
<ha-button slot="action" @click=${this._configureAutomaticBackup}>
|
||||||
|
Configure
|
||||||
|
</ha-button>
|
||||||
|
</ha-backup-summary-card>
|
||||||
|
<ha-backup-summary-card
|
||||||
|
title="3 automatic backup locations"
|
||||||
|
description="One is off-site"
|
||||||
|
has-action
|
||||||
|
.status=${"success"}
|
||||||
|
>
|
||||||
|
<ha-button slot="action" @click=${this._configureBackupLocations}>
|
||||||
|
Configure
|
||||||
|
</ha-button>
|
||||||
|
</ha-backup-summary-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this._selected.length
|
||||||
|
? html`<div
|
||||||
|
class=${classMap({
|
||||||
|
"header-toolbar": this.narrow,
|
||||||
|
"table-header": !this.narrow,
|
||||||
|
})}
|
||||||
|
slot="header"
|
||||||
|
>
|
||||||
|
<p class="selected-txt">
|
||||||
|
${this._selected.length} backups selected
|
||||||
|
</p>
|
||||||
|
<div class="header-btns">
|
||||||
|
${!this.narrow
|
||||||
|
? html`
|
||||||
|
<ha-button @click=${this._deleteSelected} class="warning">
|
||||||
|
Delete selected
|
||||||
|
</ha-button>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<ha-icon-button
|
||||||
|
.label=${"Delete selected"}
|
||||||
|
.path=${mdiDelete}
|
||||||
|
id="delete-btn"
|
||||||
|
class="warning"
|
||||||
|
@click=${this._deleteSelected}
|
||||||
|
></ha-icon-button>
|
||||||
|
<simple-tooltip animation-delay="0" for="delete-btn">
|
||||||
|
Delete selected
|
||||||
|
</simple-tooltip>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
</div> `
|
||||||
|
: nothing}
|
||||||
|
|
||||||
|
<ha-fab
|
||||||
|
slot="fab"
|
||||||
|
?disabled=${this._backingUp}
|
||||||
|
.label=${this.hass.localize("ui.panel.config.backup.create_backup")}
|
||||||
|
extended
|
||||||
|
@click=${this._newBackup}
|
||||||
|
>
|
||||||
|
<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._fetchBackupInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _fetchBackupInfo() {
|
||||||
|
const info = await fetchBackupInfo(this.hass);
|
||||||
|
this._backups = info.backups;
|
||||||
|
this._backingUp = info.backing_up;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _newBackup(): Promise<void> {
|
||||||
|
const type = await showNewBackupDialog(this, {});
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "manual") {
|
||||||
|
await this._generateBackup();
|
||||||
|
} else {
|
||||||
|
// Todo: implement trigger automatic backup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _generateBackup(): Promise<void> {
|
||||||
|
const response = await showGenerateBackupDialog(this, {});
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this._fetchBackupInfo();
|
||||||
|
|
||||||
|
// Todo subscribe for status updates instead of polling
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
await this._fetchBackupInfo();
|
||||||
|
if (!this._backingUp) {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _showBackupDetails(ev: CustomEvent): void {
|
||||||
|
const id = (ev.detail as RowClickedEvent).id;
|
||||||
|
navigate(`/config/backup/details/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _downloadBackup(backup: BackupContent): Promise<void> {
|
||||||
|
const preferedAgent = getPreferredAgentForDownload(backup!.agent_ids!);
|
||||||
|
const signedUrl = await getSignedPath(
|
||||||
|
this.hass,
|
||||||
|
getBackupDownloadUrl(backup.backup_id, preferedAgent)
|
||||||
|
);
|
||||||
|
fileDownload(signedUrl.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _deleteBackup(backup: BackupContent): Promise<void> {
|
||||||
|
const confirm = await showConfirmationDialog(this, {
|
||||||
|
title: "Delete backup",
|
||||||
|
text: "This backup will be permanently deleted.",
|
||||||
|
confirmText: this.hass.localize("ui.common.delete"),
|
||||||
|
destructive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await removeBackup(this.hass, backup.backup_id);
|
||||||
|
this._fetchBackupInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _deleteSelected() {
|
||||||
|
const confirm = await showConfirmationDialog(this, {
|
||||||
|
title: "Delete selected backups",
|
||||||
|
text: "These backups will be permanently deleted.",
|
||||||
|
confirmText: this.hass.localize("ui.common.delete"),
|
||||||
|
destructive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(
|
||||||
|
this._selected.map((slug) => removeBackup(this.hass, slug))
|
||||||
|
);
|
||||||
|
} catch (err: any) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: "Failed to delete backups",
|
||||||
|
text: extractApiErrorMessage(err),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this._fetchBackupInfo();
|
||||||
|
this._dataTable.clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _configureAutomaticBackup() {
|
||||||
|
navigate("/config/backup/automatic-config");
|
||||||
|
}
|
||||||
|
|
||||||
|
private _configureBackupLocations() {
|
||||||
|
navigate("/config/backup/locations");
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
css`
|
||||||
|
.header {
|
||||||
|
padding: 16px 16px 0 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 16px;
|
||||||
|
background-color: var(--primary-background-color);
|
||||||
|
}
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.header {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.header > * {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-fab[disabled] {
|
||||||
|
--mdc-theme-secondary: var(--disabled-text-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: var(--header-height);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
padding-inline-start: 16px;
|
||||||
|
padding-inline-end: initial;
|
||||||
|
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;
|
||||||
|
margin-inline-end: -12px;
|
||||||
|
margin-inline-start: initial;
|
||||||
|
}
|
||||||
|
.header-btns > ha-button,
|
||||||
|
.header-btns > ha-icon-button {
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-config-backup-dashboard": HaConfigBackupDashboard;
|
||||||
|
}
|
||||||
|
}
|
||||||
252
src/panels/config/backup/ha-config-backup-details.ts
Normal file
252
src/panels/config/backup/ha-config-backup-details.ts
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
import type { ActionDetail } from "@material/mwc-list";
|
||||||
|
import { mdiDelete, mdiDotsVertical, mdiDownload } from "@mdi/js";
|
||||||
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { formatDateTime } from "../../../common/datetime/format_date_time";
|
||||||
|
import { navigate } from "../../../common/navigate";
|
||||||
|
import "../../../components/ha-alert";
|
||||||
|
import "../../../components/ha-button-menu";
|
||||||
|
import "../../../components/ha-card";
|
||||||
|
import "../../../components/ha-circular-progress";
|
||||||
|
import "../../../components/ha-icon-button";
|
||||||
|
import "../../../components/ha-list-item";
|
||||||
|
import "../../../components/ha-md-list";
|
||||||
|
import "../../../components/ha-md-list-item";
|
||||||
|
import { getSignedPath } from "../../../data/auth";
|
||||||
|
import type { BackupContent } from "../../../data/backup";
|
||||||
|
import {
|
||||||
|
fetchBackupDetails,
|
||||||
|
getBackupDownloadUrl,
|
||||||
|
getPreferredAgentForDownload,
|
||||||
|
removeBackup,
|
||||||
|
} from "../../../data/backup";
|
||||||
|
import { domainToName } from "../../../data/integration";
|
||||||
|
import "../../../layouts/hass-subpage";
|
||||||
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import { brandsUrl } from "../../../util/brands-url";
|
||||||
|
import { fileDownload } from "../../../util/file_download";
|
||||||
|
import { showConfirmationDialog } from "../../lovelace/custom-card-helpers";
|
||||||
|
|
||||||
|
@customElement("ha-config-backup-details")
|
||||||
|
class HaConfigBackupDetails extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@property({ attribute: "backup-id" }) public backupId!: string;
|
||||||
|
|
||||||
|
@state() private _backup?: BackupContent | null;
|
||||||
|
|
||||||
|
@state() private _error?: string;
|
||||||
|
|
||||||
|
protected firstUpdated(changedProps) {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
|
||||||
|
if (this.backupId) {
|
||||||
|
this._fetchBackup();
|
||||||
|
} else {
|
||||||
|
this._error = "Backup id not defined";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this.hass) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<hass-subpage
|
||||||
|
back-path="/config/backup"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.narrow=${this.narrow}
|
||||||
|
.header=${this._backup?.name || "Backup"}
|
||||||
|
>
|
||||||
|
<ha-button-menu slot="toolbar-icon" @action=${this._handleAction}>
|
||||||
|
<ha-icon-button
|
||||||
|
slot="trigger"
|
||||||
|
.label=${this.hass.localize("ui.common.menu")}
|
||||||
|
.path=${mdiDotsVertical}
|
||||||
|
></ha-icon-button>
|
||||||
|
<ha-list-item graphic="icon">
|
||||||
|
<ha-svg-icon slot="graphic" .path=${mdiDownload}></ha-svg-icon>
|
||||||
|
${this.hass.localize("ui.common.download")}
|
||||||
|
</ha-list-item>
|
||||||
|
<ha-list-item graphic="icon" class="warning">
|
||||||
|
<ha-svg-icon slot="graphic" .path=${mdiDelete}></ha-svg-icon>
|
||||||
|
${this.hass.localize("ui.common.delete")}
|
||||||
|
</ha-list-item>
|
||||||
|
</ha-button-menu>
|
||||||
|
<div class="content">
|
||||||
|
${this._error &&
|
||||||
|
html`<ha-alert alert-type="error">${this._error}</ha-alert>`}
|
||||||
|
${this._backup === null
|
||||||
|
? html`<ha-alert alert-type="warning" title="Not found">
|
||||||
|
Backup matching ${this.backupId} not found
|
||||||
|
</ha-alert>`
|
||||||
|
: !this._backup
|
||||||
|
? html`<ha-circular-progress active></ha-circular-progress>`
|
||||||
|
: html`
|
||||||
|
<ha-card header="Backup">
|
||||||
|
<div class="card-content">
|
||||||
|
<ha-md-list>
|
||||||
|
<ha-md-list-item>
|
||||||
|
<span slot="headline">
|
||||||
|
${Math.ceil(this._backup.size * 10) / 10 + " MB"}
|
||||||
|
</span>
|
||||||
|
<span slot="supporting-text">Size</span>
|
||||||
|
</ha-md-list-item>
|
||||||
|
<ha-md-list-item>
|
||||||
|
${formatDateTime(
|
||||||
|
new Date(this._backup.date),
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config
|
||||||
|
)}
|
||||||
|
<span slot="supporting-text">Created</span>
|
||||||
|
</ha-md-list-item>
|
||||||
|
</ha-md-list>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
<ha-card header="Locations">
|
||||||
|
<div class="card-content">
|
||||||
|
<ha-md-list>
|
||||||
|
${this._backup.agent_ids?.map((agent) => {
|
||||||
|
const [domain, name] = agent.split(".");
|
||||||
|
const domainName = domainToName(
|
||||||
|
this.hass.localize,
|
||||||
|
domain
|
||||||
|
);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-md-list-item>
|
||||||
|
<img
|
||||||
|
.src=${brandsUrl({
|
||||||
|
domain,
|
||||||
|
type: "icon",
|
||||||
|
useFallback: true,
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
})}
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
alt=""
|
||||||
|
slot="start"
|
||||||
|
/>
|
||||||
|
<div slot="headline">${domainName}: ${name}</div>
|
||||||
|
<ha-button-menu
|
||||||
|
slot="end"
|
||||||
|
@action=${this._handleAgentAction}
|
||||||
|
.agent=${agent}
|
||||||
|
fixed
|
||||||
|
>
|
||||||
|
<ha-icon-button
|
||||||
|
slot="trigger"
|
||||||
|
.label=${this.hass.localize("ui.common.menu")}
|
||||||
|
.path=${mdiDotsVertical}
|
||||||
|
></ha-icon-button>
|
||||||
|
<ha-list-item graphic="icon">
|
||||||
|
<ha-svg-icon
|
||||||
|
slot="graphic"
|
||||||
|
.path=${mdiDownload}
|
||||||
|
></ha-svg-icon>
|
||||||
|
Download from this location
|
||||||
|
</ha-list-item>
|
||||||
|
</ha-button-menu>
|
||||||
|
</ha-md-list-item>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</ha-md-list>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
</hass-subpage>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _fetchBackup() {
|
||||||
|
try {
|
||||||
|
const response = await fetchBackupDetails(this.hass, this.backupId);
|
||||||
|
this._backup = response.backup;
|
||||||
|
} catch (err: any) {
|
||||||
|
this._error = err?.message || "Could not fetch backup details";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||||
|
switch (ev.detail.index) {
|
||||||
|
case 0:
|
||||||
|
this._downloadBackup();
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
this._deleteBackup();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleAgentAction(ev: CustomEvent<ActionDetail>) {
|
||||||
|
const button = ev.currentTarget;
|
||||||
|
const agentId = (button as any).agent;
|
||||||
|
this._downloadBackup(agentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _downloadBackup(agentId?: string): Promise<void> {
|
||||||
|
const preferedAgent =
|
||||||
|
agentId ?? getPreferredAgentForDownload(this._backup!.agent_ids!);
|
||||||
|
const signedUrl = await getSignedPath(
|
||||||
|
this.hass,
|
||||||
|
getBackupDownloadUrl(this._backup!.backup_id, preferedAgent)
|
||||||
|
);
|
||||||
|
fileDownload(signedUrl.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _deleteBackup(): Promise<void> {
|
||||||
|
const confirm = await showConfirmationDialog(this, {
|
||||||
|
title: "Delete backup",
|
||||||
|
text: "This backup will be permanently deleted.",
|
||||||
|
confirmText: this.hass.localize("ui.common.delete"),
|
||||||
|
destructive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await removeBackup(this.hass, this._backup!.backup_id);
|
||||||
|
navigate("/config/backup");
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
.content {
|
||||||
|
padding: 28px 20px 0;
|
||||||
|
max-width: 690px;
|
||||||
|
margin: 0 auto;
|
||||||
|
gap: 24px;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
.card-content {
|
||||||
|
padding: 0 20px 8px 20px;
|
||||||
|
}
|
||||||
|
ha-md-list {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
ha-md-list-item {
|
||||||
|
--md-list-item-leading-space: 0;
|
||||||
|
--md-list-item-trailing-space: 0;
|
||||||
|
}
|
||||||
|
ha-md-list-item img {
|
||||||
|
width: 48px;
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
.warning ha-svg-icon {
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-config-backup-details": HaConfigBackupDetails;
|
||||||
|
}
|
||||||
|
}
|
||||||
138
src/panels/config/backup/ha-config-backup-locations.ts
Normal file
138
src/panels/config/backup/ha-config-backup-locations.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import type { TemplateResult } from "lit";
|
||||||
|
import { css, html, LitElement } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import "../../../components/ha-card";
|
||||||
|
import "../../../components/ha-icon-next";
|
||||||
|
import "../../../components/ha-md-list";
|
||||||
|
import "../../../components/ha-md-list-item";
|
||||||
|
import type { BackupAgent } from "../../../data/backup";
|
||||||
|
import { fetchBackupAgentsInfo } from "../../../data/backup";
|
||||||
|
import "../../../layouts/hass-subpage";
|
||||||
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import { brandsUrl } from "../../../util/brands-url";
|
||||||
|
import { domainToName } from "../../../data/integration";
|
||||||
|
|
||||||
|
@customElement("ha-config-backup-locations")
|
||||||
|
class HaConfigBackupLocations extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@state() private _agents: BackupAgent[] = [];
|
||||||
|
|
||||||
|
protected firstUpdated(changedProps) {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
this._fetchAgents();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<hass-subpage
|
||||||
|
back-path="/config/backup"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.narrow=${this.narrow}
|
||||||
|
.header=${this.hass.localize("ui.panel.config.backup.caption")}
|
||||||
|
>
|
||||||
|
<div class="content">
|
||||||
|
<div class="header">
|
||||||
|
<h2 class="title">Locations</h2>
|
||||||
|
<p class="description">
|
||||||
|
To keep your data safe it is recommended your backups is at least
|
||||||
|
on two different locations and one of them is off-site.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ha-card class="agents">
|
||||||
|
<div class="card-content">
|
||||||
|
${this._agents.length > 0
|
||||||
|
? html`
|
||||||
|
<ha-md-list>
|
||||||
|
${this._agents.map((agent) => {
|
||||||
|
const [domain, name] = agent.agent_id.split(".");
|
||||||
|
const domainName = domainToName(
|
||||||
|
this.hass.localize,
|
||||||
|
domain
|
||||||
|
);
|
||||||
|
return html`
|
||||||
|
<ha-md-list-item
|
||||||
|
type="link"
|
||||||
|
href="/config/backup/locations/${agent.agent_id}"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
.src=${brandsUrl({
|
||||||
|
domain,
|
||||||
|
type: "icon",
|
||||||
|
useFallback: true,
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
})}
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
alt=""
|
||||||
|
slot="start"
|
||||||
|
/>
|
||||||
|
<div slot="headline">${domainName}: ${name}</div>
|
||||||
|
<ha-icon-next slot="end"></ha-icon-next>
|
||||||
|
</ha-md-list-item>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</ha-md-list>
|
||||||
|
`
|
||||||
|
: html`<p>No sync agents configured</p>`}
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
</div>
|
||||||
|
</hass-subpage>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _fetchAgents() {
|
||||||
|
const data = await fetchBackupAgentsInfo(this.hass);
|
||||||
|
this._agents = data.agents;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
.content {
|
||||||
|
padding: 28px 20px 0;
|
||||||
|
max-width: 690px;
|
||||||
|
margin: 0 auto;
|
||||||
|
gap: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .title {
|
||||||
|
font-size: 22px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 28px;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .description {
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px;
|
||||||
|
letter-spacing: 0.25px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-md-list {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
ha-md-list-item img {
|
||||||
|
width: 48px;
|
||||||
|
}
|
||||||
|
.card-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-config-backup-locations": HaConfigBackupLocations;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,235 +1,49 @@
|
|||||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
import type { PropertyValues } from "lit";
|
||||||
import { mdiDelete, mdiDownload, mdiPlus } from "@mdi/js";
|
import { customElement, property } from "lit/decorators";
|
||||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
import type { RouterOptions } from "../../../layouts/hass-router-page";
|
||||||
import { LitElement, css, html } from "lit";
|
import { HassRouterPage } from "../../../layouts/hass-router-page";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import memoize from "memoize-one";
|
|
||||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
|
||||||
import type { 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 type { BackupContent, BackupData } from "../../../data/backup";
|
|
||||||
import {
|
|
||||||
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 "../../../layouts/hass-tabs-subpage-data-table";
|
||||||
import type { HomeAssistant, Route } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
import "./ha-config-backup-dashboard";
|
||||||
import { fileDownload } from "../../../util/file_download";
|
|
||||||
|
|
||||||
@customElement("ha-config-backup")
|
@customElement("ha-config-backup")
|
||||||
class HaConfigBackup extends LitElement {
|
class HaConfigBackup extends HassRouterPage {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ type: Boolean }) public isWide = false;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public narrow = false;
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
@property({ attribute: false }) public route!: Route;
|
protected routerOptions: RouterOptions = {
|
||||||
|
defaultPage: "dashboard",
|
||||||
@state() private _backupData?: BackupData;
|
routes: {
|
||||||
|
dashboard: {
|
||||||
private _columns = memoize(
|
tag: "ha-config-backup-dashboard",
|
||||||
(
|
cache: true,
|
||||||
narrow,
|
|
||||||
_language,
|
|
||||||
localize: LocalizeFunc
|
|
||||||
): DataTableColumnContainer<BackupContent> => ({
|
|
||||||
name: {
|
|
||||||
title: localize("ui.panel.config.backup.name"),
|
|
||||||
main: true,
|
|
||||||
sortable: true,
|
|
||||||
filterable: true,
|
|
||||||
flex: 2,
|
|
||||||
template: narrow
|
|
||||||
? undefined
|
|
||||||
: (backup) =>
|
|
||||||
html`${backup.name}
|
|
||||||
<div class="secondary">${backup.path}</div>`,
|
|
||||||
},
|
},
|
||||||
path: {
|
details: {
|
||||||
title: localize("ui.panel.config.backup.path"),
|
tag: "ha-config-backup-details",
|
||||||
hidden: !narrow,
|
load: () => import("./ha-config-backup-details"),
|
||||||
},
|
},
|
||||||
size: {
|
locations: {
|
||||||
title: localize("ui.panel.config.backup.size"),
|
tag: "ha-config-backup-locations",
|
||||||
filterable: true,
|
load: () => import("./ha-config-backup-locations"),
|
||||||
sortable: true,
|
|
||||||
template: (backup) => Math.ceil(backup.size * 10) / 10 + " MB",
|
|
||||||
},
|
},
|
||||||
date: {
|
"automatic-config": {
|
||||||
title: localize("ui.panel.config.backup.created"),
|
tag: "ha-config-backup-automatic-config",
|
||||||
direction: "desc",
|
load: () => import("./ha-config-backup-automatic-config"),
|
||||||
filterable: true,
|
|
||||||
sortable: true,
|
|
||||||
template: (backup) =>
|
|
||||||
relativeTime(new Date(backup.date), this.hass.locale),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
|
||||||
title: "",
|
|
||||||
type: "overflow-menu",
|
|
||||||
showNarrow: true,
|
|
||||||
hideable: false,
|
|
||||||
moveable: false,
|
|
||||||
template: (backup) =>
|
|
||||||
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[]) =>
|
protected updatePageEl(pageEl, changedProps: PropertyValues) {
|
||||||
backupItems.map((backup) => ({
|
pageEl.hass = this.hass;
|
||||||
name: backup.name,
|
pageEl.route = this.routeTail;
|
||||||
slug: backup.slug,
|
|
||||||
date: backup.date,
|
|
||||||
size: backup.size,
|
|
||||||
path: backup.path,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
if (
|
||||||
if (!this.hass || this._backupData === undefined) {
|
(!changedProps || changedProps.has("route")) &&
|
||||||
return html`<hass-loading-screen></hass-loading-screen>`;
|
this._currentPage === "details"
|
||||||
|
) {
|
||||||
|
pageEl.backupId = this.routeTail.path.substr(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
|
||||||
<hass-tabs-subpage-data-table
|
|
||||||
hasFab
|
|
||||||
.tabs=${[
|
|
||||||
{
|
|
||||||
translationKey: "ui.panel.config.backup.caption",
|
|
||||||
path: `/config/backup`,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.narrow=${this.narrow}
|
|
||||||
back-path="/config/system"
|
|
||||||
.route=${this.route}
|
|
||||||
.columns=${this._columns(
|
|
||||||
this.narrow,
|
|
||||||
this.hass.language,
|
|
||||||
this.hass.localize
|
|
||||||
)}
|
|
||||||
.data=${this._getItems(this._backupData.backups)}
|
|
||||||
.noDataText=${this.hass.localize("ui.panel.config.backup.no_backups")}
|
|
||||||
.searchLabel=${this.hass.localize(
|
|
||||||
"ui.panel.config.backup.picker.search"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<ha-fab
|
|
||||||
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"
|
|
||||||
indeterminate
|
|
||||||
></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;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user