mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-11-04 00:19:47 +00:00 
			
		
		
		
	Compare commits
	
		
			15 Commits
		
	
	
		
			20251001.3
			...
			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 =
 | 
			
		||||
    "Loading";
 | 
			
		||||
 | 
			
		||||
  @property() public size: "tiny" | "small" | "medium" | "large" = "medium";
 | 
			
		||||
  @property() public size?: "tiny" | "small" | "medium" | "large";
 | 
			
		||||
 | 
			
		||||
  protected updated(changedProps: PropertyValues) {
 | 
			
		||||
    super.updated(changedProps);
 | 
			
		||||
@@ -21,7 +21,6 @@ export class HaCircularProgress extends MdCircularProgress {
 | 
			
		||||
        case "small":
 | 
			
		||||
          this.style.setProperty("--md-circular-progress-size", "28px");
 | 
			
		||||
          break;
 | 
			
		||||
        // medium is default size
 | 
			
		||||
        case "medium":
 | 
			
		||||
          this.style.setProperty("--md-circular-progress-size", "48px");
 | 
			
		||||
          break;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,36 +1,98 @@
 | 
			
		||||
import type { HomeAssistant } from "../types";
 | 
			
		||||
 | 
			
		||||
export interface BackupAgent {
 | 
			
		||||
  agent_id: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface BackupContent {
 | 
			
		||||
  slug: string;
 | 
			
		||||
  backup_id: string;
 | 
			
		||||
  date: string;
 | 
			
		||||
  name: string;
 | 
			
		||||
  protected: boolean;
 | 
			
		||||
  size: number;
 | 
			
		||||
  path: string;
 | 
			
		||||
  agent_ids?: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface BackupData {
 | 
			
		||||
  backing_up: boolean;
 | 
			
		||||
export interface BackupInfo {
 | 
			
		||||
  backups: BackupContent[];
 | 
			
		||||
  backing_up: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getBackupDownloadUrl = (slug: string) =>
 | 
			
		||||
  `/api/backup/download/${slug}`;
 | 
			
		||||
export interface BackupDetails {
 | 
			
		||||
  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({
 | 
			
		||||
    type: "backup/info",
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
export const removeBackup = (
 | 
			
		||||
export const fetchBackupDetails = (
 | 
			
		||||
  hass: HomeAssistant,
 | 
			
		||||
  slug: string
 | 
			
		||||
): Promise<void> =>
 | 
			
		||||
  id: string
 | 
			
		||||
): Promise<BackupDetails> =>
 | 
			
		||||
  hass.callWS({
 | 
			
		||||
    type: "backup/remove",
 | 
			
		||||
    slug,
 | 
			
		||||
    type: "backup/details",
 | 
			
		||||
    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({
 | 
			
		||||
    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
 | 
			
		||||
                  ? html`
 | 
			
		||||
                      <div slot="header">
 | 
			
		||||
                        <slot name="top_header"></slot>
 | 
			
		||||
                        <slot name="header">
 | 
			
		||||
                          <div class="table-header">
 | 
			
		||||
                            ${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 { mdiDelete, mdiDownload, mdiPlus } from "@mdi/js";
 | 
			
		||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
 | 
			
		||||
import { LitElement, css, html } from "lit";
 | 
			
		||||
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 type { PropertyValues } from "lit";
 | 
			
		||||
import { customElement, property } from "lit/decorators";
 | 
			
		||||
import type { RouterOptions } from "../../../layouts/hass-router-page";
 | 
			
		||||
import { HassRouterPage } from "../../../layouts/hass-router-page";
 | 
			
		||||
import "../../../layouts/hass-tabs-subpage-data-table";
 | 
			
		||||
import type { HomeAssistant, Route } from "../../../types";
 | 
			
		||||
import type { LocalizeFunc } from "../../../common/translations/localize";
 | 
			
		||||
import { fileDownload } from "../../../util/file_download";
 | 
			
		||||
import type { HomeAssistant } from "../../../types";
 | 
			
		||||
import "./ha-config-backup-dashboard";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-config-backup")
 | 
			
		||||
class HaConfigBackup extends LitElement {
 | 
			
		||||
class HaConfigBackup extends HassRouterPage {
 | 
			
		||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean }) public isWide = false;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean }) public narrow = false;
 | 
			
		||||
 | 
			
		||||
  @property({ attribute: false }) public route!: Route;
 | 
			
		||||
 | 
			
		||||
  @state() private _backupData?: BackupData;
 | 
			
		||||
 | 
			
		||||
  private _columns = memoize(
 | 
			
		||||
    (
 | 
			
		||||
      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>`,
 | 
			
		||||
  protected routerOptions: RouterOptions = {
 | 
			
		||||
    defaultPage: "dashboard",
 | 
			
		||||
    routes: {
 | 
			
		||||
      dashboard: {
 | 
			
		||||
        tag: "ha-config-backup-dashboard",
 | 
			
		||||
        cache: true,
 | 
			
		||||
      },
 | 
			
		||||
      path: {
 | 
			
		||||
        title: localize("ui.panel.config.backup.path"),
 | 
			
		||||
        hidden: !narrow,
 | 
			
		||||
      details: {
 | 
			
		||||
        tag: "ha-config-backup-details",
 | 
			
		||||
        load: () => import("./ha-config-backup-details"),
 | 
			
		||||
      },
 | 
			
		||||
      size: {
 | 
			
		||||
        title: localize("ui.panel.config.backup.size"),
 | 
			
		||||
        filterable: true,
 | 
			
		||||
        sortable: true,
 | 
			
		||||
        template: (backup) => Math.ceil(backup.size * 10) / 10 + " MB",
 | 
			
		||||
      locations: {
 | 
			
		||||
        tag: "ha-config-backup-locations",
 | 
			
		||||
        load: () => import("./ha-config-backup-locations"),
 | 
			
		||||
      },
 | 
			
		||||
      date: {
 | 
			
		||||
        title: localize("ui.panel.config.backup.created"),
 | 
			
		||||
        direction: "desc",
 | 
			
		||||
        filterable: true,
 | 
			
		||||
        sortable: true,
 | 
			
		||||
        template: (backup) =>
 | 
			
		||||
          relativeTime(new Date(backup.date), this.hass.locale),
 | 
			
		||||
      "automatic-config": {
 | 
			
		||||
        tag: "ha-config-backup-automatic-config",
 | 
			
		||||
        load: () => import("./ha-config-backup-automatic-config"),
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
      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>`,
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  );
 | 
			
		||||
  protected updatePageEl(pageEl, changedProps: PropertyValues) {
 | 
			
		||||
    pageEl.hass = this.hass;
 | 
			
		||||
    pageEl.route = this.routeTail;
 | 
			
		||||
 | 
			
		||||
  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>`;
 | 
			
		||||
    if (
 | 
			
		||||
      (!changedProps || changedProps.has("route")) &&
 | 
			
		||||
      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