mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-02 08:33:31 +01:00
Report progress for backup upload (#29748)
* Add progress bar for backup uploads * add sort * visual fixes * fix sorting * react to event * cleanup * remove log * remove fom union type * different progress bar * styling fixes * guard against empty space in other backup states * cleanup * xleanup * add checkmark on completion * remove progress bar * remove spinner during upload * remove spinner, animate * add subtext * prettier * review comments * linesbreaks
This commit is contained in:
@@ -65,6 +65,13 @@ interface RestoreBackupEvent {
|
||||
state: RestoreBackupState;
|
||||
}
|
||||
|
||||
export interface UploadBackupEvent {
|
||||
manager_state: BackupManagerState;
|
||||
agent_id: string;
|
||||
uploaded_bytes: number;
|
||||
total_bytes: number;
|
||||
}
|
||||
|
||||
export type ManagerState =
|
||||
| "idle"
|
||||
| "create_backup"
|
||||
@@ -77,12 +84,14 @@ export type ManagerStateEvent =
|
||||
| ReceiveBackupEvent
|
||||
| RestoreBackupEvent;
|
||||
|
||||
export type BackupSubscriptionEvent = ManagerStateEvent | UploadBackupEvent;
|
||||
|
||||
export const subscribeBackupEvents = (
|
||||
hass: HomeAssistant,
|
||||
callback: (event: ManagerStateEvent) => void,
|
||||
callback: (event: BackupSubscriptionEvent) => void,
|
||||
preCheck?: () => boolean | Promise<boolean>
|
||||
) =>
|
||||
hass.connection.subscribeMessage<ManagerStateEvent>(
|
||||
hass.connection.subscribeMessage<BackupSubscriptionEvent>(
|
||||
callback,
|
||||
{
|
||||
type: "backup/subscribe_events",
|
||||
|
||||
@@ -12,9 +12,15 @@ import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-icon";
|
||||
import "../../../../components/ha-spinner";
|
||||
|
||||
type SummaryStatus = "success" | "error" | "info" | "warning" | "loading";
|
||||
type SummaryStatus =
|
||||
| "success"
|
||||
| "error"
|
||||
| "info"
|
||||
| "warning"
|
||||
| "loading"
|
||||
| "none";
|
||||
|
||||
const ICONS: Record<SummaryStatus, string> = {
|
||||
const ICONS: Partial<Record<SummaryStatus, string>> = {
|
||||
success: mdiCheck,
|
||||
error: mdiAlertCircleOutline,
|
||||
warning: mdiAlertOutline,
|
||||
@@ -42,11 +48,13 @@ class HaBackupSummaryCard extends LitElement {
|
||||
<div class="summary">
|
||||
${this.status === "loading"
|
||||
? html`<ha-spinner></ha-spinner>`
|
||||
: html`
|
||||
<div class="icon ${this.status}">
|
||||
<ha-svg-icon .path=${ICONS[this.status]}></ha-svg-icon>
|
||||
</div>
|
||||
`}
|
||||
: this.status === "none"
|
||||
? nothing
|
||||
: html`
|
||||
<div class="icon ${this.status}">
|
||||
<ha-svg-icon .path=${ICONS[this.status]}></ha-svg-icon>
|
||||
</div>
|
||||
`}
|
||||
|
||||
<div class="content">
|
||||
<p class="heading">${this.heading}</p>
|
||||
@@ -92,6 +100,7 @@ class HaBackupSummaryCard extends LitElement {
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
--icon-color: var(--primary-color);
|
||||
animation: pop-in var(--ha-animation-duration-normal, 250ms) ease-out;
|
||||
}
|
||||
.icon.success {
|
||||
--icon-color: var(--success-color);
|
||||
@@ -155,6 +164,16 @@ class HaBackupSummaryCard extends LitElement {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
@keyframes pop-in {
|
||||
from {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,93 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { mdiCheck, mdiHarddisk, mdiNas } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { ManagerStateEvent } from "../../../../../data/backup_manager";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
|
||||
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-spinner";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import type { BackupAgent } from "../../../../../data/backup";
|
||||
import {
|
||||
computeBackupAgentName,
|
||||
isLocalAgent,
|
||||
isNetworkMountAgent,
|
||||
} from "../../../../../data/backup";
|
||||
import type {
|
||||
CreateBackupStage,
|
||||
ManagerStateEvent,
|
||||
} from "../../../../../data/backup_manager";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { brandsUrl } from "../../../../../util/brands-url";
|
||||
import "../ha-backup-summary-card";
|
||||
|
||||
type SegmentState = "pending" | "active" | "completed";
|
||||
|
||||
interface ProgressSegment {
|
||||
label: string;
|
||||
state: SegmentState;
|
||||
flex: number;
|
||||
}
|
||||
|
||||
const HA_STAGES: CreateBackupStage[] = ["home_assistant"];
|
||||
|
||||
const ADDON_STAGES: CreateBackupStage[] = [
|
||||
"addons",
|
||||
"apps",
|
||||
"addon_repositories",
|
||||
"app_repositories",
|
||||
"docker_config",
|
||||
"await_addon_restarts",
|
||||
"await_app_restarts",
|
||||
];
|
||||
|
||||
const MEDIA_STAGES: CreateBackupStage[] = ["folders", "finishing_file"];
|
||||
|
||||
// Ordered groups matching actual backend execution order
|
||||
const STAGE_ORDER: CreateBackupStage[][] = [
|
||||
ADDON_STAGES,
|
||||
MEDIA_STAGES,
|
||||
HA_STAGES,
|
||||
["upload_to_agents"],
|
||||
];
|
||||
|
||||
@customElement("ha-backup-overview-progress")
|
||||
export class HaBackupOverviewProgress extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public manager!: ManagerStateEvent;
|
||||
|
||||
@property({ attribute: false }) public agents: BackupAgent[] = [];
|
||||
|
||||
@property({ attribute: false }) public uploadProgress: Record<
|
||||
string,
|
||||
{ uploaded_bytes: number; total_bytes: number }
|
||||
> = {};
|
||||
|
||||
private get _heading() {
|
||||
const state = this.manager.manager_state;
|
||||
if (state === "idle") {
|
||||
const managerState = this.manager.manager_state;
|
||||
if (managerState === "idle") {
|
||||
return "";
|
||||
}
|
||||
return this.hass.localize(
|
||||
`ui.panel.config.backup.overview.progress.heading.${state}`
|
||||
`ui.panel.config.backup.overview.progress.heading.${managerState}`
|
||||
);
|
||||
}
|
||||
|
||||
private get _isUploadStage(): boolean {
|
||||
if (this.manager.manager_state === "idle") {
|
||||
return false;
|
||||
}
|
||||
return this.manager.stage === "upload_to_agents";
|
||||
}
|
||||
|
||||
private get _description() {
|
||||
switch (this.manager.manager_state) {
|
||||
case "create_backup":
|
||||
if (!this.manager.stage) {
|
||||
return "";
|
||||
}
|
||||
return this.hass.localize(
|
||||
`ui.panel.config.backup.overview.progress.description.create_backup.${this.manager.stage}`
|
||||
);
|
||||
return "";
|
||||
|
||||
case "restore_backup":
|
||||
if (!this.manager.stage) {
|
||||
return "";
|
||||
@@ -44,22 +103,373 @@ export class HaBackupOverviewProgress extends LitElement {
|
||||
return this.hass.localize(
|
||||
`ui.panel.config.backup.overview.progress.description.receive_backup.${this.manager.stage}`
|
||||
);
|
||||
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private _computeAgentPercent(agentId: string): number | undefined {
|
||||
const progress = this.uploadProgress[agentId];
|
||||
if (!progress || progress.total_bytes === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return Math.round((progress.uploaded_bytes / progress.total_bytes) * 100);
|
||||
}
|
||||
|
||||
private _getStageGroupIndex(stage: CreateBackupStage): number {
|
||||
return STAGE_ORDER.findIndex((group) => group.includes(stage));
|
||||
}
|
||||
|
||||
private _getSegmentState(
|
||||
segmentGroupIndex: number,
|
||||
currentGroupIndex: number
|
||||
): SegmentState {
|
||||
if (currentGroupIndex > segmentGroupIndex) {
|
||||
return "completed";
|
||||
}
|
||||
if (currentGroupIndex === segmentGroupIndex) {
|
||||
return "active";
|
||||
}
|
||||
return "pending";
|
||||
}
|
||||
|
||||
private _computeCreateBackupSegments(): ProgressSegment[] {
|
||||
const stage =
|
||||
this.manager.manager_state === "create_backup"
|
||||
? this.manager.stage
|
||||
: null;
|
||||
|
||||
const currentGroupIndex = stage ? this._getStageGroupIndex(stage) : -1;
|
||||
const isHassio = isComponentLoaded(this.hass, "hassio");
|
||||
|
||||
if (isHassio) {
|
||||
// Split creation into 3 sub-segments + Upload
|
||||
return [
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.backup.overview.progress.segments.apps"
|
||||
),
|
||||
state: this._getSegmentState(0, currentGroupIndex),
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.backup.overview.progress.segments.media"
|
||||
),
|
||||
state: this._getSegmentState(1, currentGroupIndex),
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.backup.overview.progress.segments.home_assistant"
|
||||
),
|
||||
state: this._getSegmentState(2, currentGroupIndex),
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.backup.overview.progress.segments.upload"
|
||||
),
|
||||
state: this._getSegmentState(3, currentGroupIndex),
|
||||
flex: 3,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Non-HAOS: No app segment, just Media, HA and Upload
|
||||
return [
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.backup.overview.progress.segments.media"
|
||||
),
|
||||
state: this._getSegmentState(1, currentGroupIndex),
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.backup.overview.progress.segments.home_assistant"
|
||||
),
|
||||
state: this._getSegmentState(2, currentGroupIndex),
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.backup.overview.progress.segments.upload"
|
||||
),
|
||||
state: this._getSegmentState(3, currentGroupIndex),
|
||||
flex: 3,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private _renderAgentIcon(agentId: string) {
|
||||
if (isLocalAgent(agentId)) {
|
||||
return html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiHarddisk}
|
||||
></ha-svg-icon>`;
|
||||
}
|
||||
if (isNetworkMountAgent(agentId)) {
|
||||
return html`<ha-svg-icon slot="start" .path=${mdiNas}></ha-svg-icon>`;
|
||||
}
|
||||
const domain = computeDomain(agentId);
|
||||
return html`
|
||||
<img
|
||||
slot="start"
|
||||
.src=${brandsUrl({
|
||||
domain,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
alt=""
|
||||
/>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderSegmentedProgress() {
|
||||
const managerState = this.manager.manager_state;
|
||||
|
||||
let segments: ProgressSegment[];
|
||||
|
||||
if (managerState === "create_backup") {
|
||||
segments = this._computeCreateBackupSegments();
|
||||
} else {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="segmented-progress">
|
||||
${segments.map(
|
||||
(segment) => html`
|
||||
<div class="segment" style="flex: ${segment.flex}">
|
||||
<div
|
||||
class="segment-bar ${classMap({
|
||||
active: segment.state === "active",
|
||||
completed: segment.state === "completed",
|
||||
pending: segment.state === "pending",
|
||||
})}"
|
||||
></div>
|
||||
<span class="segment-label">${segment.label}</span>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderAgentProgress() {
|
||||
if (!this._isUploadStage || this.agents.length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const hasProgress = Object.keys(this.uploadProgress).length > 0;
|
||||
|
||||
if (!hasProgress) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="agent-list-wrapper">
|
||||
<ha-md-list class="agent-list">
|
||||
${this.agents.map((agent) => {
|
||||
const name = computeBackupAgentName(
|
||||
this.hass.localize,
|
||||
agent.agent_id,
|
||||
this.agents
|
||||
);
|
||||
const agentPercent = this._computeAgentPercent(agent.agent_id);
|
||||
|
||||
if (agentPercent !== undefined) {
|
||||
if (agentPercent >= 100) {
|
||||
return html`
|
||||
<ha-md-list-item>
|
||||
${this._renderAgentIcon(agent.agent_id)}
|
||||
<div slot="headline">${name}</div>
|
||||
<div slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.overview.progress.agent_status.uploaded"
|
||||
)}
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
class="agent-complete"
|
||||
.path=${mdiCheck}
|
||||
></ha-svg-icon>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<ha-md-list-item>
|
||||
${this._renderAgentIcon(agent.agent_id)}
|
||||
<div slot="headline">${name}</div>
|
||||
<div slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.overview.progress.agent_status.uploading"
|
||||
)}
|
||||
</div>
|
||||
<span slot="end" class="progress-percentage">
|
||||
${agentPercent}%
|
||||
</span>
|
||||
<ha-spinner slot="end" size="tiny"></ha-spinner>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-md-list-item>
|
||||
${this._renderAgentIcon(agent.agent_id)}
|
||||
<div slot="headline">${name}</div>
|
||||
<div slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.overview.progress.agent_status.uploading"
|
||||
)}
|
||||
</div>
|
||||
<ha-spinner slot="end" size="tiny"></ha-spinner>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
})}
|
||||
</ha-md-list>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const segmentedProgress = this._renderSegmentedProgress();
|
||||
const agentProgress = this._renderAgentProgress();
|
||||
const hasProgressContent =
|
||||
segmentedProgress !== nothing || agentProgress !== nothing;
|
||||
|
||||
return html`
|
||||
<ha-backup-summary-card
|
||||
.hass=${this.hass}
|
||||
.heading=${this._heading}
|
||||
.description=${this._description}
|
||||
status="loading"
|
||||
status="none"
|
||||
>
|
||||
${hasProgressContent
|
||||
? html`
|
||||
<div class="progress-content">
|
||||
${segmentedProgress} ${agentProgress}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</ha-backup-summary-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
.progress-content {
|
||||
padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4);
|
||||
}
|
||||
.segmented-progress {
|
||||
display: flex;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
.segment {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-1);
|
||||
min-width: 0;
|
||||
}
|
||||
.segment-bar {
|
||||
height: 8px;
|
||||
border-radius: var(--ha-border-radius-pill);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
.segment-bar.pending {
|
||||
background-color: var(--divider-color);
|
||||
}
|
||||
.segment-bar.active {
|
||||
background-color: var(--primary-color);
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
.segment-bar.completed {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
.segment-label {
|
||||
font-size: var(--ha-font-size-xs);
|
||||
color: var(--secondary-text-color);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.segment-bar.active {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
.agent-list-wrapper {
|
||||
display: grid;
|
||||
grid-template-rows: 1fr;
|
||||
animation: expand var(--ha-animation-duration-slow, 350ms) ease-out;
|
||||
}
|
||||
@keyframes expand {
|
||||
from {
|
||||
grid-template-rows: 0fr;
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
grid-template-rows: 1fr;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.agent-list {
|
||||
background: none;
|
||||
padding: 0;
|
||||
margin-top: var(--ha-space-4);
|
||||
overflow: hidden;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
}
|
||||
ha-md-list-item img {
|
||||
width: 48px;
|
||||
}
|
||||
ha-md-list-item ha-svg-icon[slot="start"] {
|
||||
--mdc-icon-size: 48px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.progress-percentage {
|
||||
font-size: var(--ha-font-size-s);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-md-list-item [slot="supporting-text"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.agent-complete {
|
||||
color: var(--success-color);
|
||||
--mdc-icon-size: 24px;
|
||||
animation: pop-in var(--ha-animation-duration-normal, 250ms) ease-out;
|
||||
}
|
||||
@keyframes pop-in {
|
||||
from {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -58,7 +58,7 @@ class HaBackupOverviewBackups extends LitElement {
|
||||
|
||||
private _renderSummaryCard(
|
||||
heading: string,
|
||||
status: "error" | "info" | "warning" | "loading" | "success",
|
||||
status: "error" | "info" | "warning" | "loading" | "success" | "none",
|
||||
headline: string | null,
|
||||
description?: string | null,
|
||||
lastCompletedDate?: Date
|
||||
@@ -103,7 +103,7 @@ class HaBackupOverviewBackups extends LitElement {
|
||||
if (this.fetching) {
|
||||
return this._renderSummaryCard(
|
||||
this.hass.localize("ui.panel.config.backup.overview.summary.loading"),
|
||||
"loading",
|
||||
"none",
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
@@ -291,6 +291,9 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
||||
this._unsub = subscribeBackupEvents(
|
||||
this.hass!,
|
||||
(event) => {
|
||||
if ("agent_id" in event) {
|
||||
return;
|
||||
}
|
||||
if (event.manager_state === "idle" && this._state === "in_progress") {
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
@@ -63,6 +63,11 @@ class HaConfigBackupOverview extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public agents: BackupAgent[] = [];
|
||||
|
||||
@property({ attribute: false }) public uploadProgress: Record<
|
||||
string,
|
||||
{ uploaded_bytes: number; total_bytes: number }
|
||||
> = {};
|
||||
|
||||
private _uploadBackup = async () => {
|
||||
await showUploadBackupDialog(this, {});
|
||||
};
|
||||
@@ -182,6 +187,8 @@ class HaConfigBackupOverview extends LitElement {
|
||||
<ha-backup-overview-progress
|
||||
.hass=${this.hass}
|
||||
.manager=${this.manager}
|
||||
.agents=${this.agents}
|
||||
.uploadProgress=${this.uploadProgress}
|
||||
>
|
||||
</ha-backup-overview-progress>
|
||||
`
|
||||
|
||||
@@ -52,6 +52,11 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
|
||||
|
||||
@state() private _config?: BackupConfig;
|
||||
|
||||
@state() private _uploadProgress: Record<
|
||||
string,
|
||||
{ uploaded_bytes: number; total_bytes: number }
|
||||
> = {};
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._fetchAll();
|
||||
@@ -138,6 +143,7 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
|
||||
pageEl.config = this._config;
|
||||
pageEl.agents = this._agents;
|
||||
pageEl.fetching = this._fetching;
|
||||
pageEl.uploadProgress = this._uploadProgress;
|
||||
|
||||
if (!changedProps || changedProps.has("route")) {
|
||||
switch (this._currentPage) {
|
||||
@@ -154,6 +160,17 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
|
||||
public hassSubscribe(): Promise<UnsubscribeFunc>[] {
|
||||
return [
|
||||
subscribeBackupEvents(this.hass!, (event) => {
|
||||
if ("agent_id" in event) {
|
||||
this._uploadProgress = {
|
||||
...this._uploadProgress,
|
||||
[event.agent_id]: {
|
||||
uploaded_bytes: event.uploaded_bytes,
|
||||
total_bytes: event.total_bytes,
|
||||
},
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const curState = this._manager.manager_state;
|
||||
|
||||
this._manager = event;
|
||||
@@ -161,6 +178,7 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
|
||||
event.manager_state === "idle" &&
|
||||
event.manager_state !== curState
|
||||
) {
|
||||
this._uploadProgress = {};
|
||||
this._fetchAll();
|
||||
}
|
||||
if ("state" in event) {
|
||||
|
||||
@@ -3297,6 +3297,16 @@
|
||||
"receive_file": "Receiving file",
|
||||
"upload_to_agents": "Uploading to locations"
|
||||
}
|
||||
},
|
||||
"segments": {
|
||||
"home_assistant": "Home Assistant",
|
||||
"apps": "Apps",
|
||||
"media": "Media",
|
||||
"upload": "Uploading backup"
|
||||
},
|
||||
"agent_status": {
|
||||
"uploading": "Uploading...",
|
||||
"uploaded": "Uploaded"
|
||||
}
|
||||
},
|
||||
"summary": {
|
||||
@@ -3304,7 +3314,7 @@
|
||||
"next_automatic_backup": "Next automatic backup {day} at {time}",
|
||||
"today": "today",
|
||||
"tomorrow": "tomorrow",
|
||||
"loading": "Loading backups...",
|
||||
"loading": "Loading backups",
|
||||
"last_backup_failed_heading": "Last automatic backup failed",
|
||||
"last_backup_failed_description": "The last automatic backup triggered {relative_time} wasn't successful.",
|
||||
"last_backup_failed_locations_description": "The last automatic backup created {relative_time} wasn't stored in all locations.",
|
||||
|
||||
Reference in New Issue
Block a user