mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +00:00
Merge branch 'rc'
This commit is contained in:
commit
7aa2136c21
@ -3,6 +3,7 @@
|
|||||||
import { constants } from "node:zlib";
|
import { constants } from "node:zlib";
|
||||||
import gulp from "gulp";
|
import gulp from "gulp";
|
||||||
import brotli from "gulp-brotli";
|
import brotli from "gulp-brotli";
|
||||||
|
import zopfli from "gulp-zopfli-green";
|
||||||
import paths from "../paths.cjs";
|
import paths from "../paths.cjs";
|
||||||
|
|
||||||
const filesGlob = "*.{js,json,css,svg,xml}";
|
const filesGlob = "*.{js,json,css,svg,xml}";
|
||||||
@ -12,17 +13,18 @@ const brotliOptions = {
|
|||||||
[constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY,
|
[constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
const zopfliOptions = { threshold: 150 };
|
||||||
|
|
||||||
const compressModern = (rootDir, modernDir) =>
|
const compressModern = (rootDir, modernDir, compress) =>
|
||||||
gulp
|
gulp
|
||||||
.src([`${modernDir}/**/${filesGlob}`, `${rootDir}/sw-modern.js`], {
|
.src([`${modernDir}/**/${filesGlob}`, `${rootDir}/sw-modern.js`], {
|
||||||
base: rootDir,
|
base: rootDir,
|
||||||
allowEmpty: true,
|
allowEmpty: true,
|
||||||
})
|
})
|
||||||
.pipe(brotli(brotliOptions))
|
.pipe(compress === "zopfli" ? zopfli(zopfliOptions) : brotli(brotliOptions))
|
||||||
.pipe(gulp.dest(rootDir));
|
.pipe(gulp.dest(rootDir));
|
||||||
|
|
||||||
const compressOther = (rootDir, modernDir) =>
|
const compressOther = (rootDir, modernDir, compress) =>
|
||||||
gulp
|
gulp
|
||||||
.src(
|
.src(
|
||||||
[
|
[
|
||||||
@ -33,21 +35,52 @@ const compressOther = (rootDir, modernDir) =>
|
|||||||
],
|
],
|
||||||
{ base: rootDir, allowEmpty: true }
|
{ base: rootDir, allowEmpty: true }
|
||||||
)
|
)
|
||||||
.pipe(brotli(brotliOptions))
|
.pipe(compress === "zopfli" ? zopfli(zopfliOptions) : brotli(brotliOptions))
|
||||||
.pipe(gulp.dest(rootDir));
|
.pipe(gulp.dest(rootDir));
|
||||||
|
|
||||||
const compressAppModern = () =>
|
const compressAppModernBrotli = () =>
|
||||||
compressModern(paths.app_output_root, paths.app_output_latest);
|
compressModern(paths.app_output_root, paths.app_output_latest, "brotli");
|
||||||
const compressHassioModern = () =>
|
const compressAppModernZopfli = () =>
|
||||||
compressModern(paths.hassio_output_root, paths.hassio_output_latest);
|
compressModern(paths.app_output_root, paths.app_output_latest, "zopfli");
|
||||||
|
|
||||||
const compressAppOther = () =>
|
const compressHassioModernBrotli = () =>
|
||||||
compressOther(paths.app_output_root, paths.app_output_latest);
|
compressModern(
|
||||||
const compressHassioOther = () =>
|
paths.hassio_output_root,
|
||||||
compressOther(paths.hassio_output_root, paths.hassio_output_latest);
|
paths.hassio_output_latest,
|
||||||
|
"brotli"
|
||||||
|
);
|
||||||
|
const compressHassioModernZopfli = () =>
|
||||||
|
compressModern(
|
||||||
|
paths.hassio_output_root,
|
||||||
|
paths.hassio_output_latest,
|
||||||
|
"zopfli"
|
||||||
|
);
|
||||||
|
|
||||||
gulp.task("compress-app", gulp.parallel(compressAppModern, compressAppOther));
|
const compressAppOtherBrotli = () =>
|
||||||
|
compressOther(paths.app_output_root, paths.app_output_latest, "brotli");
|
||||||
|
const compressAppOtherZopfli = () =>
|
||||||
|
compressOther(paths.app_output_root, paths.app_output_latest, "zopfli");
|
||||||
|
|
||||||
|
const compressHassioOtherBrotli = () =>
|
||||||
|
compressOther(paths.hassio_output_root, paths.hassio_output_latest, "brotli");
|
||||||
|
const compressHassioOtherZopfli = () =>
|
||||||
|
compressOther(paths.hassio_output_root, paths.hassio_output_latest, "zopfli");
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"compress-app",
|
||||||
|
gulp.parallel(
|
||||||
|
compressAppModernBrotli,
|
||||||
|
compressAppOtherBrotli,
|
||||||
|
compressAppModernZopfli,
|
||||||
|
compressAppOtherZopfli
|
||||||
|
)
|
||||||
|
);
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"compress-hassio",
|
"compress-hassio",
|
||||||
gulp.parallel(compressHassioModern, compressHassioOther)
|
gulp.parallel(
|
||||||
|
compressHassioModernBrotli,
|
||||||
|
compressHassioOtherBrotli,
|
||||||
|
compressHassioModernZopfli,
|
||||||
|
compressHassioOtherZopfli
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
@ -179,8 +179,8 @@ class HassioBackupDialog
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _restoreClicked() {
|
private async _restoreClicked() {
|
||||||
this._restoringBackup = true;
|
|
||||||
const backupDetails = this._backupContent.backupDetails();
|
const backupDetails = this._backupContent.backupDetails();
|
||||||
|
this._restoringBackup = true;
|
||||||
|
|
||||||
const supervisor = this._dialogParams?.supervisor;
|
const supervisor = this._dialogParams?.supervisor;
|
||||||
if (supervisor !== undefined && supervisor.info.state !== "running") {
|
if (supervisor !== undefined && supervisor.info.state !== "running") {
|
||||||
@ -196,12 +196,12 @@ class HassioBackupDialog
|
|||||||
if (
|
if (
|
||||||
!(await showConfirmationDialog(this, {
|
!(await showConfirmationDialog(this, {
|
||||||
title: this._localize(
|
title: this._localize(
|
||||||
this._backupContent.backupType === "full"
|
this._backup!.type === "full"
|
||||||
? "confirm_restore_full_backup_title"
|
? "confirm_restore_full_backup_title"
|
||||||
: "confirm_restore_partial_backup_title"
|
: "confirm_restore_partial_backup_title"
|
||||||
),
|
),
|
||||||
text: this._localize(
|
text: this._localize(
|
||||||
this._backupContent.backupType === "full"
|
this._backup!.type === "full"
|
||||||
? "confirm_restore_full_backup_text"
|
? "confirm_restore_full_backup_text"
|
||||||
: "confirm_restore_partial_backup_text"
|
: "confirm_restore_partial_backup_text"
|
||||||
),
|
),
|
||||||
@ -216,9 +216,9 @@ class HassioBackupDialog
|
|||||||
try {
|
try {
|
||||||
await restoreBackup(
|
await restoreBackup(
|
||||||
this.hass,
|
this.hass,
|
||||||
this._backupContent.backupType,
|
this._backup!.type,
|
||||||
this._backup!.slug,
|
this._backup!.slug,
|
||||||
backupDetails,
|
{ ...backupDetails, background: this._dialogParams?.onboarding },
|
||||||
!!this.hass && atLeastVersion(this.hass.config.version, 2021, 9)
|
!!this.hass && atLeastVersion(this.hass.config.version, 2021, 9)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -115,6 +115,7 @@
|
|||||||
"element-internals-polyfill": "1.3.12",
|
"element-internals-polyfill": "1.3.12",
|
||||||
"fuse.js": "7.0.0",
|
"fuse.js": "7.0.0",
|
||||||
"google-timezones-json": "1.2.0",
|
"google-timezones-json": "1.2.0",
|
||||||
|
"gulp-zopfli-green": "6.0.2",
|
||||||
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
||||||
"home-assistant-js-websocket": "9.4.0",
|
"home-assistant-js-websocket": "9.4.0",
|
||||||
"idb-keyval": "6.2.1",
|
"idb-keyval": "6.2.1",
|
||||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "home-assistant-frontend"
|
name = "home-assistant-frontend"
|
||||||
version = "20241224.0"
|
version = "20250103.0"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
description = "The Home Assistant frontend"
|
description = "The Home Assistant frontend"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
type NonUndefined<T> = T extends undefined ? never : T;
|
type NonNullUndefined<T> = T extends undefined
|
||||||
|
? never
|
||||||
|
: T extends null
|
||||||
|
? never
|
||||||
|
: T;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure that the input is an array or wrap it in an array
|
* Ensure that the input is an array or wrap it in an array
|
||||||
* @param value - The value to ensure is an array
|
* @param value - The value to ensure is an array
|
||||||
*/
|
*/
|
||||||
export function ensureArray(value: undefined): undefined;
|
export function ensureArray(value: undefined): undefined;
|
||||||
export function ensureArray<T>(value: T | T[]): NonUndefined<T>[];
|
export function ensureArray(value: null): null;
|
||||||
export function ensureArray<T>(value: T | readonly T[]): NonUndefined<T>[];
|
export function ensureArray<T>(value: T | T[]): NonNullUndefined<T>[];
|
||||||
|
export function ensureArray<T>(value: T | readonly T[]): NonNullUndefined<T>[];
|
||||||
export function ensureArray(value) {
|
export function ensureArray(value) {
|
||||||
if (value === undefined || Array.isArray(value)) {
|
if (value === undefined || value === null || Array.isArray(value)) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
return [value];
|
return [value];
|
||||||
|
@ -14,9 +14,16 @@ export interface NavigateOptions {
|
|||||||
data?: any;
|
data?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const navigate = async (path: string, options?: NavigateOptions) => {
|
// max time to wait for dialogs to close before navigating
|
||||||
|
const DIALOG_WAIT_TIMEOUT = 500;
|
||||||
|
|
||||||
|
export const navigate = async (
|
||||||
|
path: string,
|
||||||
|
options?: NavigateOptions,
|
||||||
|
timestamp = Date.now()
|
||||||
|
) => {
|
||||||
const { history } = mainWindow;
|
const { history } = mainWindow;
|
||||||
if (history.state?.dialog) {
|
if (history.state?.dialog && Date.now() - timestamp < DIALOG_WAIT_TIMEOUT) {
|
||||||
const closed = await closeAllDialogs();
|
const closed = await closeAllDialogs();
|
||||||
if (!closed) {
|
if (!closed) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
@ -26,7 +33,7 @@ export const navigate = async (path: string, options?: NavigateOptions) => {
|
|||||||
return new Promise<boolean>((resolve) => {
|
return new Promise<boolean>((resolve) => {
|
||||||
// need to wait for history state to be updated in case a dialog was closed
|
// need to wait for history state to be updated in case a dialog was closed
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigate(path, options).then(resolve);
|
navigate(path, options, timestamp).then(resolve);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export const copyToClipboard = async (str) => {
|
export const copyToClipboard = async (str, rootEl?: HTMLElement) => {
|
||||||
if (navigator.clipboard) {
|
if (navigator.clipboard) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(str);
|
await navigator.clipboard.writeText(str);
|
||||||
@ -8,10 +8,12 @@ export const copyToClipboard = async (str) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const root = rootEl ?? document.body;
|
||||||
|
|
||||||
const el = document.createElement("textarea");
|
const el = document.createElement("textarea");
|
||||||
el.value = str;
|
el.value = str;
|
||||||
document.body.appendChild(el);
|
root.appendChild(el);
|
||||||
el.select();
|
el.select();
|
||||||
document.execCommand("copy");
|
document.execCommand("copy");
|
||||||
document.body.removeChild(el);
|
root.removeChild(el);
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,25 @@
|
|||||||
|
class TimeoutError extends Error {
|
||||||
|
public timeout: number;
|
||||||
|
|
||||||
|
constructor(timeout: number, ...params) {
|
||||||
|
super(...params);
|
||||||
|
|
||||||
|
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
||||||
|
if (Error.captureStackTrace) {
|
||||||
|
Error.captureStackTrace(this, TimeoutError);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.name = "TimeoutError";
|
||||||
|
// Custom debugging information
|
||||||
|
this.timeout = timeout;
|
||||||
|
this.message = `Timed out in ${timeout} ms.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const promiseTimeout = (ms: number, promise: Promise<any> | any) => {
|
export const promiseTimeout = (ms: number, promise: Promise<any> | any) => {
|
||||||
const timeout = new Promise((_resolve, reject) => {
|
const timeout = new Promise((_resolve, reject) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
reject(`Timed out in ${ms} ms.`);
|
reject(new TimeoutError(ms));
|
||||||
}, ms);
|
}, ms);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -10,11 +10,13 @@ import { css, html, nothing, LitElement } from "lit";
|
|||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
|
import { mdiRestart } from "@mdi/js";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { clamp } from "../../common/number/clamp";
|
import { clamp } from "../../common/number/clamp";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { debounce } from "../../common/util/debounce";
|
import { debounce } from "../../common/util/debounce";
|
||||||
import { isMac } from "../../util/is_mac";
|
import { isMac } from "../../util/is_mac";
|
||||||
|
import "../ha-icon-button";
|
||||||
|
|
||||||
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
||||||
|
|
||||||
@ -300,6 +302,16 @@ export class HaChartBase extends LitElement {
|
|||||||
: this.hass.localize("ui.components.history_charts.zoom_hint")}
|
: this.hass.localize("ui.components.history_charts.zoom_hint")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
${this._isZoomed && this.chartType !== "timeline"
|
||||||
|
? html`<ha-icon-button
|
||||||
|
class="zoom-reset"
|
||||||
|
.path=${mdiRestart}
|
||||||
|
@click=${this._handleZoomReset}
|
||||||
|
title=${this.hass.localize(
|
||||||
|
"ui.components.history_charts.zoom_reset"
|
||||||
|
)}
|
||||||
|
></ha-icon-button>`
|
||||||
|
: nothing}
|
||||||
${this._tooltip
|
${this._tooltip
|
||||||
? html`<div
|
? html`<div
|
||||||
class="chart-tooltip ${classMap({
|
class="chart-tooltip ${classMap({
|
||||||
@ -420,7 +432,11 @@ export class HaChartBase extends LitElement {
|
|||||||
modifierKey,
|
modifierKey,
|
||||||
speed: 0.05,
|
speed: 0.05,
|
||||||
},
|
},
|
||||||
mode: "x",
|
mode:
|
||||||
|
this.chartType !== "timeline" &&
|
||||||
|
(this.options?.scales?.y as any)?.type === "category"
|
||||||
|
? "y"
|
||||||
|
: "x",
|
||||||
onZoomComplete: () => {
|
onZoomComplete: () => {
|
||||||
const isZoomed = this.chart?.isZoomedOrPanned() ?? false;
|
const isZoomed = this.chart?.isZoomedOrPanned() ?? false;
|
||||||
if (this._isZoomed && !isZoomed) {
|
if (this._isZoomed && !isZoomed) {
|
||||||
@ -541,6 +557,10 @@ export class HaChartBase extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _handleZoomReset() {
|
||||||
|
this.chart?.resetZoom();
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
:host {
|
:host {
|
||||||
@ -552,6 +572,9 @@ export class HaChartBase extends LitElement {
|
|||||||
height: 0;
|
height: 0;
|
||||||
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
.chart-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
canvas {
|
canvas {
|
||||||
max-height: var(--chart-max-height, 400px);
|
max-height: var(--chart-max-height, 400px);
|
||||||
}
|
}
|
||||||
@ -670,6 +693,16 @@ export class HaChartBase extends LitElement {
|
|||||||
background: rgba(0, 0, 0, 0.3);
|
background: rgba(0, 0, 0, 0.3);
|
||||||
box-shadow: 0 0 32px 32px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 0 32px 32px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
.zoom-reset {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
right: 4px;
|
||||||
|
background: var(--card-background-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
--mdc-icon-button-size: 32px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -515,7 +515,7 @@ export class HaDataTable extends LitElement {
|
|||||||
return html`<div class="mdc-data-table__row">${row.content}</div>`;
|
return html`<div class="mdc-data-table__row">${row.content}</div>`;
|
||||||
}
|
}
|
||||||
if (row.empty) {
|
if (row.empty) {
|
||||||
return html`<div class="mdc-data-table__row"></div>`;
|
return html`<div class="mdc-data-table__row empty-row"></div>`;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
@ -960,6 +960,13 @@ export class HaDataTable extends LitElement {
|
|||||||
width: var(--table-row-width, 100%);
|
width: var(--table-row-width, 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mdc-data-table__row.empty-row {
|
||||||
|
height: var(
|
||||||
|
--data-table-empty-row-height,
|
||||||
|
var(--data-table-row-height, 52px)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
.mdc-data-table__row ~ .mdc-data-table__row {
|
.mdc-data-table__row ~ .mdc-data-table__row {
|
||||||
border-top: 1px solid var(--divider-color);
|
border-top: 1px solid var(--divider-color);
|
||||||
}
|
}
|
||||||
|
@ -222,7 +222,9 @@ export class HaDevicePicker extends LitElement {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: device.id,
|
id: device.id,
|
||||||
name: name,
|
name:
|
||||||
|
name ||
|
||||||
|
this.hass.localize("ui.components.device-picker.unnamed_device"),
|
||||||
area:
|
area:
|
||||||
device.area_id && areas[device.area_id]
|
device.area_id && areas[device.area_id]
|
||||||
? areas[device.area_id].name
|
? areas[device.area_id].name
|
||||||
|
@ -102,10 +102,10 @@ export class HaDialog extends DialogBase {
|
|||||||
align-items: var(--vertical-align-dialog, center);
|
align-items: var(--vertical-align-dialog, center);
|
||||||
}
|
}
|
||||||
.mdc-dialog__title {
|
.mdc-dialog__title {
|
||||||
padding: 12px 12px 0;
|
padding: 24px 24px 0 24px;
|
||||||
}
|
}
|
||||||
.mdc-dialog--scrollable .mdc-dialog__title {
|
.mdc-dialog__title:has(span) {
|
||||||
padding: 12px;
|
padding: 12px 12px 0;
|
||||||
}
|
}
|
||||||
.mdc-dialog__actions {
|
.mdc-dialog__actions {
|
||||||
padding: 12px 24px 12px 24px;
|
padding: 12px 24px 12px 24px;
|
||||||
|
@ -62,6 +62,7 @@ export class HaGauge extends LitElement {
|
|||||||
if (
|
if (
|
||||||
!this._updated ||
|
!this._updated ||
|
||||||
(!changedProperties.has("value") &&
|
(!changedProperties.has("value") &&
|
||||||
|
!changedProperties.has("valueText") &&
|
||||||
!changedProperties.has("label") &&
|
!changedProperties.has("label") &&
|
||||||
!changedProperties.has("_segment_label"))
|
!changedProperties.has("_segment_label"))
|
||||||
) {
|
) {
|
||||||
|
@ -95,7 +95,7 @@ export class HaPictureUpload extends LitElement {
|
|||||||
<ha-button
|
<ha-button
|
||||||
@click=${this._handleChangeClick}
|
@click=${this._handleChangeClick}
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.components.picture-upload.change_picture"
|
"ui.components.picture-upload.clear_picture"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
</ha-button>
|
</ha-button>
|
||||||
|
@ -89,7 +89,7 @@ export class HaServiceControl extends LitElement {
|
|||||||
@property({ attribute: "show-advanced", type: Boolean }) public showAdvanced =
|
@property({ attribute: "show-advanced", type: Boolean }) public showAdvanced =
|
||||||
false;
|
false;
|
||||||
|
|
||||||
@property({ attribute: false, type: Boolean, reflect: true })
|
@property({ attribute: "hide-picker", type: Boolean, reflect: true })
|
||||||
public hidePicker = false;
|
public hidePicker = false;
|
||||||
|
|
||||||
@property({ attribute: "hide-description", type: Boolean })
|
@property({ attribute: "hide-description", type: Boolean })
|
||||||
|
@ -117,7 +117,7 @@ class DialogMediaManage extends LitElement {
|
|||||||
: html`
|
: html`
|
||||||
<ha-button
|
<ha-button
|
||||||
class="danger"
|
class="danger"
|
||||||
slot="title"
|
slot="navigationIcon"
|
||||||
.disabled=${this._deleting}
|
.disabled=${this._deleting}
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
`ui.components.media-browser.file_management.${
|
`ui.components.media-browser.file_management.${
|
||||||
@ -212,8 +212,8 @@ class DialogMediaManage extends LitElement {
|
|||||||
>
|
>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.components.media-browser.file_management.tip_storage_panel"
|
"ui.components.media-browser.file_management.tip_storage_panel"
|
||||||
)}
|
)}</a
|
||||||
</a>`,
|
>`,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
</ha-tip>`
|
</ha-tip>`
|
||||||
|
@ -20,8 +20,8 @@ export class HatGraphNode extends LitElement {
|
|||||||
@property({ attribute: false, reflect: true, type: Boolean }) notEnabled =
|
@property({ attribute: false, reflect: true, type: Boolean }) notEnabled =
|
||||||
false;
|
false;
|
||||||
|
|
||||||
@property({ attribute: false, reflect: true, type: Boolean }) graphStart =
|
@property({ attribute: "graph-start", reflect: true, type: Boolean })
|
||||||
false;
|
graphStart = false;
|
||||||
|
|
||||||
@property({ type: Boolean, attribute: "nofocus" }) noFocus = false;
|
@property({ type: Boolean, attribute: "nofocus" }) noFocus = false;
|
||||||
|
|
||||||
@ -112,7 +112,7 @@ export class HatGraphNode extends LitElement {
|
|||||||
var(--hat-graph-node-size) + var(--hat-graph-spacing) + 1px
|
var(--hat-graph-node-size) + var(--hat-graph-spacing) + 1px
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
:host([graphStart]) {
|
:host([graph-start]) {
|
||||||
height: calc(var(--hat-graph-node-size) + 2px);
|
height: calc(var(--hat-graph-node-size) + 2px);
|
||||||
}
|
}
|
||||||
:host([track]) {
|
:host([track]) {
|
||||||
|
@ -91,7 +91,7 @@ export class HatScriptGraph extends LitElement {
|
|||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<hat-graph-node
|
<hat-graph-node
|
||||||
graphStart
|
graph-start
|
||||||
?track=${track}
|
?track=${track}
|
||||||
@focus=${this._selectNode(config, path)}
|
@focus=${this._selectNode(config, path)}
|
||||||
?active=${this.selected === path}
|
?active=${this.selected === path}
|
||||||
@ -354,8 +354,8 @@ export class HatScriptGraph extends LitElement {
|
|||||||
></hat-graph-node>
|
></hat-graph-node>
|
||||||
<div
|
<div
|
||||||
style=${`width: ${NODE_SIZE + SPACING}px;`}
|
style=${`width: ${NODE_SIZE + SPACING}px;`}
|
||||||
graphStart
|
graph-start
|
||||||
graphEnd
|
graph-end
|
||||||
></div>
|
></div>
|
||||||
<div ?track=${trackPass}></div>
|
<div ?track=${trackPass}></div>
|
||||||
<hat-graph-node
|
<hat-graph-node
|
||||||
|
@ -737,18 +737,22 @@ const tryDescribeTrigger = (
|
|||||||
? computeStateName(hass.states[trigger.entity_id])
|
? computeStateName(hass.states[trigger.entity_id])
|
||||||
: trigger.entity_id;
|
: trigger.entity_id;
|
||||||
|
|
||||||
let offsetChoice = trigger.offset.startsWith("-") ? "before" : "after";
|
let offsetChoice: string = "other";
|
||||||
let offset: string | string[] = trigger.offset.startsWith("-")
|
let offset: string | string[] = "";
|
||||||
? trigger.offset.substring(1).split(":")
|
if (trigger.offset) {
|
||||||
: trigger.offset.split(":");
|
offsetChoice = trigger.offset.startsWith("-") ? "before" : "after";
|
||||||
const duration = {
|
offset = trigger.offset.startsWith("-")
|
||||||
hours: offset.length > 0 ? +offset[0] : 0,
|
? trigger.offset.substring(1).split(":")
|
||||||
minutes: offset.length > 1 ? +offset[1] : 0,
|
: trigger.offset.split(":");
|
||||||
seconds: offset.length > 2 ? +offset[2] : 0,
|
const duration = {
|
||||||
};
|
hours: offset.length > 0 ? +offset[0] : 0,
|
||||||
offset = formatDurationLong(hass.locale, duration);
|
minutes: offset.length > 1 ? +offset[1] : 0,
|
||||||
if (offset === "") {
|
seconds: offset.length > 2 ? +offset[2] : 0,
|
||||||
offsetChoice = "other";
|
};
|
||||||
|
offset = formatDurationLong(hass.locale, duration);
|
||||||
|
if (offset === "") {
|
||||||
|
offsetChoice = "other";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return hass.localize(
|
return hass.localize(
|
||||||
|
@ -46,6 +46,7 @@ export interface HassioFullBackupCreateParams {
|
|||||||
name: string;
|
name: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
confirm_password?: string;
|
confirm_password?: string;
|
||||||
|
background?: boolean;
|
||||||
}
|
}
|
||||||
export interface HassioPartialBackupCreateParams
|
export interface HassioPartialBackupCreateParams
|
||||||
extends HassioFullBackupCreateParams {
|
extends HassioFullBackupCreateParams {
|
||||||
|
@ -9,7 +9,7 @@ export interface ShowViewConfig {
|
|||||||
|
|
||||||
export interface LovelaceViewBackgroundConfig {
|
export interface LovelaceViewBackgroundConfig {
|
||||||
image?: string;
|
image?: string;
|
||||||
transparency?: number;
|
opacity?: number;
|
||||||
size?: "auto" | "cover" | "contain";
|
size?: "auto" | "cover" | "contain";
|
||||||
alignment?:
|
alignment?:
|
||||||
| "top left"
|
| "top left"
|
||||||
|
@ -3,7 +3,7 @@ import type { CSSResultGroup } from "lit";
|
|||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import "../../components/ha-dialog";
|
import { createCloseHeading } from "../../components/ha-dialog";
|
||||||
import "../../components/ha-formfield";
|
import "../../components/ha-formfield";
|
||||||
import "../../components/ha-switch";
|
import "../../components/ha-switch";
|
||||||
import type { HaSwitch } from "../../components/ha-switch";
|
import type { HaSwitch } from "../../components/ha-switch";
|
||||||
@ -52,14 +52,14 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
|||||||
<ha-dialog
|
<ha-dialog
|
||||||
open
|
open
|
||||||
@closed=${this.closeDialog}
|
@closed=${this.closeDialog}
|
||||||
.heading=${this.hass.localize(
|
.heading=${createCloseHeading(
|
||||||
"ui.dialogs.config_entry_system_options.title",
|
this.hass,
|
||||||
{
|
this.hass.localize("ui.dialogs.config_entry_system_options.title", {
|
||||||
integration:
|
integration:
|
||||||
this.hass.localize(
|
this.hass.localize(
|
||||||
`component.${this._params.entry.domain}.title`
|
`component.${this._params.entry.domain}.title`
|
||||||
) || this._params.entry.domain,
|
) || this._params.entry.domain,
|
||||||
}
|
})
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||||
|
@ -225,6 +225,7 @@ export const makeDialogManager = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const _handleClosedFocus = async (ev: HASSDomEvent<DialogClosedParams>) => {
|
const _handleClosedFocus = async (ev: HASSDomEvent<DialogClosedParams>) => {
|
||||||
|
if (!LOADED[ev.detail.dialog]) return;
|
||||||
const closedFocusTargets = LOADED[ev.detail.dialog].closedFocusTargets;
|
const closedFocusTargets = LOADED[ev.detail.dialog].closedFocusTargets;
|
||||||
delete LOADED[ev.detail.dialog].closedFocusTargets;
|
delete LOADED[ev.detail.dialog].closedFocusTargets;
|
||||||
if (!closedFocusTargets) return;
|
if (!closedFocusTargets) return;
|
||||||
|
@ -99,6 +99,7 @@ class MoreInfoScript extends LitElement {
|
|||||||
${this.hass.localize("ui.card.script.run_script")}
|
${this.hass.localize("ui.card.script.run_script")}
|
||||||
</div>
|
</div>
|
||||||
<ha-service-control
|
<ha-service-control
|
||||||
|
hide-picker
|
||||||
hide-description
|
hide-description
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this._scriptData}
|
.value=${this._scriptData}
|
||||||
|
@ -530,7 +530,9 @@ export class QuickBar extends LitElement {
|
|||||||
? this.hass.areas[device.area_id]
|
? this.hass.areas[device.area_id]
|
||||||
: undefined;
|
: undefined;
|
||||||
const deviceItem = {
|
const deviceItem = {
|
||||||
primaryText: computeDeviceName(device, this.hass),
|
primaryText:
|
||||||
|
computeDeviceName(device, this.hass) ||
|
||||||
|
this.hass.localize("ui.components.device-picker.unnamed_device"),
|
||||||
deviceId: device.id,
|
deviceId: device.id,
|
||||||
area: area?.name,
|
area: area?.name,
|
||||||
action: () => navigate(`/config/devices/device/${device.id}`),
|
action: () => navigate(`/config/devices/device/${device.id}`),
|
||||||
|
@ -322,6 +322,13 @@ class HassTabsSubpage extends LitElement {
|
|||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host([narrow]) .content {
|
||||||
|
height: calc(100% - var(--header-height));
|
||||||
|
height: calc(
|
||||||
|
100% - var(--header-height) - env(safe-area-inset-bottom)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
:host([narrow]) .content.tabs {
|
:host([narrow]) .content.tabs {
|
||||||
height: calc(100% - 2 * var(--header-height));
|
height: calc(100% - 2 * var(--header-height));
|
||||||
height: calc(
|
height: calc(
|
||||||
|
@ -614,9 +614,6 @@ export default class HaAutomationActionRow extends LitElement {
|
|||||||
ha-icon-button {
|
ha-icon-button {
|
||||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||||
}
|
}
|
||||||
ha-card {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.disabled {
|
.disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@ -649,6 +646,8 @@ export default class HaAutomationActionRow extends LitElement {
|
|||||||
.disabled-bar {
|
.disabled-bar {
|
||||||
background: var(--divider-color, #e0e0e0);
|
background: var(--divider-color, #e0e0e0);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
border-top-right-radius: var(--ha-card-border-radius, 12px);
|
||||||
|
border-top-left-radius: var(--ha-card-border-radius, 12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
mwc-list-item[disabled] {
|
mwc-list-item[disabled] {
|
||||||
|
@ -328,8 +328,7 @@ class DialogAutomationRename extends LitElement implements HassDialog {
|
|||||||
ha-icon-picker,
|
ha-icon-picker,
|
||||||
ha-category-picker,
|
ha-category-picker,
|
||||||
ha-labels-picker,
|
ha-labels-picker,
|
||||||
ha-area-picker,
|
ha-area-picker {
|
||||||
ha-chip-set {
|
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
ha-icon-picker,
|
ha-icon-picker,
|
||||||
|
@ -504,9 +504,6 @@ export default class HaAutomationConditionRow extends LitElement {
|
|||||||
ha-button-menu {
|
ha-button-menu {
|
||||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||||
}
|
}
|
||||||
ha-card {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.disabled {
|
.disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@ -539,6 +536,8 @@ export default class HaAutomationConditionRow extends LitElement {
|
|||||||
.disabled-bar {
|
.disabled-bar {
|
||||||
background: var(--divider-color, #e0e0e0);
|
background: var(--divider-color, #e0e0e0);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
border-top-right-radius: var(--ha-card-border-radius, 12px);
|
||||||
|
border-top-left-radius: var(--ha-card-border-radius, 12px);
|
||||||
}
|
}
|
||||||
ha-list-item[disabled] {
|
ha-list-item[disabled] {
|
||||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||||
@ -560,6 +559,8 @@ export default class HaAutomationConditionRow extends LitElement {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: max-height 0.3s;
|
transition: max-height 0.3s;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
border-top-right-radius: var(--ha-card-border-radius, 12px);
|
||||||
|
border-top-left-radius: var(--ha-card-border-radius, 12px);
|
||||||
}
|
}
|
||||||
.testing.active {
|
.testing.active {
|
||||||
max-height: 100px;
|
max-height: 100px;
|
||||||
|
@ -27,6 +27,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
|||||||
import { navigate } from "../../../common/navigate";
|
import { navigate } from "../../../common/navigate";
|
||||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||||
import { afterNextRender } from "../../../common/util/render-status";
|
import { afterNextRender } from "../../../common/util/render-status";
|
||||||
|
import { promiseTimeout } from "../../../common/util/promise-timeout";
|
||||||
import "../../../components/ha-button-menu";
|
import "../../../components/ha-button-menu";
|
||||||
import "../../../components/ha-fab";
|
import "../../../components/ha-fab";
|
||||||
import "../../../components/ha-icon";
|
import "../../../components/ha-icon";
|
||||||
@ -944,8 +945,37 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
|||||||
|
|
||||||
// wait for automation to appear in entity registry when creating a new automation
|
// wait for automation to appear in entity registry when creating a new automation
|
||||||
if (entityRegPromise) {
|
if (entityRegPromise) {
|
||||||
const automation = await entityRegPromise;
|
try {
|
||||||
entityId = automation.entity_id;
|
const automation = await promiseTimeout(2000, entityRegPromise);
|
||||||
|
entityId = automation.entity_id;
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && e.name === "TimeoutError") {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.automation.editor.new_automation_setup_failed_title",
|
||||||
|
{
|
||||||
|
type: this.hass.localize(
|
||||||
|
"ui.panel.config.automation.editor.type_automation"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
text: this.hass.localize(
|
||||||
|
"ui.panel.config.automation.editor.new_automation_setup_failed_text",
|
||||||
|
{
|
||||||
|
type: this.hass.localize(
|
||||||
|
"ui.panel.config.automation.editor.type_automation"
|
||||||
|
),
|
||||||
|
types: this.hass.localize(
|
||||||
|
"ui.panel.config.automation.editor.type_automation_plural"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
warning: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entityId) {
|
if (entityId) {
|
||||||
@ -965,9 +995,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
|||||||
navigate(`/config/automation/edit/${id}`, { replace: true });
|
navigate(`/config/automation/edit/${id}`, { replace: true });
|
||||||
}
|
}
|
||||||
} catch (errors: any) {
|
} catch (errors: any) {
|
||||||
this._errors = errors.body.message || errors.error || errors.body;
|
this._errors = errors.body?.message || errors.error || errors.body;
|
||||||
showToast(this, {
|
showToast(this, {
|
||||||
message: errors.body.message || errors.error || errors.body,
|
message: errors.body?.message || errors.error || errors.body,
|
||||||
});
|
});
|
||||||
throw errors;
|
throw errors;
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -651,9 +651,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
ha-button-menu {
|
ha-button-menu {
|
||||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||||
}
|
}
|
||||||
ha-card {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.disabled {
|
.disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@ -686,6 +683,8 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
.disabled-bar {
|
.disabled-bar {
|
||||||
background: var(--divider-color, #e0e0e0);
|
background: var(--divider-color, #e0e0e0);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
border-top-right-radius: var(--ha-card-border-radius, 12px);
|
||||||
|
border-top-left-radius: var(--ha-card-border-radius, 12px);
|
||||||
}
|
}
|
||||||
.triggered {
|
.triggered {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -702,6 +701,8 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: max-height 0.3s;
|
transition: max-height 0.3s;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
border-top-right-radius: var(--ha-card-border-radius, 12px);
|
||||||
|
border-top-left-radius: var(--ha-card-border-radius, 12px);
|
||||||
}
|
}
|
||||||
.triggered.active {
|
.triggered.active {
|
||||||
max-height: 100px;
|
max-height: 100px;
|
||||||
|
@ -51,6 +51,9 @@ class HaBackupConfigAgents extends LitElement {
|
|||||||
|
|
||||||
private _description(agentId: string) {
|
private _description(agentId: string) {
|
||||||
if (agentId === CLOUD_AGENT) {
|
if (agentId === CLOUD_AGENT) {
|
||||||
|
if (this.cloudStatus.logged_in && !this.cloudStatus.active_subscription) {
|
||||||
|
return "You currently do not have an active Home Assistant Cloud subscription.";
|
||||||
|
}
|
||||||
return "Note: It stores only one backup with a maximum size of 5 GB, regardless of your settings.";
|
return "Note: It stores only one backup with a maximum size of 5 GB, regardless of your settings.";
|
||||||
}
|
}
|
||||||
if (isNetworkMountAgent(agentId)) {
|
if (isNetworkMountAgent(agentId)) {
|
||||||
@ -72,6 +75,10 @@ class HaBackupConfigAgents extends LitElement {
|
|||||||
this._agentIds
|
this._agentIds
|
||||||
);
|
);
|
||||||
const description = this._description(agentId);
|
const description = this._description(agentId);
|
||||||
|
const noCloudSubscription =
|
||||||
|
agentId === CLOUD_AGENT &&
|
||||||
|
this.cloudStatus.logged_in &&
|
||||||
|
!this.cloudStatus.active_subscription;
|
||||||
return html`
|
return html`
|
||||||
<ha-md-list-item>
|
<ha-md-list-item>
|
||||||
${isLocalAgent(agentId)
|
${isLocalAgent(agentId)
|
||||||
@ -107,7 +114,9 @@ class HaBackupConfigAgents extends LitElement {
|
|||||||
<ha-switch
|
<ha-switch
|
||||||
slot="end"
|
slot="end"
|
||||||
id=${agentId}
|
id=${agentId}
|
||||||
.checked=${this._value.includes(agentId)}
|
.checked=${!noCloudSubscription &&
|
||||||
|
this._value.includes(agentId)}
|
||||||
|
.disabled=${noCloudSubscription}
|
||||||
@change=${this._agentToggled}
|
@change=${this._agentToggled}
|
||||||
></ha-switch>
|
></ha-switch>
|
||||||
</ha-md-list-item>
|
</ha-md-list-item>
|
||||||
@ -133,7 +142,11 @@ class HaBackupConfigAgents extends LitElement {
|
|||||||
// Ensure we don't have duplicates, agents exist in the list and cloud is logged in
|
// Ensure we don't have duplicates, agents exist in the list and cloud is logged in
|
||||||
this.value = [...new Set(this.value)]
|
this.value = [...new Set(this.value)]
|
||||||
.filter((agent) => this._agentIds.some((id) => id === agent))
|
.filter((agent) => this._agentIds.some((id) => id === agent))
|
||||||
.filter((id) => id !== CLOUD_AGENT || this.cloudStatus.logged_in);
|
.filter(
|
||||||
|
(id) =>
|
||||||
|
id !== CLOUD_AGENT ||
|
||||||
|
(this.cloudStatus.logged_in && this.cloudStatus.active_subscription)
|
||||||
|
);
|
||||||
|
|
||||||
fireEvent(this, "value-changed", { value: this.value });
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
}
|
}
|
||||||
@ -144,9 +157,6 @@ class HaBackupConfigAgents extends LitElement {
|
|||||||
--md-list-item-leading-space: 0;
|
--md-list-item-leading-space: 0;
|
||||||
--md-list-item-trailing-space: 0;
|
--md-list-item-trailing-space: 0;
|
||||||
}
|
}
|
||||||
ha-md-list-item {
|
|
||||||
--md-item-overflow: visible;
|
|
||||||
}
|
|
||||||
ha-md-list-item img {
|
ha-md-list-item img {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
}
|
}
|
||||||
|
@ -138,7 +138,8 @@ class HaBackupConfigData extends LitElement {
|
|||||||
const include_addons = data.addons_mode === "custom" ? data.addons : [];
|
const include_addons = data.addons_mode === "custom" ? data.addons : [];
|
||||||
|
|
||||||
this.value = {
|
this.value = {
|
||||||
include_homeassistant: data.homeassistant || this.forceHomeAssistant,
|
include_homeassistant:
|
||||||
|
data.homeassistant || data.database || this.forceHomeAssistant,
|
||||||
include_addons: include_addons.length ? include_addons : undefined,
|
include_addons: include_addons.length ? include_addons : undefined,
|
||||||
include_all_addons: data.addons_mode === "all",
|
include_all_addons: data.addons_mode === "all",
|
||||||
include_database: data.database,
|
include_database: data.database,
|
||||||
@ -168,7 +169,7 @@ class HaBackupConfigData extends LitElement {
|
|||||||
slot="end"
|
slot="end"
|
||||||
@change=${this._switchChanged}
|
@change=${this._switchChanged}
|
||||||
.checked=${data.homeassistant}
|
.checked=${data.homeassistant}
|
||||||
.disabled=${this.forceHomeAssistant}
|
.disabled=${this.forceHomeAssistant || data.database}
|
||||||
></ha-switch>
|
></ha-switch>
|
||||||
</ha-md-list-item>
|
</ha-md-list-item>
|
||||||
|
|
||||||
@ -296,7 +297,6 @@ class HaBackupConfigData extends LitElement {
|
|||||||
...data,
|
...data,
|
||||||
[target.id]: target.checked,
|
[target.id]: target.checked,
|
||||||
});
|
});
|
||||||
fireEvent(this, "value-changed", { value: this.value });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _selectChanged(ev: Event) {
|
private _selectChanged(ev: Event) {
|
||||||
@ -309,7 +309,6 @@ class HaBackupConfigData extends LitElement {
|
|||||||
if (target.id === "addons_mode") {
|
if (target.id === "addons_mode") {
|
||||||
this._showAddons = target.value === "custom";
|
this._showAddons = target.value === "custom";
|
||||||
}
|
}
|
||||||
fireEvent(this, "value-changed", { value: this.value });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _addonsChanged(ev: CustomEvent) {
|
private _addonsChanged(ev: CustomEvent) {
|
||||||
@ -320,7 +319,6 @@ class HaBackupConfigData extends LitElement {
|
|||||||
...data,
|
...data,
|
||||||
addons,
|
addons,
|
||||||
});
|
});
|
||||||
fireEvent(this, "value-changed", { value: this.value });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
@ -332,9 +330,6 @@ class HaBackupConfigData extends LitElement {
|
|||||||
ha-md-select {
|
ha-md-select {
|
||||||
min-width: 210px;
|
min-width: 210px;
|
||||||
}
|
}
|
||||||
ha-md-list-item {
|
|
||||||
--md-item-overflow: visible;
|
|
||||||
}
|
|
||||||
@media all and (max-width: 450px) {
|
@media all and (max-width: 450px) {
|
||||||
ha-md-select {
|
ha-md-select {
|
||||||
min-width: 160px;
|
min-width: 160px;
|
||||||
|
@ -9,6 +9,7 @@ import { showChangeBackupEncryptionKeyDialog } from "../../dialogs/show-dialog-c
|
|||||||
import { showSetBackupEncryptionKeyDialog } from "../../dialogs/show-dialog-set-backup-encryption-key";
|
import { showSetBackupEncryptionKeyDialog } from "../../dialogs/show-dialog-set-backup-encryption-key";
|
||||||
|
|
||||||
import { downloadEmergencyKit } from "../../../../../data/backup";
|
import { downloadEmergencyKit } from "../../../../../data/backup";
|
||||||
|
import { showShowBackupEncryptionKeyDialog } from "../../dialogs/show-dialog-show-backup-encryption-key";
|
||||||
|
|
||||||
@customElement("ha-backup-config-encryption-key")
|
@customElement("ha-backup-config-encryption-key")
|
||||||
class HaBackupConfigEncryptionKey extends LitElement {
|
class HaBackupConfigEncryptionKey extends LitElement {
|
||||||
@ -34,7 +35,13 @@ class HaBackupConfigEncryptionKey extends LitElement {
|
|||||||
Download
|
Download
|
||||||
</ha-button>
|
</ha-button>
|
||||||
</ha-md-list-item>
|
</ha-md-list-item>
|
||||||
|
<ha-md-list-item>
|
||||||
|
<span slot="headline">Show my encryption key</span>
|
||||||
|
<span slot="supporting-text">
|
||||||
|
Please keep your encryption key private.
|
||||||
|
</span>
|
||||||
|
<ha-button slot="end" @click=${this._show}>Show</ha-button>
|
||||||
|
</ha-md-list-item>
|
||||||
<ha-md-list-item>
|
<ha-md-list-item>
|
||||||
<span slot="headline">Change encryption key</span>
|
<span slot="headline">Change encryption key</span>
|
||||||
<span slot="supporting-text">
|
<span slot="supporting-text">
|
||||||
@ -68,6 +75,10 @@ class HaBackupConfigEncryptionKey extends LitElement {
|
|||||||
downloadEmergencyKit(this.hass, this._value);
|
downloadEmergencyKit(this.hass, this._value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _show() {
|
||||||
|
showShowBackupEncryptionKeyDialog(this, { currentKey: this._value });
|
||||||
|
}
|
||||||
|
|
||||||
private _change() {
|
private _change() {
|
||||||
showChangeBackupEncryptionKeyDialog(this, {
|
showChangeBackupEncryptionKeyDialog(this, {
|
||||||
currentKey: this._value,
|
currentKey: this._value,
|
||||||
|
@ -323,9 +323,6 @@ class HaBackupConfigSchedule extends LitElement {
|
|||||||
ha-md-select {
|
ha-md-select {
|
||||||
min-width: 210px;
|
min-width: 210px;
|
||||||
}
|
}
|
||||||
ha-md-list-item {
|
|
||||||
--md-item-overflow: visible;
|
|
||||||
}
|
|
||||||
@media all and (max-width: 450px) {
|
@media all and (max-width: 450px) {
|
||||||
ha-md-select {
|
ha-md-select {
|
||||||
min-width: 160px;
|
min-width: 160px;
|
||||||
|
@ -37,10 +37,7 @@ class HaBackupOverviewBackups extends LitElement {
|
|||||||
<p>
|
<p>
|
||||||
Backups are essential for a reliable smart home. They help protect
|
Backups are essential for a reliable smart home. They help protect
|
||||||
the work you've put into setting up your smart home, and if the
|
the work you've put into setting up your smart home, and if the
|
||||||
worst happens, you can get back up and running quickly. It is
|
worst happens, you can get back up and running quickly.
|
||||||
recommended that you create a backup every day. You should keep
|
|
||||||
three backups in at least two different locations, one of which
|
|
||||||
should be off-site.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
|
@ -34,7 +34,7 @@ class HaBackupBackupsSummary extends LitElement {
|
|||||||
const { state: schedule } = config.schedule;
|
const { state: schedule } = config.schedule;
|
||||||
|
|
||||||
if (schedule === BackupScheduleState.NEVER) {
|
if (schedule === BackupScheduleState.NEVER) {
|
||||||
return "Automatic backups are disabled";
|
return "Automatic backups are not scheduled";
|
||||||
}
|
}
|
||||||
|
|
||||||
let copiesText = "and keep all backups";
|
let copiesText = "and keep all backups";
|
||||||
@ -116,7 +116,7 @@ class HaBackupBackupsSummary extends LitElement {
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-card class="my-backups">
|
<ha-card class="my-backups">
|
||||||
<div class="card-header">Automatic backups</div>
|
<div class="card-header">Backup settings</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<ha-md-list>
|
<ha-md-list>
|
||||||
<ha-md-list-item
|
<ha-md-list-item
|
||||||
@ -128,7 +128,7 @@ class HaBackupBackupsSummary extends LitElement {
|
|||||||
${this._scheduleDescription(this.config)}
|
${this._scheduleDescription(this.config)}
|
||||||
</div>
|
</div>
|
||||||
<div slot="supporting-text">
|
<div slot="supporting-text">
|
||||||
Schedule and number of backups to keep
|
Automatic backup schedule and retention
|
||||||
</div>
|
</div>
|
||||||
<ha-icon-next slot="end"></ha-icon-next>
|
<ha-icon-next slot="end"></ha-icon-next>
|
||||||
</ha-md-list-item>
|
</ha-md-list-item>
|
||||||
@ -174,7 +174,7 @@ class HaBackupBackupsSummary extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<ha-button @click=${this._configure}>
|
<ha-button @click=${this._configure}>
|
||||||
Configure automatic backups
|
Configure backup settings
|
||||||
</ha-button>
|
</ha-button>
|
||||||
</div>
|
</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
|
@ -84,22 +84,22 @@ class HaBackupOverviewBackups extends LitElement {
|
|||||||
|
|
||||||
const lastSuccessfulBackup = this._lastSuccessfulBackup(this.backups);
|
const lastSuccessfulBackup = this._lastSuccessfulBackup(this.backups);
|
||||||
|
|
||||||
const lastSuccessfulBackupDate = lastSuccessfulBackup
|
|
||||||
? new Date(lastSuccessfulBackup.date)
|
|
||||||
: new Date(0);
|
|
||||||
|
|
||||||
const lastAttempt = this.config.last_attempted_automatic_backup
|
const lastAttempt = this.config.last_attempted_automatic_backup
|
||||||
? new Date(this.config.last_attempted_automatic_backup)
|
? new Date(this.config.last_attempted_automatic_backup)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const lastCompletedBackupDate = this.config.last_completed_automatic_backup
|
||||||
|
? new Date(this.config.last_completed_automatic_backup)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
const lastBackupDescription = lastSuccessfulBackup
|
const lastBackupDescription = lastSuccessfulBackup
|
||||||
? `Last successful backup ${relativeTime(lastSuccessfulBackupDate, this.hass.locale, now, true)} and stored to ${lastSuccessfulBackup.agent_ids?.length} locations.`
|
? `Last successful backup ${relativeTime(new Date(lastSuccessfulBackup.date), this.hass.locale, now, true)} and stored in ${lastSuccessfulBackup.agent_ids?.length} locations.`
|
||||||
: "You have no successful backups.";
|
: "You have no successful backups.";
|
||||||
|
|
||||||
if (lastAttempt && lastAttempt > lastSuccessfulBackupDate) {
|
if (lastAttempt && lastAttempt > (lastCompletedBackupDate || 0)) {
|
||||||
const lastAttemptDescription = `The last automatic backup trigged ${relativeTime(lastAttempt, this.hass.locale, now, true)} wasn't successful.`;
|
const lastAttemptDescription = `The last automatic backup triggered ${relativeTime(lastAttempt, this.hass.locale, now, true)} wasn't successful.`;
|
||||||
return html`
|
return html`
|
||||||
<ha-backup-summary-card
|
<ha-backup-summary-card
|
||||||
heading="Last automatic backup failed"
|
heading="Last automatic backup failed"
|
||||||
@ -119,6 +119,10 @@ class HaBackupOverviewBackups extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nextBackupDescription = this._nextBackupDescription(
|
||||||
|
this.config.schedule.state
|
||||||
|
);
|
||||||
|
|
||||||
if (!lastSuccessfulBackup) {
|
if (!lastSuccessfulBackup) {
|
||||||
return html`
|
return html`
|
||||||
<ha-backup-summary-card
|
<ha-backup-summary-card
|
||||||
@ -126,18 +130,20 @@ class HaBackupOverviewBackups extends LitElement {
|
|||||||
description="You have no automatic backups yet."
|
description="You have no automatic backups yet."
|
||||||
status="warning"
|
status="warning"
|
||||||
>
|
>
|
||||||
|
<ha-md-list>
|
||||||
|
<ha-md-list-item>
|
||||||
|
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
|
||||||
|
<span slot="headline">${nextBackupDescription}</span>
|
||||||
|
</ha-md-list-item>
|
||||||
|
</ha-md-list>
|
||||||
</ha-backup-summary-card>
|
</ha-backup-summary-card>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextBackupDescription = this._nextBackupDescription(
|
|
||||||
this.config.schedule.state
|
|
||||||
);
|
|
||||||
|
|
||||||
const numberOfDays = differenceInDays(
|
const numberOfDays = differenceInDays(
|
||||||
// Subtract a few hours to avoid showing as overdue if it's just a few hours (e.g. daylight saving)
|
// Subtract a few hours to avoid showing as overdue if it's just a few hours (e.g. daylight saving)
|
||||||
addHours(now, -OVERDUE_MARGIN_HOURS),
|
addHours(now, -OVERDUE_MARGIN_HOURS),
|
||||||
lastSuccessfulBackupDate
|
new Date(lastSuccessfulBackup.date)
|
||||||
);
|
);
|
||||||
|
|
||||||
const isOverdue =
|
const isOverdue =
|
||||||
|
@ -90,7 +90,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
|
|||||||
|
|
||||||
public showDialog(params: BackupOnboardingDialogParams): void {
|
public showDialog(params: BackupOnboardingDialogParams): void {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
this._step = STEPS[0];
|
this._step = this._firstStep;
|
||||||
this._config = RECOMMENDED_CONFIG;
|
this._config = RECOMMENDED_CONFIG;
|
||||||
|
|
||||||
const agents: string[] = [];
|
const agents: string[] = [];
|
||||||
@ -129,6 +129,10 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
|
|||||||
this._params = undefined;
|
this._params = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get _firstStep(): Step {
|
||||||
|
return this._params?.skipWelcome ? STEPS[1] : STEPS[0];
|
||||||
|
}
|
||||||
|
|
||||||
private async _done() {
|
private async _done() {
|
||||||
if (!this._config) {
|
if (!this._config) {
|
||||||
return;
|
return;
|
||||||
@ -187,7 +191,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isLastStep = this._step === STEPS[STEPS.length - 1];
|
const isLastStep = this._step === STEPS[STEPS.length - 1];
|
||||||
const isFirstStep = this._step === STEPS[0];
|
const isFirstStep = this._step === this._firstStep;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-md-dialog disable-cancel-action open @closed=${this.closeDialog}>
|
<ha-md-dialog disable-cancel-action open @closed=${this.closeDialog}>
|
||||||
@ -396,7 +400,10 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _copyKeyToClipboard() {
|
private async _copyKeyToClipboard() {
|
||||||
await copyToClipboard(this._config!.create_backup.password!);
|
await copyToClipboard(
|
||||||
|
this._config!.create_backup.password!,
|
||||||
|
this.renderRoot.querySelector("div")!
|
||||||
|
);
|
||||||
showToast(this, {
|
showToast(this, {
|
||||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||||
});
|
});
|
||||||
|
@ -92,7 +92,9 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
|
|||||||
? "Save current encryption key"
|
? "Save current encryption key"
|
||||||
: this._step === "new"
|
: this._step === "new"
|
||||||
? "New encryption key"
|
? "New encryption key"
|
||||||
: "";
|
: this._step === "done"
|
||||||
|
? "Save new encryption key"
|
||||||
|
: "";
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-md-dialog disable-cancel-action open @closed=${this.closeDialog}>
|
<ha-md-dialog disable-cancel-action open @closed=${this.closeDialog}>
|
||||||
@ -166,10 +168,22 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
|
|||||||
case "new":
|
case "new":
|
||||||
return html`
|
return html`
|
||||||
<p>
|
<p>
|
||||||
Keep this encryption key in a safe place, as you will need it to
|
All next backups will use the new encryption key. Encryption keeps
|
||||||
access your backup, allowing it to be restored. Either record the
|
your backups private and secure.
|
||||||
|
</p>
|
||||||
|
<div class="encryption-key">
|
||||||
|
<p>${this._newEncryptionKey}</p>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiContentCopy}
|
||||||
|
@click=${this._copyKeyToClipboard}
|
||||||
|
></ha-icon-button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
case "done":
|
||||||
|
return html`<p>
|
||||||
|
Keep this new encryption key in a safe place, as you will need it to
|
||||||
|
access your backups, allowing it to be restored. Either record the
|
||||||
characters below or download them as an emergency kit file.
|
characters below or download them as an emergency kit file.
|
||||||
Encryption keeps your backups private and secure.
|
|
||||||
</p>
|
</p>
|
||||||
<div class="encryption-key">
|
<div class="encryption-key">
|
||||||
<p>${this._newEncryptionKey}</p>
|
<p>${this._newEncryptionKey}</p>
|
||||||
@ -189,24 +203,16 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
|
|||||||
Download
|
Download
|
||||||
</ha-button>
|
</ha-button>
|
||||||
</ha-md-list-item>
|
</ha-md-list-item>
|
||||||
</ha-md-list>
|
</ha-md-list>`;
|
||||||
`;
|
|
||||||
case "done":
|
|
||||||
return html`
|
|
||||||
<div class="done">
|
|
||||||
<img
|
|
||||||
src="/static/images/voice-assistant/hi.png"
|
|
||||||
alt="Casita Home Assistant logo"
|
|
||||||
/>
|
|
||||||
<p>Encryption key changed</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _copyKeyToClipboard() {
|
private async _copyKeyToClipboard() {
|
||||||
await copyToClipboard(this._newEncryptionKey);
|
await copyToClipboard(
|
||||||
|
this._newEncryptionKey,
|
||||||
|
this.renderRoot.querySelector("div")!
|
||||||
|
);
|
||||||
showToast(this, {
|
showToast(this, {
|
||||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||||
});
|
});
|
||||||
@ -216,7 +222,10 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
|
|||||||
if (!this._params?.currentKey) {
|
if (!this._params?.currentKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await copyToClipboard(this._params.currentKey);
|
await copyToClipboard(
|
||||||
|
this._params.currentKey,
|
||||||
|
this.renderRoot.querySelector("div")!
|
||||||
|
);
|
||||||
showToast(this, {
|
showToast(this, {
|
||||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||||
});
|
});
|
||||||
@ -297,13 +306,6 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
|
|||||||
p {
|
p {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
.done {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 22px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 28px;
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -100,7 +100,10 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
|
|||||||
this._agentIds = agents
|
this._agentIds = agents
|
||||||
.map((agent) => agent.agent_id)
|
.map((agent) => agent.agent_id)
|
||||||
.filter(
|
.filter(
|
||||||
(id) => id !== CLOUD_AGENT || this._params?.cloudStatus?.logged_in
|
(id) =>
|
||||||
|
id !== CLOUD_AGENT ||
|
||||||
|
(this._params?.cloudStatus?.logged_in &&
|
||||||
|
this._params?.cloudStatus?.active_subscription)
|
||||||
)
|
)
|
||||||
.sort(compareAgents);
|
.sort(compareAgents);
|
||||||
}
|
}
|
||||||
@ -200,17 +203,36 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
|
|||||||
? html`
|
? html`
|
||||||
<ha-button
|
<ha-button
|
||||||
@click=${this._submit}
|
@click=${this._submit}
|
||||||
.disabled=${!selectedAgents.length}
|
.disabled=${this._formData.agents_mode === "custom" &&
|
||||||
|
!selectedAgents.length}
|
||||||
>
|
>
|
||||||
Create backup
|
Create backup
|
||||||
</ha-button>
|
</ha-button>
|
||||||
`
|
`
|
||||||
: html`<ha-button @click=${this._nextStep}>Next</ha-button>`}
|
: html`<ha-button
|
||||||
|
@click=${this._nextStep}
|
||||||
|
.disabled=${this._step === "data" && this._noDataSelected}
|
||||||
|
>Next</ha-button
|
||||||
|
>`}
|
||||||
</div>
|
</div>
|
||||||
</ha-md-dialog>
|
</ha-md-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get _noDataSelected() {
|
||||||
|
const hassio = isComponentLoaded(this.hass, "hassio");
|
||||||
|
if (
|
||||||
|
this._formData?.data.include_homeassistant ||
|
||||||
|
this._formData?.data.include_database ||
|
||||||
|
(hassio && this._formData?.data.include_folders?.length) ||
|
||||||
|
(hassio && this._formData?.data.include_all_addons) ||
|
||||||
|
(hassio && this._formData?.data.include_addons?.length)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private _renderData() {
|
private _renderData() {
|
||||||
if (!this._formData) {
|
if (!this._formData) {
|
||||||
return nothing;
|
return nothing;
|
||||||
|
@ -23,7 +23,10 @@ import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
|||||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import type { RestoreBackupDialogParams } from "./show-dialog-restore-backup";
|
import type { RestoreBackupDialogParams } from "./show-dialog-restore-backup";
|
||||||
import type { RestoreBackupStage } from "../../../../data/backup_manager";
|
import type {
|
||||||
|
RestoreBackupStage,
|
||||||
|
RestoreBackupState,
|
||||||
|
} from "../../../../data/backup_manager";
|
||||||
import { subscribeBackupEvents } from "../../../../data/backup_manager";
|
import { subscribeBackupEvents } from "../../../../data/backup_manager";
|
||||||
|
|
||||||
type FormData = {
|
type FormData = {
|
||||||
@ -52,8 +55,12 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
|||||||
|
|
||||||
@state() private _userPassword?: string;
|
@state() private _userPassword?: string;
|
||||||
|
|
||||||
|
@state() private _usedUserInput = false;
|
||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
|
@state() private _state?: RestoreBackupState;
|
||||||
|
|
||||||
@state() private _stage?: RestoreBackupStage | null;
|
@state() private _stage?: RestoreBackupStage | null;
|
||||||
|
|
||||||
@state() private _unsub?: Promise<UnsubscribeFunc>;
|
@state() private _unsub?: Promise<UnsubscribeFunc>;
|
||||||
@ -64,6 +71,11 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
|||||||
this._params = params;
|
this._params = params;
|
||||||
|
|
||||||
this._formData = INITIAL_DATA;
|
this._formData = INITIAL_DATA;
|
||||||
|
this._userPassword = undefined;
|
||||||
|
this._usedUserInput = false;
|
||||||
|
this._error = undefined;
|
||||||
|
this._state = undefined;
|
||||||
|
this._stage = undefined;
|
||||||
if (this._params.backup.protected) {
|
if (this._params.backup.protected) {
|
||||||
this._backupEncryptionKey = await this._fetchEncryptionKey();
|
this._backupEncryptionKey = await this._fetchEncryptionKey();
|
||||||
if (!this._backupEncryptionKey) {
|
if (!this._backupEncryptionKey) {
|
||||||
@ -85,7 +97,9 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
|||||||
this._params = undefined;
|
this._params = undefined;
|
||||||
this._backupEncryptionKey = undefined;
|
this._backupEncryptionKey = undefined;
|
||||||
this._userPassword = undefined;
|
this._userPassword = undefined;
|
||||||
|
this._usedUserInput = false;
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
|
this._state = undefined;
|
||||||
this._stage = undefined;
|
this._stage = undefined;
|
||||||
this._step = undefined;
|
this._step = undefined;
|
||||||
this._unsubscribe();
|
this._unsubscribe();
|
||||||
@ -149,15 +163,24 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _renderEncryption() {
|
private _renderEncryption() {
|
||||||
return html`<p>
|
return html`${this._usedUserInput
|
||||||
${this._userPassword
|
? "The provided encryption key was incorrect, please try again."
|
||||||
? "The provided encryption key was incorrect, please try again."
|
: this._backupEncryptionKey
|
||||||
: this._backupEncryptionKey
|
? html`The Backup is encrypted with a different encryption key than
|
||||||
? "The backup is encrypted with a different key or password than that is saved on this system. Please enter the key for this backup."
|
that is saved on this system. Please enter the encryption key for
|
||||||
: "The backup is encrypted. Provide the encryption key to decrypt the backup."}
|
this backup.<br />
|
||||||
</p>
|
${this._params!.selectedData.homeassistant_included
|
||||||
|
? html`<ha-alert alert-type="warning"
|
||||||
|
>After restoring the backup, your new backups will be
|
||||||
|
encrypted with the encryption key that was present during
|
||||||
|
the time of this backup.</ha-alert
|
||||||
|
>`
|
||||||
|
: nothing}`
|
||||||
|
: "The backup is encrypted. Provide the encryption key to decrypt the backup."}
|
||||||
|
|
||||||
<ha-password-field
|
<ha-password-field
|
||||||
@change=${this._passwordChanged}
|
@input=${this._passwordChanged}
|
||||||
|
label="Encryption key"
|
||||||
.value=${this._userPassword || ""}
|
.value=${this._userPassword || ""}
|
||||||
></ha-password-field>`;
|
></ha-password-field>`;
|
||||||
}
|
}
|
||||||
@ -186,16 +209,22 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
|||||||
|
|
||||||
private async _restoreBackup() {
|
private async _restoreBackup() {
|
||||||
this._unsubscribe();
|
this._unsubscribe();
|
||||||
|
this._state = undefined;
|
||||||
|
this._stage = undefined;
|
||||||
|
this._error = undefined;
|
||||||
try {
|
try {
|
||||||
this._step = "progress";
|
this._step = "progress";
|
||||||
window.addEventListener("connection-status", this._connectionStatus);
|
|
||||||
this._subscribeBackupEvents();
|
this._subscribeBackupEvents();
|
||||||
await this._doRestoreBackup(
|
await this._doRestoreBackup(
|
||||||
this._userPassword || this._backupEncryptionKey
|
this._userPassword || this._backupEncryptionKey
|
||||||
);
|
);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this._unsubscribe();
|
await this._unsubscribe();
|
||||||
if (e.code === "password_incorrect") {
|
if (e.code === "password_incorrect") {
|
||||||
|
this._error = undefined;
|
||||||
|
if (this._userPassword) {
|
||||||
|
this._usedUserInput = true;
|
||||||
|
}
|
||||||
this._step = "encryption";
|
this._step = "encryption";
|
||||||
} else {
|
} else {
|
||||||
this._error = e.message;
|
this._error = e.message;
|
||||||
@ -203,17 +232,15 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _connectionStatus = (ev) => {
|
|
||||||
if (ev.detail === "connected") {
|
|
||||||
this.closeDialog();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private _subscribeBackupEvents() {
|
private _subscribeBackupEvents() {
|
||||||
this._unsub = subscribeBackupEvents(this.hass!, (event) => {
|
this._unsub = subscribeBackupEvents(this.hass!, (event) => {
|
||||||
|
if (event.manager_state === "idle" && this._state === "in_progress") {
|
||||||
|
this.closeDialog();
|
||||||
|
}
|
||||||
if (event.manager_state !== "restore_backup") {
|
if (event.manager_state !== "restore_backup") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this._state = event.state;
|
||||||
if (event.state === "completed") {
|
if (event.state === "completed") {
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
}
|
}
|
||||||
@ -227,11 +254,12 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _unsubscribe() {
|
private _unsubscribe() {
|
||||||
window.removeEventListener("connection-status", this._connectionStatus);
|
|
||||||
if (this._unsub) {
|
if (this._unsub) {
|
||||||
this._unsub.then((unsub) => unsub());
|
const prom = this._unsub.then((unsub) => unsub());
|
||||||
this._unsub = undefined;
|
this._unsub = undefined;
|
||||||
|
return prom;
|
||||||
}
|
}
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _restoreState() {
|
private _restoreState() {
|
||||||
@ -306,6 +334,14 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
|||||||
ha-circular-progress {
|
ha-circular-progress {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
ha-alert[alert-type="warning"] {
|
||||||
|
display: block;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
ha-password-field {
|
||||||
|
display: block;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,174 @@
|
|||||||
|
import { mdiClose, mdiContentCopy, mdiDownload } 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 { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||||
|
import "../../../../components/ha-button";
|
||||||
|
import "../../../../components/ha-dialog-header";
|
||||||
|
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-list";
|
||||||
|
import "../../../../components/ha-md-list-item";
|
||||||
|
import "../../../../components/ha-password-field";
|
||||||
|
import { downloadEmergencyKit } from "../../../../data/backup";
|
||||||
|
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||||
|
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import { showToast } from "../../../../util/toast";
|
||||||
|
import type { ShowBackupEncryptionKeyDialogParams } from "./show-dialog-show-backup-encryption-key";
|
||||||
|
|
||||||
|
@customElement("ha-dialog-show-backup-encryption-key")
|
||||||
|
class DialogShowBackupEncryptionKey extends LitElement implements HassDialog {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _params?: ShowBackupEncryptionKeyDialogParams;
|
||||||
|
|
||||||
|
@query("ha-md-dialog") private _dialog!: HaMdDialog;
|
||||||
|
|
||||||
|
public showDialog(params: ShowBackupEncryptionKeyDialogParams): void {
|
||||||
|
this._params = params;
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog(): void {
|
||||||
|
this._params = undefined;
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _closeDialog() {
|
||||||
|
this._dialog.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this._params) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-md-dialog disable-cancel-action open @closed=${this.closeDialog}>
|
||||||
|
<ha-dialog-header slot="headline">
|
||||||
|
<ha-icon-button
|
||||||
|
slot="navigationIcon"
|
||||||
|
.label=${this.hass.localize("ui.dialogs.generic.close")}
|
||||||
|
.path=${mdiClose}
|
||||||
|
@click=${this._closeDialog}
|
||||||
|
></ha-icon-button>
|
||||||
|
<span slot="title">Encryption key</span>
|
||||||
|
</ha-dialog-header>
|
||||||
|
<div slot="content">
|
||||||
|
<p>
|
||||||
|
Make sure you save the encryption key in a secure place so always
|
||||||
|
have access to your backups.
|
||||||
|
</p>
|
||||||
|
<div class="encryption-key">
|
||||||
|
<p>${this._params?.currentKey}</p>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiContentCopy}
|
||||||
|
@click=${this._copyKeyToClipboard}
|
||||||
|
></ha-icon-button>
|
||||||
|
</div>
|
||||||
|
<ha-md-list>
|
||||||
|
<ha-md-list-item>
|
||||||
|
<span slot="headline">Download emergency kit</span>
|
||||||
|
<span slot="supporting-text">
|
||||||
|
We recommend saving this encryption key file somewhere secure.
|
||||||
|
</span>
|
||||||
|
<ha-button slot="end" @click=${this._download}>
|
||||||
|
<ha-svg-icon .path=${mdiDownload} slot="icon"></ha-svg-icon>
|
||||||
|
Download
|
||||||
|
</ha-button>
|
||||||
|
</ha-md-list-item>
|
||||||
|
</ha-md-list>
|
||||||
|
</div>
|
||||||
|
<div slot="actions">
|
||||||
|
<ha-button @click=${this._closeDialog}>Close</ha-button>
|
||||||
|
</div>
|
||||||
|
</ha-md-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _copyKeyToClipboard() {
|
||||||
|
if (!this._params?.currentKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await copyToClipboard(
|
||||||
|
this._params?.currentKey,
|
||||||
|
this.renderRoot.querySelector("div")!
|
||||||
|
);
|
||||||
|
showToast(this, {
|
||||||
|
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _download() {
|
||||||
|
if (!this._params?.currentKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
downloadEmergencyKit(this.hass, this._params.currentKey, "old");
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
haStyleDialog,
|
||||||
|
css`
|
||||||
|
ha-md-dialog {
|
||||||
|
width: 90vw;
|
||||||
|
max-width: 560px;
|
||||||
|
--dialog-content-padding: 8px 24px;
|
||||||
|
}
|
||||||
|
ha-md-list {
|
||||||
|
background: none;
|
||||||
|
--md-list-item-leading-space: 0;
|
||||||
|
--md-list-item-trailing-space: 0;
|
||||||
|
}
|
||||||
|
ha-button.danger {
|
||||||
|
--mdc-theme-primary: var(--error-color);
|
||||||
|
}
|
||||||
|
.encryption-key {
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
background-color: var(--primary-background-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
.encryption-key p {
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
font-family: "Roboto Mono", "Consolas", "Menlo", monospace;
|
||||||
|
font-size: 20px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 28px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.encryption-key ha-icon-button {
|
||||||
|
flex: none;
|
||||||
|
margin: -16px;
|
||||||
|
}
|
||||||
|
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||||
|
ha-md-dialog {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
div[slot="content"] {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-dialog-show-backup-encryption-key": DialogShowBackupEncryptionKey;
|
||||||
|
}
|
||||||
|
}
|
@ -20,7 +20,6 @@ import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
|||||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import { showAlertDialog } from "../../../lovelace/custom-card-helpers";
|
import { showAlertDialog } from "../../../lovelace/custom-card-helpers";
|
||||||
import "../components/ha-backup-agents-picker";
|
|
||||||
import type { UploadBackupDialogParams } from "./show-dialog-upload-backup";
|
import type { UploadBackupDialogParams } from "./show-dialog-upload-backup";
|
||||||
|
|
||||||
const SUPPORTED_FORMAT = "application/x-tar";
|
const SUPPORTED_FORMAT = "application/x-tar";
|
||||||
@ -90,6 +89,9 @@ export class DialogUploadBackup
|
|||||||
<span slot="title">Upload backup</span>
|
<span slot="title">Upload backup</span>
|
||||||
</ha-dialog-header>
|
</ha-dialog-header>
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
|
: nothing}
|
||||||
<ha-file-upload
|
<ha-file-upload
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.uploading=${this._uploading}
|
.uploading=${this._uploading}
|
||||||
@ -99,9 +101,6 @@ export class DialogUploadBackup
|
|||||||
supports="Supports .tar files"
|
supports="Supports .tar files"
|
||||||
@file-picked=${this._filePicked}
|
@file-picked=${this._filePicked}
|
||||||
></ha-file-upload>
|
></ha-file-upload>
|
||||||
${this._error
|
|
||||||
? html`<ha-alert alertType="error">${this._error}</ha-alert>`
|
|
||||||
: nothing}
|
|
||||||
</div>
|
</div>
|
||||||
<div slot="actions">
|
<div slot="actions">
|
||||||
<ha-button @click=${this.closeDialog}>Cancel</ha-button>
|
<ha-button @click=${this.closeDialog}>Cancel</ha-button>
|
||||||
@ -161,6 +160,10 @@ export class DialogUploadBackup
|
|||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
|
ha-alert {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ export interface BackupOnboardingDialogParams {
|
|||||||
submit?: (value: boolean) => void;
|
submit?: (value: boolean) => void;
|
||||||
cancel?: () => void;
|
cancel?: () => void;
|
||||||
cloudStatus?: CloudStatus;
|
cloudStatus?: CloudStatus;
|
||||||
|
skipWelcome?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadDialog = () => import("./dialog-backup-onboarding");
|
const loadDialog = () => import("./dialog-backup-onboarding");
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
|
||||||
|
export interface ShowBackupEncryptionKeyDialogParams {
|
||||||
|
currentKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadDialog = () => import("./dialog-show-backup-encryption-key");
|
||||||
|
|
||||||
|
export const showShowBackupEncryptionKeyDialog = (
|
||||||
|
element: HTMLElement,
|
||||||
|
params?: ShowBackupEncryptionKeyDialogParams
|
||||||
|
) =>
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "ha-dialog-show-backup-encryption-key",
|
||||||
|
dialogImport: loadDialog,
|
||||||
|
dialogParams: params,
|
||||||
|
});
|
@ -301,10 +301,10 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<hass-tabs-subpage-data-table
|
<hass-tabs-subpage-data-table
|
||||||
hasFab
|
has-fab
|
||||||
.tabs=${[
|
.tabs=${[
|
||||||
{
|
{
|
||||||
translationKey: "ui.panel.config.backup.caption",
|
name: "My backups",
|
||||||
path: `/config/backup/list`,
|
path: `/config/backup/list`,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
@ -143,6 +143,14 @@ class HaConfigBackupDetails extends LitElement {
|
|||||||
)}
|
)}
|
||||||
<span slot="supporting-text">Created</span>
|
<span slot="supporting-text">Created</span>
|
||||||
</ha-md-list-item>
|
</ha-md-list-item>
|
||||||
|
<ha-md-list-item>
|
||||||
|
<span slot="headline">
|
||||||
|
${this._backup.protected
|
||||||
|
? "Encrypted AES-128"
|
||||||
|
: "Not encrypted"}
|
||||||
|
</span>
|
||||||
|
<span slot="supporting-text">Protected</span>
|
||||||
|
</ha-md-list-item>
|
||||||
</ha-md-list>
|
</ha-md-list>
|
||||||
</div>
|
</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { mdiDotsVertical, mdiHarddisk, mdiPlus, mdiUpload } from "@mdi/js";
|
import { mdiDotsVertical, mdiPlus, mdiUpload } from "@mdi/js";
|
||||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
|
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
|
||||||
import "../../../components/ha-button";
|
import "../../../components/ha-button";
|
||||||
@ -33,7 +32,6 @@ import "./components/overview/ha-backup-overview-settings";
|
|||||||
import "./components/overview/ha-backup-overview-summary";
|
import "./components/overview/ha-backup-overview-summary";
|
||||||
import { showBackupOnboardingDialog } from "./dialogs/show-dialog-backup_onboarding";
|
import { showBackupOnboardingDialog } from "./dialogs/show-dialog-backup_onboarding";
|
||||||
import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup";
|
import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup";
|
||||||
import { showLocalBackupLocationDialog } from "./dialogs/show-dialog-local-backup-location";
|
|
||||||
import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup";
|
import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup";
|
||||||
import { showUploadBackupDialog } from "./dialogs/show-dialog-upload-backup";
|
import { showUploadBackupDialog } from "./dialogs/show-dialog-upload-backup";
|
||||||
|
|
||||||
@ -63,22 +61,15 @@ class HaConfigBackupOverview extends LitElement {
|
|||||||
await showUploadBackupDialog(this, {});
|
await showUploadBackupDialog(this, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _changeLocalLocation(ev) {
|
|
||||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showLocalBackupLocationDialog(this, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleOnboardingButtonClick(ev) {
|
private _handleOnboardingButtonClick(ev) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this._setupAutomaticBackup();
|
this._setupAutomaticBackup(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _setupAutomaticBackup() {
|
private async _setupAutomaticBackup(skipWelcome = false) {
|
||||||
const success = await showBackupOnboardingDialog(this, {
|
const success = await showBackupOnboardingDialog(this, {
|
||||||
cloudStatus: this.cloudStatus,
|
cloudStatus: this.cloudStatus,
|
||||||
|
skipWelcome,
|
||||||
});
|
});
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return;
|
return;
|
||||||
@ -127,15 +118,13 @@ class HaConfigBackupOverview extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private get _needsOnboarding() {
|
private get _needsOnboarding() {
|
||||||
return !this.config?.create_backup.password;
|
return this.config && !this.config.create_backup.password;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const backupInProgress =
|
const backupInProgress =
|
||||||
"state" in this.manager && this.manager.state === "in_progress";
|
"state" in this.manager && this.manager.state === "in_progress";
|
||||||
|
|
||||||
const isHassio = isComponentLoaded(this.hass, "hassio");
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<hass-subpage
|
<hass-subpage
|
||||||
back-path="/config/system"
|
back-path="/config/system"
|
||||||
@ -143,34 +132,17 @@ class HaConfigBackupOverview extends LitElement {
|
|||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
.header=${"Backup"}
|
.header=${"Backup"}
|
||||||
>
|
>
|
||||||
<div slot="toolbar-icon">
|
<ha-button-menu slot="toolbar-icon">
|
||||||
<ha-button-menu>
|
<ha-icon-button
|
||||||
<ha-icon-button
|
slot="trigger"
|
||||||
slot="trigger"
|
.label=${this.hass.localize("ui.common.menu")}
|
||||||
.label=${this.hass.localize("ui.common.menu")}
|
.path=${mdiDotsVertical}
|
||||||
.path=${mdiDotsVertical}
|
></ha-icon-button>
|
||||||
></ha-icon-button>
|
<ha-list-item graphic="icon" @request-selected=${this._uploadBackup}>
|
||||||
${isHassio
|
<ha-svg-icon slot="graphic" .path=${mdiUpload}></ha-svg-icon>
|
||||||
? html`<ha-list-item
|
Upload backup
|
||||||
graphic="icon"
|
</ha-list-item>
|
||||||
@request-selected=${this._changeLocalLocation}
|
</ha-button-menu>
|
||||||
>
|
|
||||||
<ha-svg-icon
|
|
||||||
slot="graphic"
|
|
||||||
.path=${mdiHarddisk}
|
|
||||||
></ha-svg-icon>
|
|
||||||
Change local location
|
|
||||||
</ha-list-item>`
|
|
||||||
: nothing}
|
|
||||||
<ha-list-item
|
|
||||||
graphic="icon"
|
|
||||||
@request-selected=${this._uploadBackup}
|
|
||||||
>
|
|
||||||
<ha-svg-icon slot="graphic" .path=${mdiUpload}></ha-svg-icon>
|
|
||||||
Upload backup
|
|
||||||
</ha-list-item>
|
|
||||||
</ha-button-menu>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
${backupInProgress
|
${backupInProgress
|
||||||
? html`
|
? html`
|
||||||
@ -188,22 +160,24 @@ class HaConfigBackupOverview extends LitElement {
|
|||||||
>
|
>
|
||||||
</ha-backup-overview-onboarding>
|
</ha-backup-overview-onboarding>
|
||||||
`
|
`
|
||||||
: html`
|
: this.config
|
||||||
<ha-backup-overview-summary
|
? html`
|
||||||
.hass=${this.hass}
|
<ha-backup-overview-summary
|
||||||
.backups=${this.backups}
|
.hass=${this.hass}
|
||||||
.config=${this.config}
|
.backups=${this.backups}
|
||||||
.fetching=${this.fetching}
|
.config=${this.config}
|
||||||
>
|
.fetching=${this.fetching}
|
||||||
</ha-backup-overview-summary>
|
>
|
||||||
`}
|
</ha-backup-overview-summary>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
|
||||||
<ha-backup-overview-backups
|
<ha-backup-overview-backups
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.backups=${this.backups}
|
.backups=${this.backups}
|
||||||
></ha-backup-overview-backups>
|
></ha-backup-overview-backups>
|
||||||
|
|
||||||
${!this._needsOnboarding
|
${!this._needsOnboarding && this.config
|
||||||
? html`
|
? html`
|
||||||
<ha-backup-overview-settings
|
<ha-backup-overview-settings
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
@ -1,14 +1,21 @@
|
|||||||
|
import { mdiDotsVertical, mdiHarddisk } from "@mdi/js";
|
||||||
import type { PropertyValues } from "lit";
|
import type { PropertyValues } from "lit";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
|
||||||
import { debounce } from "../../../common/util/debounce";
|
import { debounce } from "../../../common/util/debounce";
|
||||||
import { nextRender } from "../../../common/util/render-status";
|
import { nextRender } from "../../../common/util/render-status";
|
||||||
import "../../../components/ha-button";
|
import "../../../components/ha-button";
|
||||||
|
import "../../../components/ha-button-menu";
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
|
import "../../../components/ha-icon-button";
|
||||||
import "../../../components/ha-icon-next";
|
import "../../../components/ha-icon-next";
|
||||||
|
import "../../../components/ha-list-item";
|
||||||
|
import "../../../components/ha-alert";
|
||||||
import "../../../components/ha-password-field";
|
import "../../../components/ha-password-field";
|
||||||
|
import "../../../components/ha-svg-icon";
|
||||||
import type { BackupConfig } from "../../../data/backup";
|
import type { BackupConfig } from "../../../data/backup";
|
||||||
import { updateBackupConfig } from "../../../data/backup";
|
import { updateBackupConfig } from "../../../data/backup";
|
||||||
import type { CloudStatus } from "../../../data/cloud";
|
import type { CloudStatus } from "../../../data/cloud";
|
||||||
@ -20,6 +27,7 @@ import type { BackupConfigData } from "./components/config/ha-backup-config-data
|
|||||||
import "./components/config/ha-backup-config-encryption-key";
|
import "./components/config/ha-backup-config-encryption-key";
|
||||||
import "./components/config/ha-backup-config-schedule";
|
import "./components/config/ha-backup-config-schedule";
|
||||||
import type { BackupConfigSchedule } from "./components/config/ha-backup-config-schedule";
|
import type { BackupConfigSchedule } from "./components/config/ha-backup-config-schedule";
|
||||||
|
import { showLocalBackupLocationDialog } from "./dialogs/show-dialog-local-backup-location";
|
||||||
|
|
||||||
@customElement("ha-config-backup-settings")
|
@customElement("ha-config-backup-settings")
|
||||||
class HaConfigBackupSettings extends LitElement {
|
class HaConfigBackupSettings extends LitElement {
|
||||||
@ -91,8 +99,30 @@ class HaConfigBackupSettings extends LitElement {
|
|||||||
back-path="/config/backup"
|
back-path="/config/backup"
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
.header=${"Automatic backups"}
|
.header=${"Backup settings"}
|
||||||
>
|
>
|
||||||
|
${isComponentLoaded(this.hass, "hassio")
|
||||||
|
? html`
|
||||||
|
<ha-button-menu slot="toolbar-icon">
|
||||||
|
<ha-icon-button
|
||||||
|
slot="trigger"
|
||||||
|
.label=${this.hass.localize("ui.common.menu")}
|
||||||
|
.path=${mdiDotsVertical}
|
||||||
|
></ha-icon-button>
|
||||||
|
<ha-list-item
|
||||||
|
graphic="icon"
|
||||||
|
@request-selected=${this._changeLocalLocation}
|
||||||
|
>
|
||||||
|
<ha-svg-icon
|
||||||
|
slot="graphic"
|
||||||
|
.path=${mdiHarddisk}
|
||||||
|
></ha-svg-icon>
|
||||||
|
Change default action location
|
||||||
|
</ha-list-item>
|
||||||
|
</ha-button-menu>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<ha-card id="schedule">
|
<ha-card id="schedule">
|
||||||
<div class="card-header">Automatic backups</div>
|
<div class="card-header">Automatic backups</div>
|
||||||
@ -134,6 +164,14 @@ class HaConfigBackupSettings extends LitElement {
|
|||||||
.cloudStatus=${this.cloudStatus}
|
.cloudStatus=${this.cloudStatus}
|
||||||
@value-changed=${this._agentsConfigChanged}
|
@value-changed=${this._agentsConfigChanged}
|
||||||
></ha-backup-config-agents>
|
></ha-backup-config-agents>
|
||||||
|
${!this._config.create_backup.agent_ids.length
|
||||||
|
? html`<ha-alert
|
||||||
|
alert-type="warning"
|
||||||
|
title="No location selected"
|
||||||
|
>You have to select at least one location to create a
|
||||||
|
backup.</ha-alert
|
||||||
|
><br />`
|
||||||
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
<ha-card>
|
<ha-card>
|
||||||
@ -157,6 +195,14 @@ class HaConfigBackupSettings extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _changeLocalLocation(ev) {
|
||||||
|
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLocalBackupLocationDialog(this, {});
|
||||||
|
}
|
||||||
|
|
||||||
private _scheduleConfigChanged(ev) {
|
private _scheduleConfigChanged(ev) {
|
||||||
const value = ev.detail.value as BackupConfigSchedule;
|
const value = ev.detail.value as BackupConfigSchedule;
|
||||||
this._config = {
|
this._config = {
|
||||||
|
@ -33,7 +33,7 @@ declare global {
|
|||||||
class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
|
class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public cloudStatus!: CloudStatus;
|
@property({ attribute: false }) public cloudStatus?: CloudStatus;
|
||||||
|
|
||||||
@property({ type: Boolean }) public narrow = false;
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ import { brandsUrl } from "../../../util/brands-url";
|
|||||||
import type { Helper, HelperDomain } from "./const";
|
import type { Helper, HelperDomain } from "./const";
|
||||||
import { isHelperDomain } from "./const";
|
import { isHelperDomain } from "./const";
|
||||||
import type { ShowDialogHelperDetailParams } from "./show-dialog-helper-detail";
|
import type { ShowDialogHelperDetailParams } from "./show-dialog-helper-detail";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
|
||||||
type HelperCreators = {
|
type HelperCreators = {
|
||||||
[domain in HelperDomain]: {
|
[domain in HelperDomain]: {
|
||||||
@ -129,6 +130,7 @@ export class DialogHelperDetail extends LitElement {
|
|||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
this._domain = undefined;
|
this._domain = undefined;
|
||||||
this._params = undefined;
|
this._params = undefined;
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
|
@ -177,7 +177,7 @@ export class HaConfigLovelaceRescources extends LitElement {
|
|||||||
.filter=${this._filter}
|
.filter=${this._filter}
|
||||||
@search-changed=${this._handleSearchChange}
|
@search-changed=${this._handleSearchChange}
|
||||||
@row-click=${this._editResource}
|
@row-click=${this._editResource}
|
||||||
hasFab
|
has-fab
|
||||||
clickable
|
clickable
|
||||||
>
|
>
|
||||||
<ha-fab
|
<ha-fab
|
||||||
|
@ -525,6 +525,9 @@ export class HassioNetwork extends LitElement {
|
|||||||
IP_VERSIONS.forEach((version) => {
|
IP_VERSIONS.forEach((version) => {
|
||||||
interfaceOptions[version] = {
|
interfaceOptions[version] = {
|
||||||
method: this._interface![version]?.method || "auto",
|
method: this._interface![version]?.method || "auto",
|
||||||
|
nameservers: this._interface![version]?.nameservers?.filter(
|
||||||
|
(ns: string) => ns.trim()
|
||||||
|
),
|
||||||
};
|
};
|
||||||
if (this._interface![version]?.method === "static") {
|
if (this._interface![version]?.method === "static") {
|
||||||
interfaceOptions[version] = {
|
interfaceOptions[version] = {
|
||||||
@ -533,9 +536,6 @@ export class HassioNetwork extends LitElement {
|
|||||||
(address: string) => address.trim()
|
(address: string) => address.trim()
|
||||||
),
|
),
|
||||||
gateway: this._interface![version]?.gateway,
|
gateway: this._interface![version]?.gateway,
|
||||||
nameservers: this._interface![version]?.nameservers?.filter(
|
|
||||||
(ns: string) => ns.trim()
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -180,7 +180,7 @@ class DialogPersonDetail extends LitElement implements HassDialog {
|
|||||||
</ha-settings-row>
|
</ha-settings-row>
|
||||||
|
|
||||||
${this._renderUserFields()}
|
${this._renderUserFields()}
|
||||||
${!this._deviceTrackersAvailable(this.hass)
|
${this._deviceTrackersAvailable(this.hass)
|
||||||
? html`
|
? html`
|
||||||
<p>
|
<p>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
|
@ -26,6 +26,7 @@ import { navigate } from "../../../common/navigate";
|
|||||||
import { slugify } from "../../../common/string/slugify";
|
import { slugify } from "../../../common/string/slugify";
|
||||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||||
import { afterNextRender } from "../../../common/util/render-status";
|
import { afterNextRender } from "../../../common/util/render-status";
|
||||||
|
import { promiseTimeout } from "../../../common/util/promise-timeout";
|
||||||
import "../../../components/ha-button-menu";
|
import "../../../components/ha-button-menu";
|
||||||
import "../../../components/ha-fab";
|
import "../../../components/ha-fab";
|
||||||
|
|
||||||
@ -915,17 +916,49 @@ export class HaScriptEditor extends SubscribeMixin(
|
|||||||
|
|
||||||
// wait for new script to appear in entity registry
|
// wait for new script to appear in entity registry
|
||||||
if (entityRegPromise) {
|
if (entityRegPromise) {
|
||||||
const script = await entityRegPromise;
|
try {
|
||||||
entityId = script.entity_id;
|
const script = await promiseTimeout(2000, entityRegPromise);
|
||||||
|
entityId = script.entity_id;
|
||||||
|
} catch (e) {
|
||||||
|
entityId = undefined;
|
||||||
|
if (e instanceof Error && e.name === "TimeoutError") {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.automation.editor.new_automation_setup_failed_title",
|
||||||
|
{
|
||||||
|
type: this.hass.localize(
|
||||||
|
"ui.panel.config.automation.editor.type_script"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
text: this.hass.localize(
|
||||||
|
"ui.panel.config.automation.editor.new_automation_setup_failed_text",
|
||||||
|
{
|
||||||
|
type: this.hass.localize(
|
||||||
|
"ui.panel.config.automation.editor.type_script"
|
||||||
|
),
|
||||||
|
types: this.hass.localize(
|
||||||
|
"ui.panel.config.automation.editor.type_script_plural"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
warning: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateEntityRegistryEntry(this.hass, entityId!, {
|
if (entityId) {
|
||||||
categories: {
|
await updateEntityRegistryEntry(this.hass, entityId, {
|
||||||
script: this._entityRegistryUpdate.category || null,
|
categories: {
|
||||||
},
|
script: this._entityRegistryUpdate.category || null,
|
||||||
labels: this._entityRegistryUpdate.labels || [],
|
},
|
||||||
area_id: this._entityRegistryUpdate.area || null,
|
labels: this._entityRegistryUpdate.labels || [],
|
||||||
});
|
area_id: this._entityRegistryUpdate.area || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._dirty = false;
|
this._dirty = false;
|
||||||
@ -934,9 +967,9 @@ export class HaScriptEditor extends SubscribeMixin(
|
|||||||
navigate(`/config/script/edit/${id}`, { replace: true });
|
navigate(`/config/script/edit/${id}`, { replace: true });
|
||||||
}
|
}
|
||||||
} catch (errors: any) {
|
} catch (errors: any) {
|
||||||
this._errors = errors.body.message || errors.error || errors.body;
|
this._errors = errors.body?.message || errors.error || errors.body;
|
||||||
showToast(this, {
|
showToast(this, {
|
||||||
message: errors.body.message || errors.error || errors.body,
|
message: errors.body?.message || errors.error || errors.body,
|
||||||
});
|
});
|
||||||
throw errors;
|
throw errors;
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -295,9 +295,6 @@ export default class HaScriptFieldRow extends LitElement {
|
|||||||
ha-icon-button {
|
ha-icon-button {
|
||||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||||
}
|
}
|
||||||
ha-card {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.disabled {
|
.disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@ -330,6 +327,8 @@ export default class HaScriptFieldRow extends LitElement {
|
|||||||
.disabled-bar {
|
.disabled-bar {
|
||||||
background: var(--divider-color, #e0e0e0);
|
background: var(--divider-color, #e0e0e0);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
border-top-right-radius: var(--ha-card-border-radius, 12px);
|
||||||
|
border-top-left-radius: var(--ha-card-border-radius, 12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-list-item[disabled] {
|
ha-list-item[disabled] {
|
||||||
|
@ -16,7 +16,7 @@ import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
|||||||
import { cardFeatureStyles } from "./common/card-feature-styles";
|
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||||
import type { UpdateActionsCardFeatureConfig } from "./types";
|
import type { UpdateActionsCardFeatureConfig } from "./types";
|
||||||
|
|
||||||
export const DEFAULT_UPDATE_BACKUP_OPTION = "ask";
|
export const DEFAULT_UPDATE_BACKUP_OPTION = "no";
|
||||||
|
|
||||||
export const supportsUpdateActionsCardFeature = (stateObj: HassEntity) => {
|
export const supportsUpdateActionsCardFeature = (stateObj: HassEntity) => {
|
||||||
const domain = computeDomain(stateObj.entity_id);
|
const domain = computeDomain(stateObj.entity_id);
|
||||||
|
@ -16,7 +16,7 @@ import { getSensorNumericDeviceClasses } from "../../../data/sensor";
|
|||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import { hasConfigOrEntitiesChanged } from "../common/has-changed";
|
import { hasConfigOrEntitiesChanged } from "../common/has-changed";
|
||||||
import { processConfigEntities } from "../common/process-config-entities";
|
import { processConfigEntities } from "../common/process-config-entities";
|
||||||
import type { LovelaceCard } from "../types";
|
import type { LovelaceCard, LovelaceGridOptions } from "../types";
|
||||||
import type { HistoryGraphCardConfig } from "./types";
|
import type { HistoryGraphCardConfig } from "./types";
|
||||||
import { createSearchParam } from "../../../common/url/search-params";
|
import { createSearchParam } from "../../../common/url/search-params";
|
||||||
|
|
||||||
@ -56,6 +56,14 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
|||||||
return this._config?.title ? 2 : 0 + 2 * (this._entityIds?.length || 1);
|
return this._config?.title ? 2 : 0 + 2 * (this._entityIds?.length || 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getGridOptions(): LovelaceGridOptions {
|
||||||
|
return {
|
||||||
|
columns: 12,
|
||||||
|
min_columns: 6,
|
||||||
|
min_rows: (this._config?.entities?.length || 1) * 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public setConfig(config: HistoryGraphCardConfig): void {
|
public setConfig(config: HistoryGraphCardConfig): void {
|
||||||
if (!config.entities || !Array.isArray(config.entities)) {
|
if (!config.entities || !Array.isArray(config.entities)) {
|
||||||
throw new Error("Entities need to be an array");
|
throw new Error("Entities need to be an array");
|
||||||
|
@ -18,7 +18,7 @@ import type { HomeAssistant } from "../../../types";
|
|||||||
import { findEntities } from "../common/find-entities";
|
import { findEntities } from "../common/find-entities";
|
||||||
import { hasConfigOrEntitiesChanged } from "../common/has-changed";
|
import { hasConfigOrEntitiesChanged } from "../common/has-changed";
|
||||||
import { processConfigEntities } from "../common/process-config-entities";
|
import { processConfigEntities } from "../common/process-config-entities";
|
||||||
import type { LovelaceCard } from "../types";
|
import type { LovelaceCard, LovelaceGridOptions } from "../types";
|
||||||
import type { StatisticsGraphCardConfig } from "./types";
|
import type { StatisticsGraphCardConfig } from "./types";
|
||||||
|
|
||||||
export const DEFAULT_DAYS_TO_SHOW = 30;
|
export const DEFAULT_DAYS_TO_SHOW = 30;
|
||||||
@ -93,6 +93,14 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getGridOptions(): LovelaceGridOptions {
|
||||||
|
return {
|
||||||
|
columns: 12,
|
||||||
|
min_columns: 9,
|
||||||
|
min_rows: 4,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public setConfig(config: StatisticsGraphCardConfig): void {
|
public setConfig(config: StatisticsGraphCardConfig): void {
|
||||||
if (!config.entities || !Array.isArray(config.entities)) {
|
if (!config.entities || !Array.isArray(config.entities)) {
|
||||||
throw new Error("Entities need to be an array");
|
throw new Error("Entities need to be an array");
|
||||||
|
@ -38,7 +38,7 @@ export class HuiUpdateActionsCardFeatureEditor
|
|||||||
disabled: !supportsBackup,
|
disabled: !supportsBackup,
|
||||||
selector: {
|
selector: {
|
||||||
select: {
|
select: {
|
||||||
default: "yes",
|
default: "no",
|
||||||
mode: "dropdown",
|
mode: "dropdown",
|
||||||
options: ["ask", "yes", "no"].map((option) => ({
|
options: ["ask", "yes", "no"].map((option) => ({
|
||||||
value: option,
|
value: option,
|
||||||
|
@ -37,7 +37,7 @@ export class HuiViewBackgroundEditor extends LitElement {
|
|||||||
type: "expandable" as const,
|
type: "expandable" as const,
|
||||||
schema: [
|
schema: [
|
||||||
{
|
{
|
||||||
name: "transparency",
|
name: "opacity",
|
||||||
selector: {
|
selector: {
|
||||||
number: { min: 1, max: 100, mode: "slider" },
|
number: { min: 1, max: 100, mode: "slider" },
|
||||||
},
|
},
|
||||||
@ -117,7 +117,7 @@ export class HuiViewBackgroundEditor extends LitElement {
|
|||||||
|
|
||||||
if (!background) {
|
if (!background) {
|
||||||
background = {
|
background = {
|
||||||
transparency: 33,
|
opacity: 33,
|
||||||
alignment: "center",
|
alignment: "center",
|
||||||
size: "cover",
|
size: "cover",
|
||||||
repeat: "repeat",
|
repeat: "repeat",
|
||||||
@ -125,7 +125,7 @@ export class HuiViewBackgroundEditor extends LitElement {
|
|||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
background = {
|
background = {
|
||||||
transparency: 100,
|
opacity: 100,
|
||||||
alignment: "center",
|
alignment: "center",
|
||||||
size: "cover",
|
size: "cover",
|
||||||
repeat: "no-repeat",
|
repeat: "no-repeat",
|
||||||
@ -162,9 +162,9 @@ export class HuiViewBackgroundEditor extends LitElement {
|
|||||||
return this.hass.localize(
|
return this.hass.localize(
|
||||||
"ui.panel.lovelace.editor.edit_view.background.image"
|
"ui.panel.lovelace.editor.edit_view.background.image"
|
||||||
);
|
);
|
||||||
case "transparency":
|
case "opacity":
|
||||||
return this.hass.localize(
|
return this.hass.localize(
|
||||||
"ui.panel.lovelace.editor.edit_view.background.transparency"
|
"ui.panel.lovelace.editor.edit_view.background.opacity"
|
||||||
);
|
);
|
||||||
case "alignment":
|
case "alignment":
|
||||||
return this.hass.localize(
|
return this.hass.localize(
|
||||||
|
@ -67,8 +67,8 @@ export class HUIViewBackground extends LitElement {
|
|||||||
background?: string | LovelaceViewBackgroundConfig
|
background?: string | LovelaceViewBackgroundConfig
|
||||||
) {
|
) {
|
||||||
if (typeof background === "object" && background.image) {
|
if (typeof background === "object" && background.image) {
|
||||||
if (background.transparency) {
|
if (background.opacity) {
|
||||||
return `${background.transparency}%`;
|
return `${background.opacity}%`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -16,7 +16,7 @@ import "../../layouts/hass-error-screen";
|
|||||||
import type { HomeAssistant, Route } from "../../types";
|
import type { HomeAssistant, Route } from "../../types";
|
||||||
import { documentationUrl } from "../../util/documentation-url";
|
import { documentationUrl } from "../../util/documentation-url";
|
||||||
|
|
||||||
export const getMyRedirects = (hasSupervisor: boolean): Redirects => ({
|
export const getMyRedirects = (): Redirects => ({
|
||||||
application_credentials: {
|
application_credentials: {
|
||||||
redirect: "/config/application_credentials",
|
redirect: "/config/application_credentials",
|
||||||
},
|
},
|
||||||
@ -244,16 +244,24 @@ export const getMyRedirects = (hasSupervisor: boolean): Redirects => ({
|
|||||||
redirect: "/media-browser",
|
redirect: "/media-browser",
|
||||||
},
|
},
|
||||||
backup: {
|
backup: {
|
||||||
component: hasSupervisor ? "hassio" : "backup",
|
component: "backup",
|
||||||
redirect: hasSupervisor ? "/hassio/backups" : "/config/backup",
|
redirect: "/config/backup",
|
||||||
|
},
|
||||||
|
backup_list: {
|
||||||
|
component: "backup",
|
||||||
|
redirect: "/config/backup/backups",
|
||||||
|
},
|
||||||
|
backup_config: {
|
||||||
|
component: "backup",
|
||||||
|
redirect: "/config/backup/settings",
|
||||||
},
|
},
|
||||||
supervisor_snapshots: {
|
supervisor_snapshots: {
|
||||||
component: hasSupervisor ? "hassio" : "backup",
|
component: "backup",
|
||||||
redirect: hasSupervisor ? "/hassio/backups" : "/config/backup",
|
redirect: "/config/backup",
|
||||||
},
|
},
|
||||||
supervisor_backups: {
|
supervisor_backups: {
|
||||||
component: hasSupervisor ? "hassio" : "backup",
|
component: "backup",
|
||||||
redirect: hasSupervisor ? "/hassio/backups" : "/config/backup",
|
redirect: "/config/backup",
|
||||||
},
|
},
|
||||||
supervisor_system: {
|
supervisor_system: {
|
||||||
// Moved from Supervisor panel in 2022.5
|
// Moved from Supervisor panel in 2022.5
|
||||||
@ -278,10 +286,8 @@ export const getMyRedirects = (hasSupervisor: boolean): Redirects => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const getRedirect = (
|
const getRedirect = (path: string): Redirect | undefined =>
|
||||||
path: string,
|
getMyRedirects()?.[path];
|
||||||
hasSupervisor: boolean
|
|
||||||
): Redirect | undefined => getMyRedirects(hasSupervisor)?.[path];
|
|
||||||
|
|
||||||
export type ParamType = "url" | "string" | "string?";
|
export type ParamType = "url" | "string" | "string?";
|
||||||
|
|
||||||
@ -314,7 +320,7 @@ class HaPanelMy extends LitElement {
|
|||||||
const path = this.route.path.substring(1);
|
const path = this.route.path.substring(1);
|
||||||
const hasSupervisor = isComponentLoaded(this.hass, "hassio");
|
const hasSupervisor = isComponentLoaded(this.hass, "hassio");
|
||||||
|
|
||||||
this._redirect = getRedirect(path, hasSupervisor);
|
this._redirect = getRedirect(path);
|
||||||
|
|
||||||
if (path.startsWith("supervisor") && this._redirect === undefined) {
|
if (path.startsWith("supervisor") && this._redirect === undefined) {
|
||||||
if (!hasSupervisor) {
|
if (!hasSupervisor) {
|
||||||
|
@ -150,9 +150,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
|||||||
|
|
||||||
const myPanel = await import("../panels/my/ha-panel-my");
|
const myPanel = await import("../panels/my/ha-panel-my");
|
||||||
|
|
||||||
for (const [slug, redirect] of Object.entries(
|
for (const [slug, redirect] of Object.entries(myPanel.getMyRedirects())) {
|
||||||
myPanel.getMyRedirects(isHassio)
|
|
||||||
)) {
|
|
||||||
if (targetPath.startsWith(redirect.redirect)) {
|
if (targetPath.startsWith(redirect.redirect)) {
|
||||||
myParams.append("redirect", slug);
|
myParams.append("redirect", slug);
|
||||||
if (redirect.params) {
|
if (redirect.params) {
|
||||||
|
@ -651,6 +651,7 @@
|
|||||||
"no_devices": "You don't have any devices",
|
"no_devices": "You don't have any devices",
|
||||||
"no_match": "No matching devices found",
|
"no_match": "No matching devices found",
|
||||||
"device": "Device",
|
"device": "Device",
|
||||||
|
"unnamed_device": "Unnamed device",
|
||||||
"no_area": "No area"
|
"no_area": "No area"
|
||||||
},
|
},
|
||||||
"category-picker": {
|
"category-picker": {
|
||||||
@ -755,7 +756,7 @@
|
|||||||
},
|
},
|
||||||
"picture-upload": {
|
"picture-upload": {
|
||||||
"label": "Add picture",
|
"label": "Add picture",
|
||||||
"change_picture": "Change picture",
|
"clear_picture": "Clear picture",
|
||||||
"current_image_alt": "Current picture",
|
"current_image_alt": "Current picture",
|
||||||
"supported_formats": "Supports JPEG, PNG, or GIF image.",
|
"supported_formats": "Supports JPEG, PNG, or GIF image.",
|
||||||
"unsupported_format": "Unsupported format, please choose a JPEG, PNG, or GIF image.",
|
"unsupported_format": "Unsupported format, please choose a JPEG, PNG, or GIF image.",
|
||||||
@ -830,7 +831,8 @@
|
|||||||
"source_history": "Source: History",
|
"source_history": "Source: History",
|
||||||
"source_stats": "Source: Long term statistics",
|
"source_stats": "Source: Long term statistics",
|
||||||
"zoom_hint": "Use ctrl + scroll to zoom in/out",
|
"zoom_hint": "Use ctrl + scroll to zoom in/out",
|
||||||
"zoom_hint_mac": "Use ⌘ + scroll to zoom in/out"
|
"zoom_hint_mac": "Use ⌘ + scroll to zoom in/out",
|
||||||
|
"zoom_reset": "Reset zoom"
|
||||||
},
|
},
|
||||||
"map": {
|
"map": {
|
||||||
"error": "Unable to load map"
|
"error": "Unable to load map"
|
||||||
@ -2213,7 +2215,7 @@
|
|||||||
},
|
},
|
||||||
"dialogs": {
|
"dialogs": {
|
||||||
"local_backup_location": {
|
"local_backup_location": {
|
||||||
"title": "Change local backup location",
|
"title": "Change default local backup location",
|
||||||
"description": "Change the default location where local backups are stored on your Home Assistant instance.",
|
"description": "Change the default location where local backups are stored on your Home Assistant instance.",
|
||||||
"note": "This location will be used when you create a backup using the supervisor actions in an automation for example.",
|
"note": "This location will be used when you create a backup using the supervisor actions in an automation for example.",
|
||||||
"options": {
|
"options": {
|
||||||
@ -3063,6 +3065,12 @@
|
|||||||
"unknown_entity": "unknown entity",
|
"unknown_entity": "unknown entity",
|
||||||
"edit_unknown_device": "Editor not available for unknown device",
|
"edit_unknown_device": "Editor not available for unknown device",
|
||||||
"switch_ui_yaml_error": "There are currently YAML errors in the automation, and it cannot be parsed. Switching to UI mode may cause pending changes to be lost. Press cancel to correct any errors before proceeding to prevent loss of pending changes, or continue if you are sure.",
|
"switch_ui_yaml_error": "There are currently YAML errors in the automation, and it cannot be parsed. Switching to UI mode may cause pending changes to be lost. Press cancel to correct any errors before proceeding to prevent loss of pending changes, or continue if you are sure.",
|
||||||
|
"type_automation": "automation",
|
||||||
|
"type_script": "script",
|
||||||
|
"type_automation_plural": "[%key:ui::panel::config::blueprint::overview::types_plural::automation%]",
|
||||||
|
"type_script_plural": "[%key:ui::panel::config::blueprint::overview::types_plural::script%]",
|
||||||
|
"new_automation_setup_failed_title": "New {type} setup failed",
|
||||||
|
"new_automation_setup_failed_text": "Your new {type} has saved, but waiting for it to setup has timed out. This could be due to errors parsing your configuration.yaml, please check the configuration in developer tools. Your {type} will not be visible until this is corrected, and {types} are reloaded. Changes to area, category, or labels were not saved and must be reapplied.",
|
||||||
"triggers": {
|
"triggers": {
|
||||||
"name": "Triggers",
|
"name": "Triggers",
|
||||||
"header": "When",
|
"header": "When",
|
||||||
@ -5955,7 +5963,7 @@
|
|||||||
"bottom right": "Bottom right"
|
"bottom right": "Bottom right"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"transparency": "Background transparency",
|
"opacity": "Background opacity",
|
||||||
"repeat": {
|
"repeat": {
|
||||||
"name": "Background repeat",
|
"name": "Background repeat",
|
||||||
"options": {
|
"options": {
|
||||||
|
69
yarn.lock
69
yarn.lock
@ -1801,6 +1801,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@gfx/zopfli@npm:^1.0.15":
|
||||||
|
version: 1.0.15
|
||||||
|
resolution: "@gfx/zopfli@npm:1.0.15"
|
||||||
|
dependencies:
|
||||||
|
base64-js: "npm:^1.3.0"
|
||||||
|
checksum: 10/2721ad8c0cbbdac7d5ca9e01ad05f232b4e3cdcecf88f9b0ef9a2bdc7d05e1ca54ea68905430cf36338ef1077acec178aa7b258c67baa7a4c2b6d74067605723
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@gulpjs/messages@npm:^1.1.0":
|
"@gulpjs/messages@npm:^1.1.0":
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
resolution: "@gulpjs/messages@npm:1.1.0"
|
resolution: "@gulpjs/messages@npm:1.1.0"
|
||||||
@ -5684,6 +5693,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"any-promise@npm:^1.1.0":
|
||||||
|
version: 1.3.0
|
||||||
|
resolution: "any-promise@npm:1.3.0"
|
||||||
|
checksum: 10/6737469ba353b5becf29e4dc3680736b9caa06d300bda6548812a8fee63ae7d336d756f88572fa6b5219aed36698d808fa55f62af3e7e6845c7a1dc77d240edb
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"anymatch@npm:^3.1.3, anymatch@npm:~3.1.2":
|
"anymatch@npm:^3.1.3, anymatch@npm:~3.1.2":
|
||||||
version: 3.1.3
|
version: 3.1.3
|
||||||
resolution: "anymatch@npm:3.1.3"
|
resolution: "anymatch@npm:3.1.3"
|
||||||
@ -6030,7 +6046,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"base64-js@npm:^1.3.1":
|
"base64-js@npm:^1.3.0, base64-js@npm:^1.3.1":
|
||||||
version: 1.5.1
|
version: 1.5.1
|
||||||
resolution: "base64-js@npm:1.5.1"
|
resolution: "base64-js@npm:1.5.1"
|
||||||
checksum: 10/669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005
|
checksum: 10/669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005
|
||||||
@ -6237,7 +6253,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"bytes@npm:3.1.2":
|
"bytes@npm:3.1.2, bytes@npm:^3.1.2":
|
||||||
version: 3.1.2
|
version: 3.1.2
|
||||||
resolution: "bytes@npm:3.1.2"
|
resolution: "bytes@npm:3.1.2"
|
||||||
checksum: 10/a10abf2ba70c784471d6b4f58778c0beeb2b5d405148e66affa91f23a9f13d07603d0a0354667310ae1d6dc141474ffd44e2a074be0f6e2254edb8fc21445388
|
checksum: 10/a10abf2ba70c784471d6b4f58778c0beeb2b5d405148e66affa91f23a9f13d07603d0a0354667310ae1d6dc141474ffd44e2a074be0f6e2254edb8fc21445388
|
||||||
@ -7070,7 +7086,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"defaults@npm:^1.0.3":
|
"defaults@npm:^1.0.3, defaults@npm:^1.0.4":
|
||||||
version: 1.0.4
|
version: 1.0.4
|
||||||
resolution: "defaults@npm:1.0.4"
|
resolution: "defaults@npm:1.0.4"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -8180,7 +8196,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"fancy-log@npm:2.0.0":
|
"fancy-log@npm:2.0.0, fancy-log@npm:^2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "fancy-log@npm:2.0.0"
|
resolution: "fancy-log@npm:2.0.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -8973,6 +8989,21 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"gulp-zopfli-green@npm:6.0.2":
|
||||||
|
version: 6.0.2
|
||||||
|
resolution: "gulp-zopfli-green@npm:6.0.2"
|
||||||
|
dependencies:
|
||||||
|
"@gfx/zopfli": "npm:^1.0.15"
|
||||||
|
bytes: "npm:^3.1.2"
|
||||||
|
defaults: "npm:^1.0.4"
|
||||||
|
fancy-log: "npm:^2.0.0"
|
||||||
|
plugin-error: "npm:^2.0.1"
|
||||||
|
stream-to-array: "npm:^2.3.0"
|
||||||
|
through2: "npm:^4.0.2"
|
||||||
|
checksum: 10/52e899dfb86777ff8f97a23af99c59e203ea485fbf04d0a8f4f1cfbd4d4c496808a3593ae8dac16584fc4b4d81cf127b2eda5355a61bcc213875c95cc86d41da
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"gulp@npm:5.0.0":
|
"gulp@npm:5.0.0":
|
||||||
version: 5.0.0
|
version: 5.0.0
|
||||||
resolution: "gulp@npm:5.0.0"
|
resolution: "gulp@npm:5.0.0"
|
||||||
@ -9254,6 +9285,7 @@ __metadata:
|
|||||||
gulp-brotli: "npm:3.0.0"
|
gulp-brotli: "npm:3.0.0"
|
||||||
gulp-json-transform: "npm:0.5.0"
|
gulp-json-transform: "npm:0.5.0"
|
||||||
gulp-rename: "npm:2.0.0"
|
gulp-rename: "npm:2.0.0"
|
||||||
|
gulp-zopfli-green: "npm:6.0.2"
|
||||||
hls.js: "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch"
|
hls.js: "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch"
|
||||||
home-assistant-js-websocket: "npm:9.4.0"
|
home-assistant-js-websocket: "npm:9.4.0"
|
||||||
html-minifier-terser: "npm:7.2.0"
|
html-minifier-terser: "npm:7.2.0"
|
||||||
@ -12161,6 +12193,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"plugin-error@npm:^2.0.1":
|
||||||
|
version: 2.0.1
|
||||||
|
resolution: "plugin-error@npm:2.0.1"
|
||||||
|
dependencies:
|
||||||
|
ansi-colors: "npm:^1.0.1"
|
||||||
|
checksum: 10/9a4f91461cd24cce401112098969991d7aa6b4c94f78e0381234280c07da779570a8b21ab143292b534ec0117c09705a67e5d756c1c303d4706fdd7f861bf5bc
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"pngjs@npm:^3.0.0, pngjs@npm:^3.3.3":
|
"pngjs@npm:^3.0.0, pngjs@npm:^3.3.3":
|
||||||
version: 3.4.0
|
version: 3.4.0
|
||||||
resolution: "pngjs@npm:3.4.0"
|
resolution: "pngjs@npm:3.4.0"
|
||||||
@ -12383,7 +12424,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"readable-stream@npm:2 || 3, readable-stream@npm:^3.0.6, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0":
|
"readable-stream@npm:2 || 3, readable-stream@npm:3, readable-stream@npm:^3.0.6, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0":
|
||||||
version: 3.6.2
|
version: 3.6.2
|
||||||
resolution: "readable-stream@npm:3.6.2"
|
resolution: "readable-stream@npm:3.6.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -13566,6 +13607,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"stream-to-array@npm:^2.3.0":
|
||||||
|
version: 2.3.0
|
||||||
|
resolution: "stream-to-array@npm:2.3.0"
|
||||||
|
dependencies:
|
||||||
|
any-promise: "npm:^1.1.0"
|
||||||
|
checksum: 10/7feaf63b38399b850615e6ffcaa951e96e4c8f46745dbce4b553a94c5dc43966933813747014935a3ff97793e7f30a65270bde19f82b2932871a1879229a77cf
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"streamx@npm:^2.12.0, streamx@npm:^2.12.5, streamx@npm:^2.13.2, streamx@npm:^2.14.0":
|
"streamx@npm:^2.12.0, streamx@npm:^2.12.5, streamx@npm:^2.13.2, streamx@npm:^2.14.0":
|
||||||
version: 2.21.1
|
version: 2.21.1
|
||||||
resolution: "streamx@npm:2.21.1"
|
resolution: "streamx@npm:2.21.1"
|
||||||
@ -13993,6 +14043,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"through2@npm:^4.0.2":
|
||||||
|
version: 4.0.2
|
||||||
|
resolution: "through2@npm:4.0.2"
|
||||||
|
dependencies:
|
||||||
|
readable-stream: "npm:3"
|
||||||
|
checksum: 10/72c246233d9a989bbebeb6b698ef0b7b9064cb1c47930f79b25d87b6c867e075432811f69b7b2ac8da00ca308191c507bdab913944be8019ac43b036ce88f6ba
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"thunky@npm:^1.0.2":
|
"thunky@npm:^1.0.2":
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
resolution: "thunky@npm:1.1.0"
|
resolution: "thunky@npm:1.1.0"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user