mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-07 06:58:18 +00:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3f0dfc62b | ||
|
|
d4125d433f | ||
|
|
c824e58e0a | ||
|
|
c6617718b7 | ||
|
|
cd1ca72e45 | ||
|
|
4f9ca3b173 | ||
|
|
1349c8520c | ||
|
|
6d1a55cc3a | ||
|
|
23a9ae6835 | ||
|
|
dbd1e928de | ||
|
|
e86ad21ce2 | ||
|
|
0d97afb3f2 | ||
|
|
03a415beff | ||
|
|
44cc75afbc | ||
|
|
748642a8d6 | ||
|
|
3d5c65d652 | ||
|
|
a26bf80b13 |
@@ -65,6 +65,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
mockEntityRegistry(hass, [
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
config_subentry_id: null,
|
||||
device_id: "co2signal",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
@@ -85,6 +86,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
},
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
config_subentry_id: null,
|
||||
device_id: "co2signal",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
|
||||
@@ -11,6 +11,7 @@ export const mockConfigEntries = (hass: MockHomeAssistant) => {
|
||||
supports_remove_device: false,
|
||||
supports_unload: true,
|
||||
supports_reconfigure: true,
|
||||
supported_subentry_types: {},
|
||||
pref_disable_new_entities: false,
|
||||
pref_disable_polling: false,
|
||||
disabled_by: null,
|
||||
|
||||
@@ -48,6 +48,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: "bedroom",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_1"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
@@ -71,6 +72,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: "backyard",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_2"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
@@ -94,6 +96,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: null,
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_3"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
|
||||
@@ -47,6 +47,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: "bedroom",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_1"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
@@ -70,6 +71,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: "backyard",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_2"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
@@ -93,6 +95,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: null,
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_3"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
|
||||
@@ -32,6 +32,8 @@ const createConfigEntry = (
|
||||
supports_remove_device: false,
|
||||
supports_unload: true,
|
||||
supports_reconfigure: true,
|
||||
supported_subentry_types: {},
|
||||
num_subentries: 0,
|
||||
disabled_by: null,
|
||||
pref_disable_new_entities: false,
|
||||
pref_disable_polling: false,
|
||||
@@ -188,6 +190,7 @@ const createEntityRegistryEntries = (
|
||||
): EntityRegistryEntry[] => [
|
||||
{
|
||||
config_entry_id: item.entry_id,
|
||||
config_subentry_id: null,
|
||||
device_id: "mock-device-id",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
@@ -214,6 +217,7 @@ const createDeviceRegistryEntries = (
|
||||
{
|
||||
entry_type: null,
|
||||
config_entries: [item.entry_id],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
manufacturer: "ESPHome",
|
||||
model: "Mock Device",
|
||||
|
||||
@@ -14,7 +14,7 @@ import type { LocalizeFunc } from "../../../src/common/translations/localize";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"backup-uploaded": { backup: HassioBackup };
|
||||
"hassio-backup-uploaded": { backup: HassioBackup };
|
||||
"backup-cleared": undefined;
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ export class HassioUploadBackup extends LitElement {
|
||||
this._uploading = true;
|
||||
try {
|
||||
const backup = await uploadBackup(this.hass, file);
|
||||
fireEvent(this, "backup-uploaded", { backup: backup.data });
|
||||
fireEvent(this, "hassio-backup-uploaded", { backup: backup.data });
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: "Upload failed",
|
||||
|
||||
@@ -5,7 +5,6 @@ import { customElement, property, query } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { formatDate } from "../../../src/common/datetime/format_date";
|
||||
import { formatDateTime } from "../../../src/common/datetime/format_date_time";
|
||||
import type { LocalizeFunc } from "../../../src/common/translations/localize";
|
||||
import "../../../src/components/ha-checkbox";
|
||||
import "../../../src/components/ha-formfield";
|
||||
import "../../../src/components/ha-textfield";
|
||||
@@ -19,13 +18,10 @@ import type {
|
||||
} from "../../../src/data/hassio/backup";
|
||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { mdiHomeAssistant } from "../../../src/resources/home-assistant-logo-svg";
|
||||
import type { HomeAssistant, TranslationDict } from "../../../src/types";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import "./supervisor-formfield-label";
|
||||
import type { HaTextField } from "../../../src/components/ha-textfield";
|
||||
|
||||
type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] &
|
||||
keyof TranslationDict["ui"]["panel"]["page-onboarding"]["restore"];
|
||||
|
||||
interface CheckboxItem {
|
||||
slug: string;
|
||||
checked: boolean;
|
||||
@@ -67,8 +63,6 @@ const _computeAddons = (addons): AddonCheckboxItem[] =>
|
||||
export class SupervisorBackupContent extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false }) public supervisor?: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public backup?: HassioBackupDetail;
|
||||
@@ -115,10 +109,6 @@ export class SupervisorBackupContent extends LitElement {
|
||||
this._focusTarget?.focus();
|
||||
}
|
||||
|
||||
private _localize = (key: BackupOrRestoreKey) =>
|
||||
this.supervisor?.localize(`backup.${key}`) ||
|
||||
this.localize!(`ui.panel.page-onboarding.restore.${key}`);
|
||||
|
||||
protected render() {
|
||||
if (!this.onboarding && !this.supervisor) {
|
||||
return nothing;
|
||||
@@ -132,8 +122,8 @@ export class SupervisorBackupContent extends LitElement {
|
||||
${this.backup
|
||||
? html`<div class="details">
|
||||
${this.backup.type === "full"
|
||||
? this._localize("full_backup")
|
||||
: this._localize("partial_backup")}
|
||||
? this.supervisor?.localize("backup.full_backup")
|
||||
: this.supervisor?.localize("backup.partial_backup")}
|
||||
(${Math.ceil(this.backup.size * 10) / 10 + " MB"})<br />
|
||||
${this.hass
|
||||
? formatDateTime(
|
||||
@@ -145,7 +135,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
</div>`
|
||||
: html`<ha-textfield
|
||||
name="backupName"
|
||||
.label=${this._localize("name")}
|
||||
.label=${this.supervisor?.localize("backup.name")}
|
||||
.value=${this.backupName}
|
||||
@change=${this._handleTextValueChanged}
|
||||
>
|
||||
@@ -153,11 +143,13 @@ export class SupervisorBackupContent extends LitElement {
|
||||
${!this.backup || this.backup.type === "full"
|
||||
? html`<div class="sub-header">
|
||||
${!this.backup
|
||||
? this._localize("type")
|
||||
: this._localize("select_type")}
|
||||
? this.supervisor?.localize("backup.type")
|
||||
: this.supervisor?.localize("backup.select_type")}
|
||||
</div>
|
||||
<div class="backup-types">
|
||||
<ha-formfield .label=${this._localize("full_backup")}>
|
||||
<ha-formfield
|
||||
.label=${this.supervisor?.localize("backup.full_backup")}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="full"
|
||||
@@ -166,7 +158,9 @@ export class SupervisorBackupContent extends LitElement {
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield .label=${this._localize("partial_backup")}>
|
||||
<ha-formfield
|
||||
.label=${this.supervisor?.localize("backup.partial_backup")}
|
||||
>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="partial"
|
||||
@@ -202,7 +196,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
.label=${this._localize("folders")}
|
||||
.label=${this.supervisor?.localize("backup.folders")}
|
||||
.iconPath=${mdiFolder}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
@@ -222,7 +216,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
.label=${this._localize("addons")}
|
||||
.label=${this.supervisor?.localize("backup.addons")}
|
||||
.iconPath=${mdiPuzzle}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
@@ -247,7 +241,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
${!this.backup
|
||||
? html`<ha-formfield
|
||||
class="password"
|
||||
.label=${this._localize("password_protection")}
|
||||
.label=${this.supervisor?.localize("backup.password_protection")}
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this.backupHasPassword}
|
||||
@@ -259,7 +253,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
${this.backupHasPassword
|
||||
? html`
|
||||
<ha-password-field
|
||||
.label=${this._localize("password")}
|
||||
.label=${this.supervisor?.localize("backup.password")}
|
||||
name="backupPassword"
|
||||
.value=${this.backupPassword}
|
||||
@change=${this._handleTextValueChanged}
|
||||
@@ -267,7 +261,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
</ha-password-field>
|
||||
${!this.backup
|
||||
? html`<ha-password-field
|
||||
.label=${this._localize("confirm_password")}
|
||||
.label=${this.supervisor?.localize("backup.confirm_password")}
|
||||
name="confirmBackupPassword"
|
||||
.value=${this.confirmBackupPassword}
|
||||
@change=${this._handleTextValueChanged}
|
||||
|
||||
@@ -72,7 +72,7 @@ export class DialogHassioBackupUpload
|
||||
</ha-header-bar>
|
||||
</div>
|
||||
<hassio-upload-backup
|
||||
@backup-uploaded=${this._backupUploaded}
|
||||
@hassio-backup-uploaded=${this._backupUploaded}
|
||||
.hass=${this.hass}
|
||||
></hassio-upload-backup>
|
||||
</ha-dialog>
|
||||
|
||||
@@ -35,7 +35,6 @@ import { fileDownload } from "../../../../src/util/file_download";
|
||||
import "../../components/supervisor-backup-content";
|
||||
import type { SupervisorBackupContent } from "../../components/supervisor-backup-content";
|
||||
import type { HassioBackupDialogParams } from "./show-dialog-hassio-backup";
|
||||
import type { BackupOrRestoreKey } from "../../util/translations";
|
||||
import type { HaMdDialog } from "../../../../src/components/ha-md-dialog";
|
||||
|
||||
@customElement("dialog-hassio-backup")
|
||||
@@ -43,7 +42,7 @@ class HassioBackupDialog
|
||||
extends LitElement
|
||||
implements HassDialog<HassioBackupDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@@ -62,9 +61,13 @@ class HassioBackupDialog
|
||||
this._dialogParams = dialogParams;
|
||||
this._backup = await fetchHassioBackupInfo(this.hass, dialogParams.slug);
|
||||
if (!this._backup) {
|
||||
this._error = this._localize("no_backup_found");
|
||||
this._error = this._dialogParams.supervisor?.localize(
|
||||
"backup.no_backup_found"
|
||||
);
|
||||
} else if (this._dialogParams.onboarding && !this._backup.homeassistant) {
|
||||
this._error = this._localize("restore_no_home_assistant");
|
||||
this._error = this._dialogParams.supervisor?.localize(
|
||||
"backup.restore_no_home_assistant"
|
||||
);
|
||||
}
|
||||
this._restoringBackup = false;
|
||||
}
|
||||
@@ -82,13 +85,6 @@ class HassioBackupDialog
|
||||
return true;
|
||||
}
|
||||
|
||||
private _localize(key: BackupOrRestoreKey) {
|
||||
return (
|
||||
this._dialogParams!.supervisor?.localize(`backup.${key}`) ||
|
||||
this._dialogParams!.localize!(`ui.panel.page-onboarding.restore.${key}`)
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._dialogParams || !this._backup) {
|
||||
return nothing;
|
||||
@@ -102,7 +98,7 @@ class HassioBackupDialog
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this._localize("close")}
|
||||
.label=${this._dialogParams.supervisor?.localize("backup.close")}
|
||||
.path=${mdiClose}
|
||||
@click=${this.closeDialog}
|
||||
.disabled=${this._restoringBackup}
|
||||
@@ -150,7 +146,6 @@ class HassioBackupDialog
|
||||
.supervisor=${this._dialogParams.supervisor}
|
||||
.backup=${this._backup}
|
||||
.onboarding=${this._dialogParams.onboarding || false}
|
||||
.localize=${this._dialogParams.localize}
|
||||
dialogInitialFocus
|
||||
>
|
||||
</supervisor-backup-content>
|
||||
@@ -161,7 +156,7 @@ class HassioBackupDialog
|
||||
.disabled=${this._restoringBackup || !!this._error}
|
||||
@click=${this._restoreClicked}
|
||||
>
|
||||
${this._localize("restore")}
|
||||
${this._dialogParams.supervisor?.localize("backup.restore")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
@@ -196,18 +191,22 @@ class HassioBackupDialog
|
||||
}
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: this._localize(
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_title"
|
||||
: "confirm_restore_partial_backup_title"
|
||||
title: supervisor?.localize(
|
||||
`backup.${
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_title"
|
||||
: "confirm_restore_partial_backup_title"
|
||||
}`
|
||||
),
|
||||
text: this._localize(
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_text"
|
||||
: "confirm_restore_partial_backup_text"
|
||||
text: supervisor?.localize(
|
||||
`backup.${
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_text"
|
||||
: "confirm_restore_partial_backup_text"
|
||||
}`
|
||||
),
|
||||
confirmText: this._localize("restore"),
|
||||
dismissText: this._localize("cancel"),
|
||||
confirmText: supervisor?.localize("backup.restore"),
|
||||
dismissText: supervisor?.localize("backup.cancel"),
|
||||
}))
|
||||
) {
|
||||
this._restoringBackup = false;
|
||||
@@ -227,7 +226,8 @@ class HassioBackupDialog
|
||||
this.closeDialog();
|
||||
} catch (error: any) {
|
||||
this._error =
|
||||
error?.body?.message || this._localize("restore_start_failed");
|
||||
error?.body?.message ||
|
||||
supervisor?.localize("backup.restore_start_failed");
|
||||
} finally {
|
||||
this._restoringBackup = false;
|
||||
}
|
||||
@@ -286,7 +286,7 @@ class HassioBackupDialog
|
||||
title: supervisor.localize("backup.remote_download_title"),
|
||||
text: supervisor.localize("backup.remote_download_text"),
|
||||
confirmText: supervisor.localize("backup.download"),
|
||||
dismissText: this._localize("cancel"),
|
||||
dismissText: supervisor?.localize("backup.cancel"),
|
||||
});
|
||||
if (!confirm) {
|
||||
return;
|
||||
@@ -302,7 +302,7 @@ class HassioBackupDialog
|
||||
private get _computeName() {
|
||||
return this._backup
|
||||
? this._backup.name || this._backup.slug
|
||||
: this._localize("unnamed_backup");
|
||||
: this._dialogParams!.supervisor?.localize("backup.unnamed_backup") || "";
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../src/common/translations/localize";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
|
||||
export interface HassioBackupDialogParams {
|
||||
@@ -8,7 +7,6 @@ export interface HassioBackupDialogParams {
|
||||
onRestoring?: () => void;
|
||||
onboarding?: boolean;
|
||||
supervisor?: Supervisor;
|
||||
localize?: LocalizeFunc;
|
||||
}
|
||||
|
||||
export const showHassioBackupDialog = (
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import type { TranslationDict } from "../../../src/types";
|
||||
|
||||
export type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] &
|
||||
keyof TranslationDict["ui"]["panel"]["page-onboarding"]["restore"];
|
||||
+3
-3
@@ -91,8 +91,8 @@
|
||||
"@polymer/polymer": "3.5.2",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.6.3",
|
||||
"@vaadin/vaadin-themable-mixin": "24.6.3",
|
||||
"@vaadin/combo-box": "24.6.4",
|
||||
"@vaadin/vaadin-themable-mixin": "24.6.4",
|
||||
"@vibrant/color": "4.0.0",
|
||||
"@vue/web-component-wrapper": "1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
||||
@@ -190,7 +190,7 @@
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.0",
|
||||
"eslint": "9.19.0",
|
||||
"eslint": "9.20.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.0.1",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
|
||||
@@ -99,7 +99,6 @@ export const DOMAINS_TOGGLE = new Set([
|
||||
"switch",
|
||||
"group",
|
||||
"automation",
|
||||
"climate",
|
||||
"humidifier",
|
||||
"valve",
|
||||
]);
|
||||
|
||||
@@ -26,6 +26,20 @@ const formatDateTimeMem = memoizeOne(
|
||||
})
|
||||
);
|
||||
|
||||
export const formatDateTimeWithBrowserDefaults = (dateObj: Date) =>
|
||||
formatDateTimeWithBrowserDefaultsMem().format(dateObj);
|
||||
|
||||
const formatDateTimeWithBrowserDefaultsMem = memoizeOne(
|
||||
() =>
|
||||
new Intl.DateTimeFormat(undefined, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
);
|
||||
|
||||
// Aug 9, 2021, 8:23 AM
|
||||
export const formatShortDateTimeWithYear = (
|
||||
dateObj: Date,
|
||||
|
||||
@@ -41,7 +41,7 @@ export class HaProgressButton extends LitElement {
|
||||
indeterminate
|
||||
></ha-circular-progress>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
</div>
|
||||
`}
|
||||
`;
|
||||
@@ -117,6 +117,9 @@ export class HaProgressButton extends LitElement {
|
||||
mwc-button.error slot {
|
||||
visibility: hidden;
|
||||
}
|
||||
:host([destructive]) {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import "./ha-icon-button";
|
||||
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { bytesToString } from "../util/bytes-to-string";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -23,6 +24,8 @@ declare global {
|
||||
export class HaFileUpload extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
|
||||
@property() public accept!: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
@@ -31,6 +34,10 @@ export class HaFileUpload extends LitElement {
|
||||
|
||||
@property() public secondary?: string;
|
||||
|
||||
@property({ attribute: "uploading-label" }) public uploadingLabel?: string;
|
||||
|
||||
@property({ attribute: "delete-label" }) public deleteLabel?: string;
|
||||
|
||||
@property() public supports?: string;
|
||||
|
||||
@property({ type: Object }) public value?: File | File[] | FileList | string;
|
||||
@@ -73,23 +80,22 @@ export class HaFileUpload extends LitElement {
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
const localize = this.localize || this.hass!.localize;
|
||||
return html`
|
||||
${this.uploading
|
||||
? html`<div class="container">
|
||||
<div class="uploading">
|
||||
<span class="header"
|
||||
>${this.value
|
||||
? this.hass?.localize(
|
||||
"ui.components.file-upload.uploading_name",
|
||||
{ name: this._name }
|
||||
)
|
||||
: this.hass?.localize(
|
||||
"ui.components.file-upload.uploading"
|
||||
)}</span
|
||||
>${this.uploadingLabel || this.value
|
||||
? localize("ui.components.file-upload.uploading_name", {
|
||||
name: this._name,
|
||||
})
|
||||
: localize("ui.components.file-upload.uploading")}</span
|
||||
>
|
||||
${this.progress
|
||||
? html`<div class="progress">
|
||||
${this.progress}${blankBeforePercent(this.hass!.locale)}%
|
||||
${this.progress}${this.hass &&
|
||||
blankBeforePercent(this.hass!.locale)}%
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
@@ -116,14 +122,11 @@ export class HaFileUpload extends LitElement {
|
||||
.path=${this.icon || mdiFileUpload}
|
||||
></ha-svg-icon>
|
||||
<ha-button unelevated @click=${this._openFilePicker}>
|
||||
${this.label ||
|
||||
this.hass?.localize("ui.components.file-upload.label")}
|
||||
${this.label || localize("ui.components.file-upload.label")}
|
||||
</ha-button>
|
||||
<span class="secondary"
|
||||
>${this.secondary ||
|
||||
this.hass?.localize(
|
||||
"ui.components.file-upload.secondary"
|
||||
)}</span
|
||||
localize("ui.components.file-upload.secondary")}</span
|
||||
>
|
||||
<span class="supports">${this.supports}</span>`
|
||||
: typeof this.value === "string"
|
||||
@@ -136,8 +139,7 @@ export class HaFileUpload extends LitElement {
|
||||
</div>
|
||||
<ha-icon-button
|
||||
@click=${this._clearValue}
|
||||
.label=${this.hass?.localize("ui.common.delete") ||
|
||||
"Delete"}
|
||||
.label=${this.deleteLabel || localize("ui.common.delete")}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
@@ -155,8 +157,8 @@ export class HaFileUpload extends LitElement {
|
||||
</div>
|
||||
<ha-icon-button
|
||||
@click=${this._clearValue}
|
||||
.label=${this.hass?.localize("ui.common.delete") ||
|
||||
"Delete"}
|
||||
.label=${this.deleteLabel ||
|
||||
localize("ui.common.delete")}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
@@ -238,6 +240,10 @@ export class HaFileUpload extends LitElement {
|
||||
border-radius: var(--mdc-shape-small, 4px);
|
||||
height: 100%;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
label.container {
|
||||
border: dashed 1px
|
||||
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
|
||||
|
||||
+22
-15
@@ -2,7 +2,6 @@ import { memoize } from "@fullcalendar/core/internal";
|
||||
import { setHours, setMinutes } from "date-fns";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import memoizeOne from "memoize-one";
|
||||
import checkValidDate from "../common/datetime/check_valid_date";
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDateTimeNumeric,
|
||||
@@ -13,6 +12,8 @@ import type { HomeAssistant } from "../types";
|
||||
import { fileDownload } from "../util/file_download";
|
||||
import { domainToName } from "./integration";
|
||||
import type { FrontendLocaleData } from "./translation";
|
||||
import checkValidDate from "../common/datetime/check_valid_date";
|
||||
import { handleFetchPromise } from "../util/hass-call-api";
|
||||
|
||||
export const enum BackupScheduleRecurrence {
|
||||
NEVER = "never",
|
||||
@@ -231,27 +232,23 @@ export const restoreBackup = (
|
||||
export const uploadBackup = async (
|
||||
hass: HomeAssistant,
|
||||
file: File,
|
||||
agent_ids: string[]
|
||||
): Promise<void> => {
|
||||
agentIds: string[]
|
||||
): Promise<{ backup_id: string }> => {
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
|
||||
const params = agent_ids.reduce((acc, agent_id) => {
|
||||
acc.append("agent_id", agent_id);
|
||||
return acc;
|
||||
}, new URLSearchParams());
|
||||
const params = new URLSearchParams();
|
||||
|
||||
const resp = await hass.fetchWithAuth(
|
||||
`/api/backup/upload?${params.toString()}`,
|
||||
{
|
||||
agentIds.forEach((agentId) => {
|
||||
params.append("agent_id", agentId);
|
||||
});
|
||||
|
||||
return handleFetchPromise(
|
||||
hass.fetchWithAuth(`/api/backup/upload?${params.toString()}`, {
|
||||
method: "POST",
|
||||
body: fd,
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if (!resp.ok) {
|
||||
throw new Error(`${resp.status} ${resp.statusText}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const getPreferredAgentForDownload = (agents: string[]) => {
|
||||
@@ -449,3 +446,13 @@ export const getFormattedBackupTime = memoizeOne(
|
||||
return `${formatTime(DEFAULT_OPTIMIZED_BACKUP_START_TIME, locale, config)} - ${formatTime(DEFAULT_OPTIMIZED_BACKUP_END_TIME, locale, config)}`;
|
||||
}
|
||||
);
|
||||
|
||||
export const SUPPORTED_UPLOAD_FORMAT = "application/x-tar";
|
||||
|
||||
export interface BackupUploadFileFormData {
|
||||
file?: File;
|
||||
}
|
||||
|
||||
export const INITIAL_UPLOAD_FORM_DATA: BackupUploadFileFormData = {
|
||||
file: undefined,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { handleFetchPromise } from "../util/hass-call-api";
|
||||
import type { BackupContentExtended } from "./backup";
|
||||
import type {
|
||||
BackupManagerState,
|
||||
RestoreBackupStage,
|
||||
RestoreBackupState,
|
||||
} from "./backup_manager";
|
||||
|
||||
export interface BackupOnboardingInfo {
|
||||
state: BackupManagerState;
|
||||
last_non_idle_event?: {
|
||||
manager_state: BackupManagerState;
|
||||
stage: RestoreBackupStage | null;
|
||||
state: RestoreBackupState;
|
||||
reason: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface BackupOnboardingConfig extends BackupOnboardingInfo {
|
||||
backups: BackupContentExtended[];
|
||||
}
|
||||
|
||||
export const fetchBackupOnboardingInfo = async () =>
|
||||
handleFetchPromise<BackupOnboardingConfig>(
|
||||
fetch("/api/onboarding/backup/info")
|
||||
);
|
||||
|
||||
export interface RestoreOnboardingBackupParams {
|
||||
backup_id: string;
|
||||
agent_id: string;
|
||||
password?: string;
|
||||
restore_addons?: string[];
|
||||
restore_database?: boolean;
|
||||
restore_folders?: string[];
|
||||
}
|
||||
|
||||
export const restoreOnboardingBackup = async (
|
||||
params: RestoreOnboardingBackupParams
|
||||
) =>
|
||||
handleFetchPromise(
|
||||
fetch("/api/onboarding/backup/restore", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(params),
|
||||
})
|
||||
);
|
||||
|
||||
export const uploadOnboardingBackup = async (
|
||||
file: File,
|
||||
agentIds: string[]
|
||||
): Promise<{ backup_id: string }> => {
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
|
||||
agentIds.forEach((agentId) => {
|
||||
params.append("agent_id", agentId);
|
||||
});
|
||||
|
||||
return handleFetchPromise(
|
||||
fetch(`/api/onboarding/backup/upload?${params.toString()}`, {
|
||||
method: "POST",
|
||||
body: fd,
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -19,6 +19,8 @@ export interface ConfigEntry {
|
||||
supports_remove_device: boolean;
|
||||
supports_unload: boolean;
|
||||
supports_reconfigure: boolean;
|
||||
supported_subentry_types: Record<string, { supports_reconfigure: boolean }>;
|
||||
num_subentries: number;
|
||||
pref_disable_new_entities: boolean;
|
||||
pref_disable_polling: boolean;
|
||||
disabled_by: "user" | null;
|
||||
@@ -27,6 +29,30 @@ export interface ConfigEntry {
|
||||
error_reason_translation_placeholders: Record<string, string> | null;
|
||||
}
|
||||
|
||||
export interface SubEntry {
|
||||
subentry_id: string;
|
||||
subentry_type: string;
|
||||
title: string;
|
||||
unique_id: string;
|
||||
}
|
||||
|
||||
export const getSubEntries = (hass: HomeAssistant, entry_id: string) =>
|
||||
hass.callWS<SubEntry[]>({
|
||||
type: "config_entries/subentries/list",
|
||||
entry_id,
|
||||
});
|
||||
|
||||
export const deleteSubEntry = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
subentry_id: string
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "config_entries/subentries/delete",
|
||||
entry_id,
|
||||
subentry_id,
|
||||
});
|
||||
|
||||
export type ConfigEntryMutableParams = Partial<
|
||||
Pick<
|
||||
ConfigEntry,
|
||||
|
||||
@@ -2,7 +2,11 @@ import type { Connection } from "home-assistant-js-websocket";
|
||||
import type { HaFormSchema } from "../components/ha-form/types";
|
||||
import type { ConfigEntry } from "./config_entries";
|
||||
|
||||
export type FlowType = "config_flow" | "options_flow" | "repair_flow";
|
||||
export type FlowType =
|
||||
| "config_flow"
|
||||
| "config_subentries_flow"
|
||||
| "options_flow"
|
||||
| "repair_flow";
|
||||
|
||||
export interface DataEntryFlowProgressedEvent {
|
||||
type: "data_entry_flow_progressed";
|
||||
|
||||
@@ -17,6 +17,7 @@ export {
|
||||
export interface DeviceRegistryEntry extends RegistryEntry {
|
||||
id: string;
|
||||
config_entries: string[];
|
||||
config_entries_subentries: Record<string, (string | null)[]>;
|
||||
connections: [string, string][];
|
||||
identifiers: [string, string][];
|
||||
manufacturer: string | null;
|
||||
|
||||
@@ -50,6 +50,7 @@ export interface EntityRegistryEntry extends RegistryEntry {
|
||||
icon: string | null;
|
||||
platform: string;
|
||||
config_entry_id: string | null;
|
||||
config_subentry_id: string | null;
|
||||
device_id: string | null;
|
||||
area_id: string | null;
|
||||
labels: string[];
|
||||
|
||||
+20
-40
@@ -1,6 +1,5 @@
|
||||
import { atLeastVersion } from "../../common/config/version";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { handleFetchPromise } from "../../util/hass-call-api";
|
||||
import type { HassioResponse } from "./common";
|
||||
import { hassioApiResultExtractor } from "./common";
|
||||
|
||||
@@ -82,34 +81,24 @@ export const fetchHassioBackups = async (
|
||||
};
|
||||
|
||||
export const fetchHassioBackupInfo = async (
|
||||
hass: HomeAssistant | undefined,
|
||||
hass: HomeAssistant,
|
||||
backup: string
|
||||
): Promise<HassioBackupDetail> => {
|
||||
if (hass) {
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
return hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/${
|
||||
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
||||
}/${backup}/info`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
return hassioApiResultExtractor(
|
||||
await hass.callApi<HassioResponse<HassioBackupDetail>>(
|
||||
"GET",
|
||||
`hassio/${
|
||||
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
||||
}/${backup}/info`
|
||||
)
|
||||
);
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
return hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: `/${
|
||||
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
||||
}/${backup}/info`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
// When called from onboarding we don't have hass
|
||||
return hassioApiResultExtractor(
|
||||
await handleFetchPromise(
|
||||
fetch(`/api/hassio/backups/${backup}/info`, {
|
||||
method: "GET",
|
||||
})
|
||||
await hass.callApi<HassioResponse<HassioBackupDetail>>(
|
||||
"GET",
|
||||
`hassio/${
|
||||
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
||||
}/${backup}/info`
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -240,24 +229,15 @@ export const uploadBackup = async (
|
||||
};
|
||||
|
||||
export const restoreBackup = async (
|
||||
hass: HomeAssistant | undefined,
|
||||
hass: HomeAssistant,
|
||||
type: HassioBackupDetail["type"],
|
||||
backupSlug: string,
|
||||
backupDetails: HassioPartialBackupCreateParams | HassioFullBackupCreateParams,
|
||||
useSnapshotUrl: boolean
|
||||
): Promise<void> => {
|
||||
if (hass) {
|
||||
await hass.callApi<HassioResponse<{ job_id: string }>>(
|
||||
"POST",
|
||||
`hassio/${useSnapshotUrl ? "snapshots" : "backups"}/${backupSlug}/restore/${type}`,
|
||||
backupDetails
|
||||
);
|
||||
} else {
|
||||
await handleFetchPromise(
|
||||
fetch(`/api/hassio/backups/${backupSlug}/restore/${type}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(backupDetails),
|
||||
})
|
||||
);
|
||||
}
|
||||
await hass.callApi<HassioResponse<{ job_id: string }>>(
|
||||
"POST",
|
||||
`hassio/${useSnapshotUrl ? "snapshots" : "backups"}/${backupSlug}/restore/${type}`,
|
||||
backupDetails
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { DataEntryFlowStep } from "./data_entry_flow";
|
||||
|
||||
const HEADERS = {
|
||||
"HA-Frontend-Base": `${location.protocol}//${location.host}`,
|
||||
};
|
||||
|
||||
export const createSubConfigFlow = (
|
||||
hass: HomeAssistant,
|
||||
configEntryId: string,
|
||||
subFlowType: string,
|
||||
subentry_id?: string
|
||||
) =>
|
||||
hass.callApi<DataEntryFlowStep>(
|
||||
"POST",
|
||||
"config/config_entries/subentries/flow",
|
||||
{
|
||||
handler: [configEntryId, subFlowType],
|
||||
show_advanced_options: Boolean(hass.userData?.showAdvanced),
|
||||
subentry_id,
|
||||
},
|
||||
HEADERS
|
||||
);
|
||||
|
||||
export const fetchSubConfigFlow = (hass: HomeAssistant, flowId: string) =>
|
||||
hass.callApi<DataEntryFlowStep>(
|
||||
"GET",
|
||||
`config/config_entries/subentries/flow/${flowId}`,
|
||||
undefined,
|
||||
HEADERS
|
||||
);
|
||||
|
||||
export const handleSubConfigFlowStep = (
|
||||
hass: HomeAssistant,
|
||||
flowId: string,
|
||||
data: Record<string, any>
|
||||
) =>
|
||||
hass.callApi<DataEntryFlowStep>(
|
||||
"POST",
|
||||
`config/config_entries/subentries/flow/${flowId}`,
|
||||
data,
|
||||
HEADERS
|
||||
);
|
||||
|
||||
export const deleteSubConfigFlow = (hass: HomeAssistant, flowId: string) =>
|
||||
hass.callApi("DELETE", `config/config_entries/subentries/flow/${flowId}`);
|
||||
@@ -63,6 +63,7 @@ export type TranslationCategory =
|
||||
| "entity_component"
|
||||
| "exceptions"
|
||||
| "config"
|
||||
| "config_subentries"
|
||||
| "config_panel"
|
||||
| "options"
|
||||
| "device_automation"
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import { createCollection } from "home-assistant-js-websocket";
|
||||
import type { HomeAssistant, ThemeSettings } from "../types";
|
||||
import {
|
||||
fetchFrontendUserData,
|
||||
saveFrontendUserData,
|
||||
subscribeFrontendUserData,
|
||||
} from "./frontend";
|
||||
|
||||
export interface ThemeVars {
|
||||
// Incomplete
|
||||
@@ -50,3 +56,16 @@ export const subscribeThemes = (
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
|
||||
export const SELECTED_THEME_KEY = "selectedTheme";
|
||||
|
||||
export const saveSelectedTheme = (hass: HomeAssistant, data?: ThemeSettings) =>
|
||||
saveFrontendUserData(hass.connection, SELECTED_THEME_KEY, data);
|
||||
|
||||
export const subscribeSelectedTheme = (
|
||||
hass: HomeAssistant,
|
||||
callback: (selectedTheme?: ThemeSettings | null) => void
|
||||
) => subscribeFrontendUserData(hass.connection, SELECTED_THEME_KEY, callback);
|
||||
|
||||
export const fetchSelectedTheme = (hass: HomeAssistant) =>
|
||||
fetchFrontendUserData(hass.connection, SELECTED_THEME_KEY);
|
||||
|
||||
@@ -282,6 +282,8 @@ class DataEntryFlowDialog extends LitElement {
|
||||
.flowConfig=${this._params.flowConfig}
|
||||
.step=${this._step}
|
||||
.hass=${this.hass}
|
||||
.navigateToResult=${this._params
|
||||
.navigateToResult}
|
||||
></step-flow-create-entry>
|
||||
`}
|
||||
`}
|
||||
|
||||
@@ -77,7 +77,7 @@ export class FlowPreviewGeneric extends LitElement {
|
||||
(await this._unsub)();
|
||||
this._unsub = undefined;
|
||||
}
|
||||
if (this.flowType === "repair_flow") {
|
||||
if (this.flowType !== "config_flow" && this.flowType !== "options_flow") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -147,7 +147,7 @@ class FlowPreviewTemplate extends LitElement {
|
||||
(await this._unsub)();
|
||||
this._unsub = undefined;
|
||||
}
|
||||
if (this.flowType === "repair_flow") {
|
||||
if (this.flowType !== "config_flow" && this.flowType !== "options_flow") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -16,7 +16,9 @@ export const loadConfigFlowDialog = loadDataEntryFlowDialog;
|
||||
|
||||
export const showConfigFlowDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: Omit<DataEntryFlowDialogParams, "flowConfig">
|
||||
dialogParams: Omit<DataEntryFlowDialogParams, "flowConfig"> & {
|
||||
entryId?: string;
|
||||
}
|
||||
): void =>
|
||||
showFlowDialog(element, dialogParams, {
|
||||
flowType: "config_flow",
|
||||
|
||||
@@ -148,8 +148,8 @@ export interface DataEntryFlowDialogParams {
|
||||
}) => void;
|
||||
flowConfig: FlowConfig;
|
||||
showAdvanced?: boolean;
|
||||
entryId?: string;
|
||||
dialogParentElement?: HTMLElement;
|
||||
navigateToResult?: boolean;
|
||||
}
|
||||
|
||||
export const loadDataEntryFlowDialog = () => import("./dialog-data-entry-flow");
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
import { html } from "lit";
|
||||
import type { ConfigEntry } from "../../data/config_entries";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import {
|
||||
createSubConfigFlow,
|
||||
deleteSubConfigFlow,
|
||||
fetchSubConfigFlow,
|
||||
handleSubConfigFlowStep,
|
||||
} from "../../data/sub_config_flow";
|
||||
import type { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow";
|
||||
import {
|
||||
loadDataEntryFlowDialog,
|
||||
showFlowDialog,
|
||||
} from "./show-dialog-data-entry-flow";
|
||||
|
||||
export const loadSubConfigFlowDialog = loadDataEntryFlowDialog;
|
||||
|
||||
export const showSubConfigFlowDialog = (
|
||||
element: HTMLElement,
|
||||
configEntry: ConfigEntry,
|
||||
flowType: string,
|
||||
dialogParams: Omit<DataEntryFlowDialogParams, "flowConfig"> & {
|
||||
subEntryId?: string;
|
||||
}
|
||||
): void =>
|
||||
showFlowDialog(element, dialogParams, {
|
||||
flowType: "config_subentries_flow",
|
||||
showDevices: true,
|
||||
createFlow: async (hass, handler) => {
|
||||
const [step] = await Promise.all([
|
||||
createSubConfigFlow(hass, handler, flowType, dialogParams.subEntryId),
|
||||
hass.loadFragmentTranslation("config"),
|
||||
hass.loadBackendTranslation("config_subentries", configEntry.domain),
|
||||
hass.loadBackendTranslation("selector", configEntry.domain),
|
||||
// Used as fallback if no header defined for step
|
||||
hass.loadBackendTranslation("title", configEntry.domain),
|
||||
]);
|
||||
return step;
|
||||
},
|
||||
fetchFlow: async (hass, flowId) => {
|
||||
const step = await fetchSubConfigFlow(hass, flowId);
|
||||
await hass.loadFragmentTranslation("config");
|
||||
await hass.loadBackendTranslation(
|
||||
"config_subentries",
|
||||
configEntry.domain
|
||||
);
|
||||
await hass.loadBackendTranslation("selector", configEntry.domain);
|
||||
return step;
|
||||
},
|
||||
handleFlowStep: handleSubConfigFlowStep,
|
||||
deleteFlow: deleteSubConfigFlow,
|
||||
|
||||
renderAbortDescription(hass, step) {
|
||||
const description = hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.abort.${step.reason}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
|
||||
return description
|
||||
? html`
|
||||
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
|
||||
`
|
||||
: step.reason;
|
||||
},
|
||||
|
||||
renderShowFormStepHeader(hass, step) {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.title`,
|
||||
step.description_placeholders
|
||||
) || hass.localize(`component.${configEntry.domain}.title`)
|
||||
);
|
||||
},
|
||||
|
||||
renderShowFormStepDescription(hass, step) {
|
||||
const description = hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.description`,
|
||||
step.description_placeholders
|
||||
);
|
||||
return description
|
||||
? html`
|
||||
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
|
||||
`
|
||||
: "";
|
||||
},
|
||||
|
||||
renderShowFormStepFieldLabel(hass, step, field, options) {
|
||||
if (field.type === "expandable") {
|
||||
return hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.sections.${field.name}.name`
|
||||
);
|
||||
}
|
||||
|
||||
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
|
||||
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.${prefix}data.${field.name}`
|
||||
) || field.name
|
||||
);
|
||||
},
|
||||
|
||||
renderShowFormStepFieldHelper(hass, step, field, options) {
|
||||
if (field.type === "expandable") {
|
||||
return hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.sections.${field.name}.description`
|
||||
);
|
||||
}
|
||||
|
||||
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
|
||||
|
||||
const description = hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.${prefix}data_description.${field.name}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
|
||||
return description
|
||||
? html`<ha-markdown breaks .content=${description}></ha-markdown>`
|
||||
: "";
|
||||
},
|
||||
|
||||
renderShowFormStepFieldError(hass, step, error) {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${step.translation_domain || step.translation_domain || configEntry.domain}.config_subentries.${flowType}.error.${error}`,
|
||||
step.description_placeholders
|
||||
) || error
|
||||
);
|
||||
},
|
||||
|
||||
renderShowFormStepFieldLocalizeValue(hass, _step, key) {
|
||||
return hass.localize(`component.${configEntry.domain}.selector.${key}`);
|
||||
},
|
||||
|
||||
renderShowFormStepSubmitButton(hass, step) {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.submit`
|
||||
) ||
|
||||
hass.localize(
|
||||
`ui.panel.config.integrations.config_flow.${
|
||||
step.last_step === false ? "next" : "submit"
|
||||
}`
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
renderExternalStepHeader(hass, step) {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.title`
|
||||
) ||
|
||||
hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.external_step.open_site"
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
renderExternalStepDescription(hass, step) {
|
||||
const description = hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.description`,
|
||||
step.description_placeholders
|
||||
);
|
||||
|
||||
return html`
|
||||
<p>
|
||||
${hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.external_step.description"
|
||||
)}
|
||||
</p>
|
||||
${description
|
||||
? html`
|
||||
<ha-markdown
|
||||
allowsvg
|
||||
breaks
|
||||
.content=${description}
|
||||
></ha-markdown>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
},
|
||||
|
||||
renderCreateEntryDescription(hass, step) {
|
||||
const description = hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.create_entry.${
|
||||
step.description || "default"
|
||||
}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
|
||||
return html`
|
||||
${description
|
||||
? html`
|
||||
<ha-markdown
|
||||
allowsvg
|
||||
breaks
|
||||
.content=${description}
|
||||
></ha-markdown>
|
||||
`
|
||||
: ""}
|
||||
<p>
|
||||
${hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.created_config",
|
||||
{ name: step.title }
|
||||
)}
|
||||
</p>
|
||||
`;
|
||||
},
|
||||
|
||||
renderShowFormProgressHeader(hass, step) {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.title`
|
||||
) || hass.localize(`component.${configEntry.domain}.title`)
|
||||
);
|
||||
},
|
||||
|
||||
renderShowFormProgressDescription(hass, step) {
|
||||
const description = hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.progress.${step.progress_action}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
return description
|
||||
? html`
|
||||
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
|
||||
`
|
||||
: "";
|
||||
},
|
||||
|
||||
renderMenuHeader(hass, step) {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.title`,
|
||||
step.description_placeholders
|
||||
) || hass.localize(`component.${configEntry.domain}.title`)
|
||||
);
|
||||
},
|
||||
|
||||
renderMenuDescription(hass, step) {
|
||||
const description = hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.description`,
|
||||
step.description_placeholders
|
||||
);
|
||||
return description
|
||||
? html`
|
||||
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
|
||||
`
|
||||
: "";
|
||||
},
|
||||
|
||||
renderMenuOption(hass, step, option) {
|
||||
return hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.menu_options.${option}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
},
|
||||
|
||||
renderLoadingDescription(hass, reason, handler, step) {
|
||||
if (reason !== "loading_flow" && reason !== "loading_step") {
|
||||
return "";
|
||||
}
|
||||
const domain = step?.handler || handler;
|
||||
return hass.localize(
|
||||
`ui.panel.config.integrations.config_flow.loading.${reason}`,
|
||||
{
|
||||
integration: domain
|
||||
? domainToName(hass.localize, domain)
|
||||
: // when we are continuing a config flow, we only know the ID and not the domain
|
||||
hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.loading.fallback_title"
|
||||
),
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -60,6 +60,7 @@ class StepFlowAbort extends LitElement {
|
||||
dialogClosedCallback: this.params.dialogClosedCallback,
|
||||
startFlowHandler: this.domain,
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
navigateToResult: this.params.navigateToResult,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -29,6 +29,8 @@ class StepFlowCreateEntry extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry;
|
||||
|
||||
navigateToResult = false;
|
||||
|
||||
private _devices = memoizeOne(
|
||||
(
|
||||
showDevices: boolean,
|
||||
@@ -153,7 +155,7 @@ class StepFlowCreateEntry extends LitElement {
|
||||
|
||||
private _flowDone(): void {
|
||||
fireEvent(this, "flow-update", { step: undefined });
|
||||
if (this.step.result) {
|
||||
if (this.step.result && this.navigateToResult) {
|
||||
navigate(
|
||||
`/config/integrations/integration/${this.step.result.domain}#config_entry=${this.step.result.entry_id}`
|
||||
);
|
||||
|
||||
@@ -20,6 +20,12 @@
|
||||
<meta name="color-scheme" content="dark light" />
|
||||
<%= renderTemplate("_style_base.html.template") %>
|
||||
<style>
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation: none;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: var(--primary-background-color, #fafafa);
|
||||
color: var(--primary-text-color, #212121);
|
||||
|
||||
@@ -17,7 +17,7 @@ export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
|
||||
@property({ attribute: false }) public localize: LocalizeFunc = empty;
|
||||
|
||||
// Use browser language setup before login.
|
||||
@property() public language?: string = getLocalLanguage();
|
||||
@property() public language: string = getLocalLanguage();
|
||||
|
||||
@property() public translationFragment?: string;
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ import "./onboarding-analytics";
|
||||
import "./onboarding-create-user";
|
||||
import "./onboarding-loading";
|
||||
import "./onboarding-welcome";
|
||||
import "./onboarding-restore-backup";
|
||||
import "./onboarding-welcome-links";
|
||||
import { makeDialogManager } from "../dialogs/make-dialog-manager";
|
||||
import { navigate } from "../common/navigate";
|
||||
@@ -157,8 +158,9 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
private _renderStep() {
|
||||
if (this._restoring) {
|
||||
return html`<onboarding-restore-backup
|
||||
.hass=${this.hass}
|
||||
.localize=${this.localize}
|
||||
.supervisor=${this._supervisor ?? false}
|
||||
.language=${this.language}
|
||||
>
|
||||
</onboarding-restore-backup>`;
|
||||
}
|
||||
@@ -166,8 +168,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
if (this._init) {
|
||||
return html`<onboarding-welcome
|
||||
.localize=${this.localize}
|
||||
.language=${this.language}
|
||||
.supervisor=${this._supervisor}
|
||||
></onboarding-welcome>`;
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
}
|
||||
}
|
||||
if (changedProps.has("language")) {
|
||||
document.querySelector("html")!.setAttribute("lang", this.language!);
|
||||
document.querySelector("html")!.setAttribute("lang", this.language);
|
||||
}
|
||||
if (changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
@@ -272,10 +272,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
"Home Assistant OS",
|
||||
"Home Assistant Supervised",
|
||||
].includes(response.installation_type);
|
||||
if (this._supervisor) {
|
||||
// Only load if we have supervisor
|
||||
import("./onboarding-restore-backup");
|
||||
}
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
@@ -454,7 +450,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
subscribeOne(conn, subscribeUser),
|
||||
]);
|
||||
this.initializeHass(auth, conn);
|
||||
if (this.language && this.language !== this.hass!.language) {
|
||||
if (this.language !== this.hass!.language) {
|
||||
this._updateHass({
|
||||
locale: { ...this.hass!.locale, language: this.language },
|
||||
language: this.language,
|
||||
|
||||
@@ -1,136 +1,336 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup";
|
||||
import "../../hassio/src/components/hassio-upload-backup";
|
||||
import "./restore-backup/onboarding-restore-backup-upload";
|
||||
import "./restore-backup/onboarding-restore-backup-details";
|
||||
import "./restore-backup/onboarding-restore-backup-restore";
|
||||
import "./restore-backup/onboarding-restore-backup-status";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-ansi-to-html";
|
||||
import "../components/ha-card";
|
||||
import "../components/ha-icon-button-arrow-prev";
|
||||
import "../components/ha-circular-progress";
|
||||
import "../components/ha-alert";
|
||||
import "../components/ha-button";
|
||||
import { fetchInstallationType } from "../data/onboarding";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./onboarding-loading";
|
||||
import { onBoardingStyles } from "./styles";
|
||||
import { removeSearchParam } from "../common/url/search-params";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { onBoardingStyles } from "./styles";
|
||||
import {
|
||||
fetchBackupOnboardingInfo,
|
||||
type BackupOnboardingConfig,
|
||||
type BackupOnboardingInfo,
|
||||
} from "../data/backup_onboarding";
|
||||
import type { BackupContentExtended, BackupData } from "../data/backup";
|
||||
import { showConfirmationDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { storage } from "../common/decorators/storage";
|
||||
|
||||
const STATUS_INTERVAL_IN_MS = 5000;
|
||||
|
||||
@customElement("onboarding-restore-backup")
|
||||
class OnboardingRestoreBackup extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||
|
||||
@property() public language!: string;
|
||||
|
||||
@state() private _restoring = false;
|
||||
@property({ type: Boolean }) public supervisor = false;
|
||||
|
||||
@state() private _backupSlug?: string;
|
||||
@state() private _view:
|
||||
| "loading"
|
||||
| "upload"
|
||||
| "select_data"
|
||||
| "confirm_restore"
|
||||
| "status" = "loading";
|
||||
|
||||
@state() private _backup?: BackupContentExtended;
|
||||
|
||||
@state() private _backupInfo?: BackupOnboardingInfo;
|
||||
|
||||
@state() private _selectedData?: BackupData;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _failed?: boolean;
|
||||
|
||||
@storage({
|
||||
key: "onboarding-restore-backup-backup-id",
|
||||
})
|
||||
private _backupId?: string;
|
||||
|
||||
@storage({
|
||||
key: "onboarding-restore-running",
|
||||
})
|
||||
private _restoreRunning?: boolean;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this._restoring
|
||||
? html`<h1>
|
||||
${this.localize("ui.panel.page-onboarding.restore.in_progress")}
|
||||
</h1>
|
||||
<ha-alert alert-type="info">
|
||||
${this.localize("ui.panel.page-onboarding.restore.in_progress")}
|
||||
</ha-alert>
|
||||
<onboarding-loading></onboarding-loading>`
|
||||
: html` <h1>
|
||||
${this.localize("ui.panel.page-onboarding.restore.header")}
|
||||
</h1>
|
||||
<hassio-upload-backup
|
||||
@backup-uploaded=${this._backupUploaded}
|
||||
@backup-cleared=${this._backupCleared}
|
||||
.hass=${this.hass}
|
||||
.localize=${this.localize}
|
||||
></hassio-upload-backup>`}
|
||||
<div class="footer">
|
||||
<ha-button @click=${this._back} .disabled=${this._restoring}>
|
||||
${this.localize("ui.panel.page-onboarding.back")}
|
||||
</ha-button>
|
||||
${this._backupSlug
|
||||
? html`<ha-button
|
||||
@click=${this._showBackupDialog}
|
||||
.disabled=${this._restoring}
|
||||
${
|
||||
this._view !== "status" || this._failed
|
||||
? html`<ha-icon-button-arrow-prev
|
||||
.label=${this.localize("ui.panel.page-onboarding.restore.back")}
|
||||
@click=${this._back}
|
||||
></ha-icon-button-arrow-prev>`
|
||||
: nothing
|
||||
}
|
||||
</ha-icon-button>
|
||||
<h1>${this.localize("ui.panel.page-onboarding.restore.header")}</h1>
|
||||
${
|
||||
this._error || (this._failed && this._view !== "status")
|
||||
? html`<ha-alert
|
||||
alert-type="error"
|
||||
.title=${this._failed && this._view !== "status"
|
||||
? this.localize("ui.panel.page-onboarding.restore.failed")
|
||||
: ""}
|
||||
>
|
||||
${this.localize("ui.panel.page-onboarding.restore.restore")}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._failed && this._view !== "status"
|
||||
? this.localize(
|
||||
`ui.panel.page-onboarding.restore.${this._backupInfo?.last_non_idle_event?.reason === "password_incorrect" ? "failed_wrong_password_description" : "failed_description"}`
|
||||
)
|
||||
: this._error}
|
||||
</ha-alert>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._view === "loading"
|
||||
? html`<div class="loading">
|
||||
<ha-circular-progress indeterminate></ha-circular-progress>
|
||||
</div>`
|
||||
: this._view === "upload"
|
||||
? html`
|
||||
<onboarding-restore-backup-upload
|
||||
.supervisor=${this.supervisor}
|
||||
.localize=${this.localize}
|
||||
@backup-uploaded=${this._backupUploaded}
|
||||
></onboarding-restore-backup-upload>
|
||||
`
|
||||
: this._view === "select_data"
|
||||
? html`<onboarding-restore-backup-details
|
||||
.localize=${this.localize}
|
||||
.backup=${this._backup!}
|
||||
@backup-restore=${this._restore}
|
||||
></onboarding-restore-backup-details>`
|
||||
: this._view === "confirm_restore"
|
||||
? html`<onboarding-restore-backup-restore
|
||||
.localize=${this.localize}
|
||||
.backup=${this._backup!}
|
||||
.supervisor=${this.supervisor}
|
||||
.selectedData=${this._selectedData!}
|
||||
@restore-started=${this._restoreStarted}
|
||||
></onboarding-restore-backup-restore>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._view === "status" && this._backupInfo
|
||||
? html`<onboarding-restore-backup-status
|
||||
.localize=${this.localize}
|
||||
.backupInfo=${this._backupInfo}
|
||||
@show-backup-upload=${this._reupload}
|
||||
></onboarding-restore-backup-status>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
["select_data", "confirm_restore"].includes(this._view) && this._backup
|
||||
? html`<div class="backup-summary-wrapper">
|
||||
<ha-backup-details-summary
|
||||
translation-key-panel="page-onboarding.restore"
|
||||
show-upload-another
|
||||
.backup=${this._backup}
|
||||
.localize=${this.localize}
|
||||
@show-backup-upload=${this._reupload}
|
||||
.isHassio=${this.supervisor}
|
||||
></ha-backup-details-summary>
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _back(): void {
|
||||
navigate(`${location.pathname}?${removeSearchParam("page")}`);
|
||||
}
|
||||
|
||||
private _backupUploaded(ev) {
|
||||
const backup = ev.detail.backup;
|
||||
this._backupSlug = backup.slug;
|
||||
this._showBackupDialog();
|
||||
}
|
||||
|
||||
private _backupCleared() {
|
||||
this._backupSlug = undefined;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
this._loadBackupInfo();
|
||||
}
|
||||
|
||||
private async _checkRestoreStatus(): Promise<void> {
|
||||
if (this._restoring) {
|
||||
try {
|
||||
await fetchInstallationType();
|
||||
} catch (err: any) {
|
||||
private async _loadBackupInfo() {
|
||||
let onboardingInfo: BackupOnboardingConfig;
|
||||
try {
|
||||
onboardingInfo = await fetchBackupOnboardingInfo();
|
||||
} catch (err: any) {
|
||||
if (this._restoreRunning) {
|
||||
if (
|
||||
(err as Error).message === "unauthorized" ||
|
||||
(err as Error).message === "not_found"
|
||||
err.error === "Request error" ||
|
||||
// core can restart but haven't loaded the backup integration yet
|
||||
(err.status_code === 500 && err.body?.error === "backup_disabled")
|
||||
) {
|
||||
// core is down because of restore, keep trying
|
||||
this._scheduleLoadBackupInfo();
|
||||
return;
|
||||
}
|
||||
|
||||
// core seems to be back up restored
|
||||
if (err.status_code === 404) {
|
||||
this._restoreRunning = undefined;
|
||||
this._backupId = undefined;
|
||||
window.location.replace("/");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._error = err?.message || "Cannot get backup info";
|
||||
|
||||
// if we are in an unknown state, show upload
|
||||
if (this._view === "loading") {
|
||||
this._view = "upload";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
last_non_idle_event: lastNonIdleEvent,
|
||||
state: currentState,
|
||||
backups,
|
||||
} = onboardingInfo;
|
||||
|
||||
this._backupInfo = {
|
||||
state: currentState,
|
||||
last_non_idle_event: lastNonIdleEvent,
|
||||
};
|
||||
|
||||
if (this._backupId) {
|
||||
this._backup = backups.find(
|
||||
({ backup_id }) => backup_id === this._backupId
|
||||
);
|
||||
}
|
||||
|
||||
const failedRestore =
|
||||
lastNonIdleEvent?.manager_state === "restore_backup" &&
|
||||
lastNonIdleEvent?.state === "failed";
|
||||
|
||||
if (failedRestore) {
|
||||
this._failed = true;
|
||||
}
|
||||
|
||||
if (this._restoreRunning) {
|
||||
this._view = "status";
|
||||
if (failedRestore || currentState !== "restore_backup") {
|
||||
this._failed = true;
|
||||
this._restoreRunning = undefined;
|
||||
} else {
|
||||
this._scheduleLoadBackupInfo();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this._backup &&
|
||||
// after backup was uploaded
|
||||
(lastNonIdleEvent?.manager_state === "receive_backup" ||
|
||||
// when restore was confirmed but failed to start (for example, encryption key was wrong)
|
||||
failedRestore)
|
||||
) {
|
||||
if (!this.supervisor && this._backup.homeassistant_included) {
|
||||
this._selectedData = {
|
||||
homeassistant_included: true,
|
||||
folders: [],
|
||||
addons: [],
|
||||
homeassistant_version: this._backup.homeassistant_version,
|
||||
database_included: this._backup.database_included,
|
||||
};
|
||||
// skip select data when supervisor is not available and backup includes HA
|
||||
this._view = "confirm_restore";
|
||||
} else {
|
||||
this._view = "select_data";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// show upload as default
|
||||
this._view = "upload";
|
||||
}
|
||||
|
||||
private _scheduleLoadBackupInfo() {
|
||||
setTimeout(() => this._loadBackupInfo(), STATUS_INTERVAL_IN_MS);
|
||||
}
|
||||
|
||||
private async _backupUploaded(ev: CustomEvent) {
|
||||
this._backupId = ev.detail.backupId;
|
||||
await this._loadBackupInfo();
|
||||
}
|
||||
|
||||
private async _restoreStarted() {
|
||||
if (this._backupInfo) {
|
||||
this._backupInfo.state = "restore_backup";
|
||||
}
|
||||
this._view = "status";
|
||||
this._restoreRunning = true;
|
||||
await this._loadBackupInfo();
|
||||
}
|
||||
|
||||
private async _back() {
|
||||
if (this._view === "upload" || (this._view === "status" && this._failed)) {
|
||||
navigate(`${location.pathname}?${removeSearchParam("page")}`);
|
||||
} else {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.localize(
|
||||
"ui.panel.page-onboarding.restore.cancel_restore.title"
|
||||
),
|
||||
text: this.localize(
|
||||
"ui.panel.page-onboarding.restore.cancel_restore.text"
|
||||
),
|
||||
confirmText: this.localize(
|
||||
"ui.panel.page-onboarding.restore.cancel_restore.yes"
|
||||
),
|
||||
dismissText: this.localize(
|
||||
"ui.panel.page-onboarding.restore.cancel_restore.no"
|
||||
),
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
navigate(`${location.pathname}?${removeSearchParam("page")}`);
|
||||
}
|
||||
}
|
||||
|
||||
private _scheduleCheckRestoreStatus(): void {
|
||||
setTimeout(() => this._checkRestoreStatus(), 1000);
|
||||
private _restore(ev: CustomEvent) {
|
||||
if (!this._backup || !ev.detail.selectedData) {
|
||||
return;
|
||||
}
|
||||
this._selectedData = ev.detail.selectedData;
|
||||
|
||||
this._view = "confirm_restore";
|
||||
}
|
||||
|
||||
private _showBackupDialog(): void {
|
||||
showHassioBackupDialog(this, {
|
||||
slug: this._backupSlug!,
|
||||
onboarding: true,
|
||||
localize: this.localize,
|
||||
onRestoring: () => {
|
||||
this._restoring = true;
|
||||
this._scheduleCheckRestoreStatus();
|
||||
},
|
||||
});
|
||||
private _reupload() {
|
||||
this._backup = undefined;
|
||||
this._backupId = undefined;
|
||||
this._view = "upload";
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
onBoardingStyles,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
hassio-upload-backup {
|
||||
width: 100%;
|
||||
}
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
static styles = [
|
||||
onBoardingStyles,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
ha-icon-button-arrow-prev {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
}
|
||||
ha-card {
|
||||
width: 100%;
|
||||
}
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 32px;
|
||||
}
|
||||
.backup-summary-wrapper {
|
||||
margin-top: 24px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type { HomeAssistant } from "../types";
|
||||
@@ -13,8 +13,6 @@ class OnboardingWelcome extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||
|
||||
@property({ type: Boolean }) public supervisor = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<h1>${this.localize("ui.panel.page-onboarding.welcome.header")}</h1>
|
||||
@@ -24,11 +22,9 @@ class OnboardingWelcome extends LitElement {
|
||||
${this.localize("ui.panel.page-onboarding.welcome.start")}
|
||||
</ha-button>
|
||||
|
||||
${this.supervisor
|
||||
? html`<ha-button @click=${this._restoreBackup}>
|
||||
${this.localize("ui.panel.page-onboarding.welcome.restore_backup")}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
<ha-button @click=${this._restoreBackup}>
|
||||
${this.localize("ui.panel.page-onboarding.welcome.restore_backup")}
|
||||
</ha-button>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { css, html, LitElement, type CSSResultGroup } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../components/ha-card";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/ha-button";
|
||||
import "../../panels/config/backup/components/ha-backup-details-restore";
|
||||
import "../../panels/config/backup/components/ha-backup-details-summary";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import type { BackupContentExtended } from "../../data/backup";
|
||||
|
||||
@customElement("onboarding-restore-backup-details")
|
||||
class OnboardingRestoreBackupDetails extends LitElement {
|
||||
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false }) public backup!: BackupContentExtended;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
${this.backup.homeassistant_included
|
||||
? html`<ha-backup-details-restore
|
||||
.backup=${this.backup}
|
||||
.localize=${this.localize}
|
||||
translation-key-panel="page-onboarding.restore"
|
||||
ha-required
|
||||
></ha-backup-details-restore>`
|
||||
: html`
|
||||
<ha-alert alert-type="error">
|
||||
${this.localize(
|
||||
"ui.panel.page-onboarding.restore.details.home_assistant_missing"
|
||||
)}
|
||||
</ha-alert>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
padding: 28px 20px 0;
|
||||
}
|
||||
ha-backup-details-restore {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"onboarding-restore-backup-details": OnboardingRestoreBackupDetails;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../components/ha-card";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/buttons/ha-progress-button";
|
||||
import "../../components/ha-password-field";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import {
|
||||
CORE_LOCAL_AGENT,
|
||||
HASSIO_LOCAL_AGENT,
|
||||
type BackupContentExtended,
|
||||
type BackupData,
|
||||
} from "../../data/backup";
|
||||
import { restoreOnboardingBackup } from "../../data/backup_onboarding";
|
||||
import type { HaProgressButton } from "../../components/buttons/ha-progress-button";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
@customElement("onboarding-restore-backup-restore")
|
||||
class OnboardingRestoreBackupRestore extends LitElement {
|
||||
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false }) public backup!: BackupContentExtended;
|
||||
|
||||
@property({ attribute: false })
|
||||
public selectedData!: BackupData;
|
||||
|
||||
@property({ type: Boolean }) public supervisor = false;
|
||||
|
||||
@state() private _encryptionKey = "";
|
||||
|
||||
@state() private _encryptionKeyWrong = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _loading = false;
|
||||
|
||||
render() {
|
||||
const agentId = this.supervisor ? HASSIO_LOCAL_AGENT : CORE_LOCAL_AGENT;
|
||||
const backupProtected = this.backup.agents[agentId].protected;
|
||||
|
||||
return html`
|
||||
${this.backup.homeassistant_included &&
|
||||
!this.supervisor &&
|
||||
(this.backup.addons.length > 0 || this.backup.folders.length > 0)
|
||||
? html`<ha-alert alert-type="warning" class="supervisor-warning">
|
||||
${this.localize(
|
||||
"ui.panel.page-onboarding.restore.details.addons_unsupported"
|
||||
)}
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
<ha-card
|
||||
.header=${this.localize("ui.panel.page-onboarding.restore.restore")}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert> `
|
||||
: nothing}
|
||||
<p>
|
||||
${this.localize(
|
||||
"ui.panel.page-onboarding.restore.confirm_restore_full_backup_text"
|
||||
)}
|
||||
</p>
|
||||
${backupProtected
|
||||
? html`<p>
|
||||
${this.localize(
|
||||
"ui.panel.page-onboarding.restore.details.restore.encryption.title"
|
||||
)}
|
||||
</p>
|
||||
${this._encryptionKeyWrong
|
||||
? html`
|
||||
<ha-alert alert-type="error">
|
||||
${this.localize(
|
||||
"ui.panel.page-onboarding.restore.details.restore.encryption.incorrect_key"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
<ha-password-field
|
||||
.disabled=${this._loading}
|
||||
@input=${this._encryptionKeyChanged}
|
||||
.label=${this.localize(
|
||||
"ui.panel.page-onboarding.restore.details.restore.encryption.input_label"
|
||||
)}
|
||||
.value=${this._encryptionKey}
|
||||
></ha-password-field>`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-progress-button
|
||||
.progress=${this._loading}
|
||||
.disabled=${this._loading ||
|
||||
(backupProtected && this._encryptionKey === "")}
|
||||
@click=${this._startRestore}
|
||||
destructive
|
||||
>
|
||||
${this.localize(
|
||||
"ui.panel.page-onboarding.restore.details.restore.action"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _encryptionKeyChanged(ev): void {
|
||||
this._encryptionKey = ev.target.value;
|
||||
}
|
||||
|
||||
private async _startRestore(ev: CustomEvent): Promise<void> {
|
||||
const button = ev.currentTarget as HaProgressButton;
|
||||
this._loading = true;
|
||||
this._error = undefined;
|
||||
this._encryptionKeyWrong = false;
|
||||
|
||||
const backupAgent = this.supervisor ? HASSIO_LOCAL_AGENT : CORE_LOCAL_AGENT;
|
||||
|
||||
try {
|
||||
await restoreOnboardingBackup({
|
||||
agent_id: backupAgent,
|
||||
backup_id: this.backup.backup_id,
|
||||
password: this._encryptionKey || undefined,
|
||||
restore_addons: this.selectedData.addons.map((addon) => addon.slug),
|
||||
restore_database: this.selectedData.database_included,
|
||||
restore_folders: this.selectedData.folders,
|
||||
});
|
||||
button.actionSuccess();
|
||||
fireEvent(this, "restore-started");
|
||||
} catch (err: any) {
|
||||
if (err.error === "Request error") {
|
||||
// core can shutdown before we get a response
|
||||
button.actionSuccess();
|
||||
fireEvent(this, "restore-started");
|
||||
return;
|
||||
}
|
||||
|
||||
button.actionError();
|
||||
if (err.body?.code === "incorrect_password") {
|
||||
this._encryptionKeyWrong = true;
|
||||
} else {
|
||||
this._error =
|
||||
err.body?.message || err.message || "Unknown error occurred";
|
||||
}
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
padding: 28px 20px 0;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.supervisor-warning {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"onboarding-restore-backup-restore": OnboardingRestoreBackupRestore;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"restore-started";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../components/ha-card";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/ha-button";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import type { BackupOnboardingInfo } from "../../data/backup_onboarding";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { removeSearchParam } from "../../common/url/search-params";
|
||||
|
||||
@customElement("onboarding-restore-backup-status")
|
||||
class OnboardingRestoreBackupStatus extends LitElement {
|
||||
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false })
|
||||
public backupInfo!: BackupOnboardingInfo;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<ha-card
|
||||
.header=${this.localize(
|
||||
`ui.panel.page-onboarding.restore.${this.backupInfo.state === "restore_backup" ? "in_progress" : "failed"}`
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this.backupInfo.state === "restore_backup"
|
||||
? html`
|
||||
<div class="loading">
|
||||
<ha-circular-progress indeterminate></ha-circular-progress>
|
||||
</div>
|
||||
<p>
|
||||
${this.localize(
|
||||
"ui.panel.page-onboarding.restore.in_progress_description"
|
||||
)}
|
||||
</p>
|
||||
`
|
||||
: html`
|
||||
<ha-alert alert-type="error">
|
||||
${this.localize(
|
||||
"ui.panel.page-onboarding.restore.failed_status_description"
|
||||
)}
|
||||
</ha-alert>
|
||||
${this.backupInfo.last_non_idle_event?.reason
|
||||
? html`
|
||||
<div class="failed">
|
||||
<h4>Error:</h4>
|
||||
${this.backupInfo.last_non_idle_event?.reason}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
`}
|
||||
</div>
|
||||
${this.backupInfo.state !== "restore_backup"
|
||||
? html`<div class="card-actions">
|
||||
<ha-button @click=${this._uploadAnother} destructive>
|
||||
${this.localize(
|
||||
`ui.panel.page-onboarding.restore.details.summary.upload_another`
|
||||
)}
|
||||
</ha-button>
|
||||
<ha-button @click=${this._home} destructive>
|
||||
${this.localize(
|
||||
`ui.panel.page-onboarding.restore.details.summary.home`
|
||||
)}
|
||||
</ha-button>
|
||||
</div>`
|
||||
: nothing}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _uploadAnother() {
|
||||
fireEvent(this, "show-backup-upload");
|
||||
}
|
||||
|
||||
private _home() {
|
||||
navigate(`${location.pathname}?${removeSearchParam("page")}`);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
padding: 28px 20px 0;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 32px;
|
||||
}
|
||||
p {
|
||||
text-align: center;
|
||||
padding: 0 16px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.failed {
|
||||
padding: 16px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"onboarding-restore-backup-status": OnboardingRestoreBackupStatus;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"restore-started";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { mdiFolderUpload } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../components/ha-card";
|
||||
import "../../components/ha-file-upload";
|
||||
import "../../components/ha-alert";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import {
|
||||
CORE_LOCAL_AGENT,
|
||||
HASSIO_LOCAL_AGENT,
|
||||
SUPPORTED_UPLOAD_FORMAT,
|
||||
} from "../../data/backup";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { uploadOnboardingBackup } from "../../data/backup_onboarding";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"backup-uploaded": { backupId: string };
|
||||
}
|
||||
}
|
||||
@customElement("onboarding-restore-backup-upload")
|
||||
class OnboardingRestoreBackupUpload extends LitElement {
|
||||
@property({ type: Boolean }) public supervisor = false;
|
||||
|
||||
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||
|
||||
@state() private _uploading = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<ha-card
|
||||
.header=${this.localize(
|
||||
"ui.panel.page-onboarding.restore.upload_backup"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
<ha-file-upload
|
||||
.uploading=${this._uploading}
|
||||
.icon=${mdiFolderUpload}
|
||||
accept=${SUPPORTED_UPLOAD_FORMAT}
|
||||
.localize=${this.localize}
|
||||
.label=${this.localize(
|
||||
"ui.panel.page-onboarding.restore.upload_input_label"
|
||||
)}
|
||||
.secondary=${this.localize(
|
||||
"ui.panel.page-onboarding.restore.upload_secondary"
|
||||
)}
|
||||
.supports=${this.localize(
|
||||
"ui.panel.page-onboarding.restore.upload_supports_tar"
|
||||
)}
|
||||
.deleteLabel=${this.localize(
|
||||
"ui.panel.page-onboarding.restore.delete"
|
||||
)}
|
||||
.uploadingLabel=${this.localize(
|
||||
"ui.panel.page-onboarding.restore.uploading"
|
||||
)}
|
||||
@file-picked=${this._filePicked}
|
||||
></ha-file-upload>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _filePicked(ev: HASSDomEvent<{ files: File[] }>) {
|
||||
this._error = undefined;
|
||||
const file = ev.detail.files[0];
|
||||
|
||||
if (!file || file.type !== SUPPORTED_UPLOAD_FORMAT) {
|
||||
showAlertDialog(this, {
|
||||
title: this.localize(
|
||||
"ui.panel.page-onboarding.restore.unsupported.title"
|
||||
),
|
||||
text: this.localize(
|
||||
"ui.panel.page-onboarding.restore.unsupported.text"
|
||||
),
|
||||
confirmText: this.localize("ui.panel.page-onboarding.restore.ok"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const agentIds = this.supervisor
|
||||
? [HASSIO_LOCAL_AGENT]
|
||||
: [CORE_LOCAL_AGENT];
|
||||
|
||||
this._uploading = true;
|
||||
try {
|
||||
const { backup_id } = await uploadOnboardingBackup(file, agentIds);
|
||||
fireEvent(this, "backup-uploaded", { backupId: backup_id });
|
||||
} catch (err: any) {
|
||||
this._error =
|
||||
typeof err.body === "string"
|
||||
? err.body
|
||||
: err.body?.message || err.message || "Unknown error occurred";
|
||||
} finally {
|
||||
this._uploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
width: 100%;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"onboarding-restore-backup-upload": OnboardingRestoreBackupUpload;
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export interface BackupAddonItem {
|
||||
|
||||
@customElement("ha-backup-addons-picker")
|
||||
export class HaBackupAddonsPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public addons!: BackupAddonItem[];
|
||||
|
||||
@@ -32,7 +32,7 @@ export class HaBackupAddonsPicker extends LitElement {
|
||||
|
||||
private _addons = memoizeOne((addons: BackupAddonItem[]) =>
|
||||
addons.sort((a, b) =>
|
||||
stringCompare(a.name, b.name, this.hass.locale.language)
|
||||
stringCompare(a.name, b.name, this.hass?.locale?.language)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -47,23 +47,32 @@ interface SelectedItems {
|
||||
|
||||
@customElement("ha-backup-data-picker")
|
||||
export class HaBackupDataPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public data!: BackupData;
|
||||
|
||||
@property({ attribute: false }) public value?: BackupData;
|
||||
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
|
||||
@property({ type: Array, attribute: "required-items" })
|
||||
public requiredItems: string[] = [];
|
||||
|
||||
@property({ attribute: "translation-key-panel" }) public translationKeyPanel:
|
||||
| "page-onboarding.restore"
|
||||
| "config.backup" = "config.backup";
|
||||
|
||||
@state() public _addonIcons: Record<string, boolean> = {};
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues): void {
|
||||
super.firstUpdated(changedProps);
|
||||
if (isComponentLoaded(this.hass, "hassio")) {
|
||||
if (this.hass && isComponentLoaded(this.hass, "hassio")) {
|
||||
this._fetchAddonInfo();
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchAddonInfo() {
|
||||
const { addons } = await fetchHassioAddonsInfo(this.hass);
|
||||
const { addons } = await fetchHassioAddonsInfo(this.hass!);
|
||||
this._addonIcons = addons.reduce<Record<string, boolean>>(
|
||||
(acc, addon) => ({
|
||||
...acc,
|
||||
@@ -74,16 +83,14 @@ export class HaBackupDataPicker extends LitElement {
|
||||
}
|
||||
|
||||
private _homeAssistantItems = memoizeOne(
|
||||
(data: BackupData, _localize: LocalizeFunc) => {
|
||||
(data: BackupData, localize: LocalizeFunc) => {
|
||||
const items: CheckBoxItem[] = [];
|
||||
|
||||
if (data.homeassistant_included) {
|
||||
items.push({
|
||||
label: data.database_included
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.backup.data_picker.settings_and_history"
|
||||
)
|
||||
: this.hass.localize("ui.panel.config.backup.data_picker.settings"),
|
||||
label: localize(
|
||||
`ui.panel.${this.translationKeyPanel}.data_picker.${data.database_included ? "settings_and_history" : "settings"}`
|
||||
),
|
||||
id: "config",
|
||||
version: data.homeassistant_version,
|
||||
});
|
||||
@@ -99,18 +106,22 @@ export class HaBackupDataPicker extends LitElement {
|
||||
);
|
||||
|
||||
private _localizeFolder(folder: string): string {
|
||||
const localize = this.localize || this.hass!.localize;
|
||||
|
||||
switch (folder) {
|
||||
case "media":
|
||||
return this.hass.localize("ui.panel.config.backup.data_picker.media");
|
||||
return localize(
|
||||
`ui.panel.${this.translationKeyPanel}.data_picker.media`
|
||||
);
|
||||
case "share":
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.backup.data_picker.share_folder"
|
||||
return localize(
|
||||
`ui.panel.${this.translationKeyPanel}.data_picker.share_folder`
|
||||
);
|
||||
case "ssl":
|
||||
return this.hass.localize("ui.panel.config.backup.data_picker.ssl");
|
||||
return localize(`ui.panel.${this.translationKeyPanel}.data_picker.ssl`);
|
||||
case "addons/local":
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.backup.data_picker.local_addons"
|
||||
return localize(
|
||||
`ui.panel.${this.translationKeyPanel}.data_picker.local_addons`
|
||||
);
|
||||
}
|
||||
return capitalizeFirstLetter(folder);
|
||||
@@ -215,14 +226,13 @@ export class HaBackupDataPicker extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const homeAssistantItems = this._homeAssistantItems(
|
||||
this.data,
|
||||
this.hass.localize
|
||||
);
|
||||
const localize = this.localize || this.hass!.localize;
|
||||
|
||||
const homeAssistantItems = this._homeAssistantItems(this.data, localize);
|
||||
|
||||
const addonsItems = this._addonsItems(
|
||||
this.data,
|
||||
this.hass.localize,
|
||||
localize,
|
||||
this._addonIcons
|
||||
);
|
||||
|
||||
@@ -247,6 +257,7 @@ export class HaBackupDataPicker extends LitElement {
|
||||
selectedItems.homeassistant.length <
|
||||
homeAssistantItems.length}
|
||||
@change=${this._sectionChanged}
|
||||
?disabled=${this.requiredItems.length > 0}
|
||||
></ha-checkbox>
|
||||
</ha-formfield>
|
||||
<div class="items">
|
||||
@@ -266,6 +277,7 @@ export class HaBackupDataPicker extends LitElement {
|
||||
item.id
|
||||
)}
|
||||
@change=${this._homeassistantChanged}
|
||||
.disabled=${this.requiredItems.includes(item.id)}
|
||||
></ha-checkbox>
|
||||
</ha-formfield>
|
||||
`
|
||||
@@ -280,8 +292,8 @@ export class HaBackupDataPicker extends LitElement {
|
||||
<ha-formfield>
|
||||
<ha-backup-formfield-label
|
||||
slot="label"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.backup.data_picker.addons"
|
||||
.label=${localize(
|
||||
`ui.panel.${this.translationKeyPanel}.data_picker.addons`
|
||||
)}
|
||||
.iconPath=${mdiPuzzle}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-button";
|
||||
import "./ha-backup-data-picker";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import type {
|
||||
BackupContentExtended,
|
||||
BackupData,
|
||||
} from "../../../../data/backup";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
|
||||
@customElement("ha-backup-details-restore")
|
||||
class HaBackupDetailsRestore extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||
|
||||
@property({ type: Object }) public backup!: BackupContentExtended;
|
||||
|
||||
@property({ type: Boolean, attribute: "ha-required" })
|
||||
public haRequired = false;
|
||||
|
||||
@property({ attribute: "translation-key-panel" }) public translationKeyPanel:
|
||||
| "page-onboarding.restore"
|
||||
| "config.backup" = "config.backup";
|
||||
|
||||
@state() private _selectedData?: BackupData;
|
||||
|
||||
protected willUpdate() {
|
||||
if (!this.hasUpdated && this.haRequired) {
|
||||
this._selectedData = {
|
||||
homeassistant_included: true,
|
||||
folders: [],
|
||||
addons: [],
|
||||
homeassistant_version: this.backup.homeassistant_version,
|
||||
database_included: this.backup.database_included,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.details.restore.title`
|
||||
)}
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<ha-backup-data-picker
|
||||
.translationKeyPanel=${this.translationKeyPanel}
|
||||
.localize=${this.localize}
|
||||
.hass=${this.hass}
|
||||
.data=${this.backup}
|
||||
.value=${this._selectedData}
|
||||
@value-changed=${this._selectedBackupChanged}
|
||||
.requiredItems=${this._isHomeAssistantRequired(this.haRequired)}
|
||||
>
|
||||
</ha-backup-data-picker>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button
|
||||
@click=${this._restore}
|
||||
.disabled=${this._isRestoreDisabled}
|
||||
destructive
|
||||
>
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.details.restore.action`
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _restore() {
|
||||
fireEvent(this, "backup-restore", { selectedData: this._selectedData });
|
||||
}
|
||||
|
||||
private _selectedBackupChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._selectedData = ev.detail.value;
|
||||
}
|
||||
|
||||
private _isHomeAssistantRequired = memoizeOne((required: boolean) =>
|
||||
required ? ["config"] : []
|
||||
);
|
||||
|
||||
private get _isRestoreDisabled(): boolean {
|
||||
return (
|
||||
!this._selectedData ||
|
||||
(this.haRequired && !this._selectedData.homeassistant_included) ||
|
||||
!(
|
||||
this._selectedData?.database_included ||
|
||||
this._selectedData?.homeassistant_included ||
|
||||
this._selectedData.addons.length ||
|
||||
this._selectedData.folders.length
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
max-width: 690px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
gap: 24px;
|
||||
display: grid;
|
||||
}
|
||||
.card-content {
|
||||
padding: 0 20px;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
ha-md-list {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
--md-list-item-two-line-container-height: 64px;
|
||||
}
|
||||
ha-md-list-item [slot="supporting-text"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
line-height: normal;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-backup-details-restore": HaBackupDetailsRestore;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"backup-restore": { selectedData?: BackupData };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-button";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDateTimeWithBrowserDefaults,
|
||||
} from "../../../../common/datetime/format_date_time";
|
||||
import {
|
||||
computeBackupSize,
|
||||
computeBackupType,
|
||||
type BackupContentExtended,
|
||||
} from "../../../../data/backup";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { bytesToString } from "../../../../util/bytes-to-string";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"show-backup-upload": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("ha-backup-details-summary")
|
||||
class HaBackupDetailsSummary extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||
|
||||
@property({ type: Object }) public backup!: BackupContentExtended;
|
||||
|
||||
@property({ type: Boolean, attribute: "hassio" }) public isHassio = false;
|
||||
|
||||
@property({ attribute: "translation-key-panel" }) public translationKeyPanel:
|
||||
| "page-onboarding.restore"
|
||||
| "config.backup" = "config.backup";
|
||||
|
||||
@property({ type: Boolean, attribute: "show-upload-another" })
|
||||
public showUploadAnother = false;
|
||||
|
||||
render() {
|
||||
const backupDate = new Date(this.backup.date);
|
||||
const formattedDate = this.hass
|
||||
? formatDateTime(backupDate, this.hass.locale, this.hass.config)
|
||||
: formatDateTimeWithBrowserDefaults(backupDate);
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.details.summary.title`
|
||||
)}
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<ha-md-list class="summary">
|
||||
${this.translationKeyPanel === "config.backup"
|
||||
? html`<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.localize("ui.panel.config.backup.backup_type")}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.localize(
|
||||
`ui.panel.config.backup.type.${computeBackupType(this.backup, this.isHassio)}`
|
||||
)}
|
||||
</span>
|
||||
</ha-md-list-item>`
|
||||
: nothing}
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.details.summary.size`
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${bytesToString(computeBackupSize(this.backup))}
|
||||
</span>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.details.summary.created`
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text"> ${formattedDate} </span>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</div>
|
||||
${this.showUploadAnother
|
||||
? html`<div class="card-actions">
|
||||
<ha-button @click=${this._uploadAnother} destructive>
|
||||
${this.localize(
|
||||
`ui.panel.page-onboarding.restore.details.summary.upload_another`
|
||||
)}
|
||||
</ha-button>
|
||||
</div>`
|
||||
: nothing}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _uploadAnother() {
|
||||
fireEvent(this, "show-backup-upload");
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
max-width: 690px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
gap: 24px;
|
||||
display: grid;
|
||||
}
|
||||
.card-content {
|
||||
padding: 0 20px;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
ha-md-list {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
--md-list-item-two-line-container-height: 64px;
|
||||
}
|
||||
ha-md-list.summary ha-md-list-item {
|
||||
--md-list-item-supporting-text-size: 1rem;
|
||||
--md-list-item-label-text-size: 0.875rem;
|
||||
|
||||
--md-list-item-label-text-color: var(--secondary-text-color);
|
||||
--md-list-item-supporting-text-color: var(--primary-text-color);
|
||||
}
|
||||
ha-md-list-item [slot="supporting-text"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
line-height: normal;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-backup-details-summary": HaBackupDetailsSummary;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,10 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import {
|
||||
fireEvent,
|
||||
type HASSDomEvent,
|
||||
} from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
@@ -14,7 +17,10 @@ import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||
import {
|
||||
CORE_LOCAL_AGENT,
|
||||
HASSIO_LOCAL_AGENT,
|
||||
SUPPORTED_UPLOAD_FORMAT,
|
||||
uploadBackup,
|
||||
INITIAL_UPLOAD_FORM_DATA,
|
||||
type BackupUploadFileFormData,
|
||||
} from "../../../../data/backup";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
@@ -22,16 +28,6 @@ import type { HomeAssistant } from "../../../../types";
|
||||
import { showAlertDialog } from "../../../lovelace/custom-card-helpers";
|
||||
import type { UploadBackupDialogParams } from "./show-dialog-upload-backup";
|
||||
|
||||
const SUPPORTED_FORMAT = "application/x-tar";
|
||||
|
||||
interface FormData {
|
||||
file?: File;
|
||||
}
|
||||
|
||||
const INITIAL_DATA: FormData = {
|
||||
file: undefined,
|
||||
};
|
||||
|
||||
@customElement("ha-dialog-upload-backup")
|
||||
export class DialogUploadBackup
|
||||
extends LitElement
|
||||
@@ -45,13 +41,13 @@ export class DialogUploadBackup
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _formData?: FormData;
|
||||
@state() private _formData?: BackupUploadFileFormData;
|
||||
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
|
||||
public async showDialog(params: UploadBackupDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._formData = INITIAL_DATA;
|
||||
this._formData = INITIAL_UPLOAD_FORM_DATA;
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
@@ -78,13 +74,18 @@ export class DialogUploadBackup
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-md-dialog open @closed=${this._dialogClosed}>
|
||||
<ha-md-dialog
|
||||
open
|
||||
@closed=${this._dialogClosed}
|
||||
.disableCancelAction=${this._uploading}
|
||||
>
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.dialogs.generic.close")}
|
||||
.path=${mdiClose}
|
||||
@click=${this.closeDialog}
|
||||
.disabled=${this._uploading}
|
||||
></ha-icon-button>
|
||||
|
||||
<span slot="title">
|
||||
@@ -99,7 +100,8 @@ export class DialogUploadBackup
|
||||
.hass=${this.hass}
|
||||
.uploading=${this._uploading}
|
||||
.icon=${mdiFolderUpload}
|
||||
accept=${SUPPORTED_FORMAT}
|
||||
.accept=${SUPPORTED_UPLOAD_FORMAT}
|
||||
.localize=${this.hass.localize}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.upload.input_label"
|
||||
)}
|
||||
@@ -107,13 +109,17 @@ export class DialogUploadBackup
|
||||
"ui.panel.config.backup.dialogs.upload.supports_tar"
|
||||
)}
|
||||
@file-picked=${this._filePicked}
|
||||
@files-cleared=${this._filesCleared}
|
||||
></ha-file-upload>
|
||||
</div>
|
||||
<div slot="actions">
|
||||
<ha-button @click=${this.closeDialog}
|
||||
<ha-button @click=${this.closeDialog} .disabled=${this._uploading}
|
||||
>${this.hass.localize("ui.common.cancel")}</ha-button
|
||||
>
|
||||
<ha-button @click=${this._upload} .disabled=${!this._formValid()}>
|
||||
<ha-button
|
||||
@click=${this._upload}
|
||||
.disabled=${!this._formValid() || this._uploading}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.upload.action"
|
||||
)}
|
||||
@@ -123,7 +129,7 @@ export class DialogUploadBackup
|
||||
`;
|
||||
}
|
||||
|
||||
private async _filePicked(ev: CustomEvent<{ files: File[] }>): Promise<void> {
|
||||
private _filePicked(ev: HASSDomEvent<{ files: File[] }>) {
|
||||
this._error = undefined;
|
||||
const file = ev.detail.files[0];
|
||||
|
||||
@@ -133,9 +139,14 @@ export class DialogUploadBackup
|
||||
};
|
||||
}
|
||||
|
||||
private _filesCleared() {
|
||||
this._error = undefined;
|
||||
this._formData = INITIAL_UPLOAD_FORM_DATA;
|
||||
}
|
||||
|
||||
private async _upload() {
|
||||
const { file } = this._formData!;
|
||||
if (!file || file.type !== SUPPORTED_FORMAT) {
|
||||
if (!file || file.type !== SUPPORTED_UPLOAD_FORMAT) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.upload.unsupported.title"
|
||||
@@ -154,7 +165,7 @@ export class DialogUploadBackup
|
||||
|
||||
this._uploading = true;
|
||||
try {
|
||||
await uploadBackup(this.hass!, file, agentIds);
|
||||
await uploadBackup(this.hass, file, agentIds);
|
||||
this._params!.submit?.();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
} from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { formatDateTime } from "../../../common/datetime/format_date_time";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import "../../../components/ha-alert";
|
||||
@@ -25,24 +24,20 @@ import type {
|
||||
BackupConfig,
|
||||
BackupContentAgent,
|
||||
BackupContentExtended,
|
||||
BackupData,
|
||||
} from "../../../data/backup";
|
||||
import "./components/ha-backup-details-summary";
|
||||
import "./components/ha-backup-details-restore";
|
||||
import {
|
||||
compareAgents,
|
||||
computeBackupAgentName,
|
||||
computeBackupSize,
|
||||
computeBackupType,
|
||||
deleteBackup,
|
||||
fetchBackupDetails,
|
||||
isLocalAgent,
|
||||
isNetworkMountAgent,
|
||||
} from "../../../data/backup";
|
||||
import type { HassioAddonInfo } from "../../../data/hassio/addon";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import { bytesToString } from "../../../util/bytes-to-string";
|
||||
import "./components/ha-backup-data-picker";
|
||||
import { showRestoreBackupDialog } from "./dialogs/show-dialog-restore-backup";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
@@ -93,10 +88,6 @@ class HaConfigBackupDetails extends LitElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _selectedData?: BackupData;
|
||||
|
||||
@state() private _addonsInfo?: HassioAddonInfo[];
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
@@ -157,81 +148,18 @@ class HaConfigBackupDetails extends LitElement {
|
||||
: !this._backup
|
||||
? html`<ha-circular-progress active></ha-circular-progress>`
|
||||
: html`
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.details.summary.title"
|
||||
)}
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<ha-md-list class="summary">
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.backup_type"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.backup.type.${computeBackupType(this._backup, isHassio)}`
|
||||
)}
|
||||
</span>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.details.summary.size"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${bytesToString(computeBackupSize(this._backup))}
|
||||
</span>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.details.summary.created"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${formatDateTime(
|
||||
new Date(this._backup.date),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}
|
||||
</span>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.details.restore.title"
|
||||
)}
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<ha-backup-data-picker
|
||||
.hass=${this.hass}
|
||||
.data=${this._backup}
|
||||
.value=${this._selectedData}
|
||||
@value-changed=${this._selectedBackupChanged}
|
||||
.addonsInfo=${this._addonsInfo}
|
||||
>
|
||||
</ha-backup-data-picker>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button
|
||||
@click=${this._restore}
|
||||
.disabled=${this._isRestoreDisabled()}
|
||||
class="danger"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.details.restore.action"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-backup-details-summary
|
||||
.backup=${this._backup}
|
||||
.hass=${this.hass}
|
||||
.localize=${this.hass.localize}
|
||||
.isHassio=${isHassio}
|
||||
></ha-backup-details-summary>
|
||||
<ha-backup-details-restore
|
||||
.backup=${this._backup}
|
||||
@backup-restore=${this._restore}
|
||||
.hass=${this.hass}
|
||||
.localize=${this.hass.localize}
|
||||
></ha-backup-details-restore>
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
${this.hass.localize(
|
||||
@@ -360,30 +288,13 @@ class HaConfigBackupDetails extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _selectedBackupChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._selectedData = ev.detail.value;
|
||||
}
|
||||
|
||||
private _isRestoreDisabled() {
|
||||
return (
|
||||
!this._selectedData ||
|
||||
!(
|
||||
this._selectedData?.database_included ||
|
||||
this._selectedData?.homeassistant_included ||
|
||||
this._selectedData.addons.length ||
|
||||
this._selectedData.folders.length
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _restore() {
|
||||
if (!this._backup || !this._selectedData) {
|
||||
private _restore(ev: CustomEvent) {
|
||||
if (!this._backup || !ev.detail.selectedData) {
|
||||
return;
|
||||
}
|
||||
showRestoreBackupDialog(this, {
|
||||
backup: this._backup,
|
||||
selectedData: this._selectedData,
|
||||
selectedData: ev.detail.selectedData,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -469,13 +380,6 @@ class HaConfigBackupDetails extends LitElement {
|
||||
--mdc-icon-size: 48px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
ha-md-list.summary ha-md-list-item {
|
||||
--md-list-item-supporting-text-size: 1rem;
|
||||
--md-list-item-label-text-size: 0.875rem;
|
||||
|
||||
--md-list-item-label-text-color: var(--secondary-text-color);
|
||||
--md-list-item-supporting-text-color: var(--primary-text-color);
|
||||
}
|
||||
.warning {
|
||||
color: var(--error-color);
|
||||
}
|
||||
@@ -485,9 +389,6 @@ class HaConfigBackupDetails extends LitElement {
|
||||
ha-button.danger {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
ha-backup-data-picker {
|
||||
display: block;
|
||||
}
|
||||
ha-md-list-item [slot="supporting-text"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -51,8 +51,8 @@ import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { ConfigEntry } from "../../../data/config_entries";
|
||||
import { sortConfigEntries } from "../../../data/config_entries";
|
||||
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
|
||||
import { getSubEntries, sortConfigEntries } from "../../../data/config_entries";
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
import type { DataTableFilters } from "../../../data/data_table_filters";
|
||||
import {
|
||||
@@ -108,6 +108,8 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ attribute: false }) public entries!: ConfigEntry[];
|
||||
|
||||
@state() private _subEntries?: SubEntry[];
|
||||
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
entities!: EntityRegistryEntry[];
|
||||
@@ -219,6 +221,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
private _setFiltersFromUrl() {
|
||||
const domain = this._searchParms.get("domain");
|
||||
const configEntry = this._searchParms.get("config_entry");
|
||||
const subEntry = this._searchParms.get("sub_entry");
|
||||
const label = this._searchParms.has("label");
|
||||
|
||||
if (!domain && !configEntry && !label) {
|
||||
@@ -243,6 +246,10 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
value: configEntry ? [configEntry] : [],
|
||||
items: undefined,
|
||||
},
|
||||
sub_entry: {
|
||||
value: subEntry ? [subEntry] : [],
|
||||
items: undefined,
|
||||
},
|
||||
};
|
||||
this._filterLabel();
|
||||
}
|
||||
@@ -334,6 +341,32 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
if (configEntries.length === 1) {
|
||||
filteredConfigEntry = configEntries[0];
|
||||
}
|
||||
} else if (
|
||||
key === "sub_entry" &&
|
||||
Array.isArray(filter.value) &&
|
||||
filter.value.length
|
||||
) {
|
||||
if (
|
||||
!(
|
||||
Array.isArray(this._filters.config_entry?.value) &&
|
||||
this._filters.config_entry.value.length === 1
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const configEntryId = this._filters.config_entry.value[0];
|
||||
outputDevices = outputDevices.filter(
|
||||
(device) =>
|
||||
device.config_entries_subentries[configEntryId] &&
|
||||
(filter.value as string[]).some((subEntryId) =>
|
||||
device.config_entries_subentries[configEntryId].includes(
|
||||
subEntryId
|
||||
)
|
||||
)
|
||||
);
|
||||
if (!this._subEntries) {
|
||||
this._loadSubEntries(configEntryId);
|
||||
}
|
||||
} else if (
|
||||
key === "ha-filter-integrations" &&
|
||||
Array.isArray(filter.value) &&
|
||||
@@ -755,7 +788,15 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
${this.entries?.find(
|
||||
(entry) =>
|
||||
entry.entry_id === this._filters.config_entry!.value![0]
|
||||
)?.title || this._filters.config_entry.value[0]}
|
||||
)?.title || this._filters.config_entry.value[0]}${this._filters
|
||||
.config_entry.value.length === 1 &&
|
||||
Array.isArray(this._filters.sub_entry?.value) &&
|
||||
this._filters.sub_entry.value.length
|
||||
? html` (${this._subEntries?.find(
|
||||
(entry) =>
|
||||
entry.subentry_id === this._filters.sub_entry!.value![0]
|
||||
)?.title || this._filters.sub_entry!.value![0]})`
|
||||
: nothing}
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
<ha-filter-floor-areas
|
||||
@@ -888,6 +929,10 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _loadSubEntries(entryId: string) {
|
||||
this._subEntries = await getSubEntries(this.hass, entryId);
|
||||
}
|
||||
|
||||
private _filterExpanded(ev) {
|
||||
if (ev.detail.expanded) {
|
||||
this._expandedFilter = ev.target.localName;
|
||||
|
||||
@@ -66,8 +66,8 @@ import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { ConfigEntry } from "../../../data/config_entries";
|
||||
import { getConfigEntries } from "../../../data/config_entries";
|
||||
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
|
||||
import { getConfigEntries, getSubEntries } from "../../../data/config_entries";
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
import type {
|
||||
DataTableFiltersItems,
|
||||
@@ -146,6 +146,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _entries?: ConfigEntry[];
|
||||
|
||||
@state() private _subEntries?: SubEntry[];
|
||||
|
||||
@state() private _manifests?: IntegrationManifest[];
|
||||
|
||||
@state()
|
||||
@@ -522,6 +524,27 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
if (configEntries.length === 1) {
|
||||
filteredConfigEntry = configEntries[0];
|
||||
}
|
||||
} else if (
|
||||
key === "sub_entry" &&
|
||||
Array.isArray(filter) &&
|
||||
filter.length
|
||||
) {
|
||||
if (
|
||||
!(
|
||||
Array.isArray(this._filters.config_entry) &&
|
||||
this._filters.config_entry.length === 1
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
filteredEntities = filteredEntities.filter(
|
||||
(entity) =>
|
||||
entity.config_subentry_id &&
|
||||
(filter as string[]).includes(entity.config_subentry_id)
|
||||
);
|
||||
if (!this._subEntries) {
|
||||
this._loadSubEntries(this._filters.config_entry[0]);
|
||||
}
|
||||
} else if (
|
||||
key === "ha-filter-integrations" &&
|
||||
Array.isArray(filter) &&
|
||||
@@ -904,14 +927,22 @@ ${
|
||||
</ha-md-button-menu>
|
||||
${
|
||||
Array.isArray(this._filters.config_entry) &&
|
||||
this._filters.config_entry?.length
|
||||
this._filters.config_entry.length
|
||||
? html`<ha-alert slot="filter-pane">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.filtering_by_config_entry"
|
||||
)}
|
||||
${this._entries?.find(
|
||||
(entry) => entry.entry_id === this._filters.config_entry![0]
|
||||
)?.title || this._filters.config_entry[0]}
|
||||
)?.title || this._filters.config_entry[0]}${this._filters
|
||||
.config_entry.length === 1 &&
|
||||
Array.isArray(this._filters.sub_entry) &&
|
||||
this._filters.sub_entry.length
|
||||
? html` (${this._subEntries?.find(
|
||||
(entry) =>
|
||||
entry.subentry_id === this._filters.sub_entry![0]
|
||||
)?.title || this._filters.sub_entry[0]})`
|
||||
: nothing}
|
||||
</ha-alert>`
|
||||
: nothing
|
||||
}
|
||||
@@ -1024,6 +1055,7 @@ ${
|
||||
private _setFiltersFromUrl() {
|
||||
const domain = this._searchParms.get("domain");
|
||||
const configEntry = this._searchParms.get("config_entry");
|
||||
const subEntry = this._searchParms.get("sub_entry");
|
||||
const label = this._searchParms.has("label");
|
||||
|
||||
if (!domain && !configEntry && !label) {
|
||||
@@ -1036,6 +1068,7 @@ ${
|
||||
"ha-filter-states": [],
|
||||
"ha-filter-integrations": domain ? [domain] : [],
|
||||
config_entry: configEntry ? [configEntry] : [],
|
||||
sub_entry: subEntry ? [subEntry] : [],
|
||||
};
|
||||
this._filterLabel();
|
||||
}
|
||||
@@ -1093,6 +1126,7 @@ ${
|
||||
hidden_by: null,
|
||||
area_id: null,
|
||||
config_entry_id: null,
|
||||
config_subentry_id: null,
|
||||
device_id: null,
|
||||
icon: null,
|
||||
readonly: true,
|
||||
@@ -1384,6 +1418,10 @@ ${rejected
|
||||
this._entries = await getConfigEntries(this.hass);
|
||||
}
|
||||
|
||||
private async _loadSubEntries(entryId: string) {
|
||||
this._subEntries = await getSubEntries(this.hass, entryId);
|
||||
}
|
||||
|
||||
private _addDevice() {
|
||||
const { filteredConfigEntry, filteredDomains } =
|
||||
this._filteredEntitiesAndDomains(
|
||||
|
||||
@@ -654,6 +654,7 @@ class AddIntegrationDialog extends LitElement {
|
||||
startFlowHandler: domain,
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
manifest,
|
||||
navigateToResult: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -137,6 +137,7 @@ export class HaConfigFlowCard extends LitElement {
|
||||
}
|
||||
showConfigFlowDialog(this, {
|
||||
continueFlowId: this.flow.flow_id,
|
||||
navigateToResult: true,
|
||||
dialogClosedCallback: () => {
|
||||
this._handleFlowUpdated();
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
mdiOpenInNew,
|
||||
mdiPackageVariant,
|
||||
mdiPlayCircleOutline,
|
||||
mdiPlus,
|
||||
mdiProgressHelper,
|
||||
mdiReload,
|
||||
mdiReloadAlert,
|
||||
@@ -52,14 +53,17 @@ import { getSignedPath } from "../../../data/auth";
|
||||
import type {
|
||||
ConfigEntry,
|
||||
DisableConfigEntryResult,
|
||||
SubEntry,
|
||||
} from "../../../data/config_entries";
|
||||
import {
|
||||
ERROR_STATES,
|
||||
RECOVERABLE_STATES,
|
||||
deleteConfigEntry,
|
||||
deleteSubEntry,
|
||||
disableConfigEntry,
|
||||
enableConfigEntry,
|
||||
getConfigEntries,
|
||||
getSubEntries,
|
||||
reloadConfigEntry,
|
||||
updateConfigEntry,
|
||||
} from "../../../data/config_entries";
|
||||
@@ -106,6 +110,7 @@ import { fileDownload } from "../../../util/file_download";
|
||||
import type { DataEntryFlowProgressExtended } from "./ha-config-integrations";
|
||||
import { showAddIntegrationDialog } from "./show-add-integration-dialog";
|
||||
import { QUALITY_SCALE_MAP } from "../../../data/integration_quality_scale";
|
||||
import { showSubConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-sub-config-flow";
|
||||
|
||||
export const renderConfigEntryError = (
|
||||
hass: HomeAssistant,
|
||||
@@ -172,6 +177,8 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _domainEntities: Record<string, string[]> = {};
|
||||
|
||||
@state() private _subEntries: Record<string, SubEntry[]> = {};
|
||||
|
||||
private _configPanel = memoizeOne(
|
||||
(domain: string, panels: HomeAssistant["panels"]): string | undefined =>
|
||||
Object.values(panels).find(
|
||||
@@ -214,11 +221,18 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (changedProperties.has("domain")) {
|
||||
this.hass.loadBackendTranslation("title", [this.domain]);
|
||||
this.hass.loadBackendTranslation("config_subentries", [this.domain]);
|
||||
this._extraConfigEntries = undefined;
|
||||
this._fetchManifest();
|
||||
this._fetchDiagnostics();
|
||||
this._fetchEntitySources();
|
||||
}
|
||||
if (
|
||||
changedProperties.has("configEntries") ||
|
||||
changedProperties.has("_extraConfigEntries")
|
||||
) {
|
||||
this._fetchSubEntries();
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchEntitySources() {
|
||||
@@ -573,7 +587,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
const attention = ATTENTION_SOURCES.includes(
|
||||
flow.context.source
|
||||
);
|
||||
return html` <ha-md-list-item
|
||||
return html`<ha-md-list-item
|
||||
class="config_entry ${attention ? "attention" : ""}"
|
||||
>
|
||||
${flow.localized_title}
|
||||
@@ -673,6 +687,73 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
ev.target.style.display = "none";
|
||||
}
|
||||
|
||||
private _renderDeviceLine(
|
||||
item: ConfigEntry,
|
||||
devices: DeviceRegistryEntry[],
|
||||
services: DeviceRegistryEntry[],
|
||||
entities: EntityRegistryEntry[],
|
||||
subItem?: SubEntry
|
||||
) {
|
||||
let devicesLine: (TemplateResult | string)[] = [];
|
||||
for (const [items, localizeKey] of [
|
||||
[devices, "devices"],
|
||||
[services, "services"],
|
||||
] as const) {
|
||||
if (items.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const url =
|
||||
items.length === 1
|
||||
? `/config/devices/device/${items[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}${subItem ? `&sub_entry=${subItem.subentry_id}` : ""}`;
|
||||
devicesLine.push(
|
||||
// no white space before/after template on purpose
|
||||
html`<a href=${url}
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.${localizeKey}`,
|
||||
{ count: items.length }
|
||||
)}</a
|
||||
>`
|
||||
);
|
||||
}
|
||||
|
||||
if (entities.length) {
|
||||
devicesLine.push(
|
||||
// no white space before/after template on purpose
|
||||
html`<a
|
||||
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}${subItem ? `&sub_entry=${subItem.subentry_id}` : ""}`}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.entities",
|
||||
{ count: entities.length }
|
||||
)}</a
|
||||
>`
|
||||
);
|
||||
}
|
||||
|
||||
if (devicesLine.length === 0) {
|
||||
devicesLine = [
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.no_devices_or_entities"
|
||||
),
|
||||
];
|
||||
} else if (devicesLine.length === 2) {
|
||||
devicesLine = [
|
||||
devicesLine[0],
|
||||
` ${this.hass.localize("ui.common.and")} `,
|
||||
devicesLine[1],
|
||||
];
|
||||
} else if (devicesLine.length === 3) {
|
||||
devicesLine = [
|
||||
devicesLine[0],
|
||||
", ",
|
||||
devicesLine[1],
|
||||
` ${this.hass.localize("ui.common.and")} `,
|
||||
devicesLine[2],
|
||||
];
|
||||
}
|
||||
return devicesLine;
|
||||
}
|
||||
|
||||
private _renderConfigEntry(item: ConfigEntry) {
|
||||
let stateText: Parameters<typeof this.hass.localize> | undefined;
|
||||
let stateTextExtra: TemplateResult | string | undefined;
|
||||
@@ -720,274 +801,299 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
)}.`);
|
||||
}
|
||||
} else {
|
||||
for (const [items, localizeKey] of [
|
||||
[devices, "devices"],
|
||||
[services, "services"],
|
||||
] as const) {
|
||||
if (items.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const url =
|
||||
items.length === 1
|
||||
? `/config/devices/device/${items[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`;
|
||||
devicesLine.push(
|
||||
// no white space before/after template on purpose
|
||||
html`<a href=${url}
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.${localizeKey}`,
|
||||
{ count: items.length }
|
||||
)}</a
|
||||
>`
|
||||
);
|
||||
}
|
||||
|
||||
if (entities.length) {
|
||||
devicesLine.push(
|
||||
// no white space before/after template on purpose
|
||||
html`<a
|
||||
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.entities",
|
||||
{ count: entities.length }
|
||||
)}</a
|
||||
>`
|
||||
);
|
||||
}
|
||||
|
||||
if (devicesLine.length === 0) {
|
||||
devicesLine = [
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.no_devices_or_entities"
|
||||
),
|
||||
];
|
||||
} else if (devicesLine.length === 2) {
|
||||
devicesLine = [
|
||||
devicesLine[0],
|
||||
` ${this.hass.localize("ui.common.and")} `,
|
||||
devicesLine[1],
|
||||
];
|
||||
} else if (devicesLine.length === 3) {
|
||||
devicesLine = [
|
||||
devicesLine[0],
|
||||
", ",
|
||||
devicesLine[1],
|
||||
` ${this.hass.localize("ui.common.and")} `,
|
||||
devicesLine[2],
|
||||
];
|
||||
}
|
||||
devicesLine = this._renderDeviceLine(item, devices, services, entities);
|
||||
}
|
||||
|
||||
const configPanel = this._configPanel(item.domain, this.hass.panels);
|
||||
|
||||
const subEntries = this._subEntries[item.entry_id] || [];
|
||||
|
||||
return html`<ha-md-list-item
|
||||
class=${classMap({
|
||||
config_entry: true,
|
||||
"state-not-loaded": item!.state === "not_loaded",
|
||||
"state-failed-unload": item!.state === "failed_unload",
|
||||
"state-setup": item!.state === "setup_in_progress",
|
||||
"state-error": ERROR_STATES.includes(item!.state),
|
||||
"state-disabled": item.disabled_by !== null,
|
||||
})}
|
||||
data-entry-id=${item.entry_id}
|
||||
.configEntry=${item}
|
||||
>
|
||||
<div slot="headline">
|
||||
${item.title || domainToName(this.hass.localize, item.domain)}
|
||||
</div>
|
||||
<div slot="supporting-text">
|
||||
<div>${devicesLine}</div>
|
||||
${stateText
|
||||
? html`
|
||||
<div class="message">
|
||||
<ha-svg-icon .path=${icon}></ha-svg-icon>
|
||||
<div>
|
||||
${this.hass.localize(...stateText)}${stateTextExtra
|
||||
? html`: ${stateTextExtra}`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
${item.disabled_by === "user"
|
||||
? html`<ha-button unelevated slot="end" @click=${this._handleEnable}>
|
||||
${this.hass.localize("ui.common.enable")}
|
||||
</ha-button>`
|
||||
: configPanel &&
|
||||
(item.domain !== "matter" ||
|
||||
isDevVersion(this.hass.config.version)) &&
|
||||
!stateText
|
||||
? html`<a
|
||||
slot="end"
|
||||
href=${`/${configPanel}?config_entry=${item.entry_id}`}
|
||||
><ha-button>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.configure"
|
||||
)}
|
||||
</ha-button></a
|
||||
>`
|
||||
: item.supports_options
|
||||
class=${classMap({
|
||||
config_entry: true,
|
||||
"state-not-loaded": item!.state === "not_loaded",
|
||||
"state-failed-unload": item!.state === "failed_unload",
|
||||
"state-setup": item!.state === "setup_in_progress",
|
||||
"state-error": ERROR_STATES.includes(item!.state),
|
||||
"state-disabled": item.disabled_by !== null,
|
||||
})}
|
||||
data-entry-id=${item.entry_id}
|
||||
.configEntry=${item}
|
||||
>
|
||||
<div slot="headline">
|
||||
${item.title || domainToName(this.hass.localize, item.domain)}
|
||||
</div>
|
||||
<div slot="supporting-text">
|
||||
<div>${devicesLine}</div>
|
||||
${stateText
|
||||
? html`
|
||||
<ha-button slot="end" @click=${this._showOptions}>
|
||||
<div class="message">
|
||||
<ha-svg-icon .path=${icon}></ha-svg-icon>
|
||||
<div>
|
||||
${this.hass.localize(...stateText)}${stateTextExtra
|
||||
? html`: ${stateTextExtra}`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
${item.disabled_by === "user"
|
||||
? html`<ha-button unelevated slot="end" @click=${this._handleEnable}>
|
||||
${this.hass.localize("ui.common.enable")}
|
||||
</ha-button>`
|
||||
: configPanel &&
|
||||
(item.domain !== "matter" ||
|
||||
isDevVersion(this.hass.config.version)) &&
|
||||
!stateText
|
||||
? html`<a
|
||||
slot="end"
|
||||
href=${`/${configPanel}?config_entry=${item.entry_id}`}
|
||||
><ha-button>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.configure"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-button></a
|
||||
>`
|
||||
: item.supports_options
|
||||
? html`
|
||||
<ha-button slot="end" @click=${this._showOptions}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.configure"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
<ha-md-button-menu positioning="popover" slot="end">
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
${item.disabled_by && devices.length
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
href=${devices.length === 1
|
||||
? `/config/devices/device/${devices[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDevices} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.devices`,
|
||||
{ count: devices.length }
|
||||
)}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
${item.disabled_by && services.length
|
||||
? html`<ha-md-menu-item
|
||||
href=${services.length === 1
|
||||
? `/config/devices/device/${services[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiHandExtendedOutline}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.services`,
|
||||
{ count: services.length }
|
||||
)}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-menu-item> `
|
||||
: nothing}
|
||||
${item.disabled_by && entities.length
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiShapeOutline}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.entities`,
|
||||
{ count: entities.length }
|
||||
)}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
${!item.disabled_by &&
|
||||
RECOVERABLE_STATES.includes(item.state) &&
|
||||
item.supports_unload &&
|
||||
item.source !== "system"
|
||||
? html`
|
||||
<ha-md-menu-item @click=${this._handleReload}>
|
||||
<ha-svg-icon slot="start" .path=${mdiReload}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.reload"
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-md-menu-item @click=${this._handleRename} graphic="icon">
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.rename"
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
|
||||
${Object.keys(item.supported_subentry_types).map(
|
||||
(flowType) =>
|
||||
html`<ha-md-menu-item
|
||||
@click=${this._addSubEntry}
|
||||
.entry=${item}
|
||||
.flowType=${flowType}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`component.${item.domain}.config_subentries.${flowType}.title`
|
||||
)}</ha-md-menu-item
|
||||
>`
|
||||
)}
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
${this._diagnosticHandler && item.state === "loaded"
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
href=${getConfigEntryDiagnosticsDownloadUrl(item.entry_id)}
|
||||
target="_blank"
|
||||
@click=${this._signUrl}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiDownload}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.download_diagnostics"
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
${!item.disabled_by &&
|
||||
item.supports_reconfigure &&
|
||||
item.source !== "system"
|
||||
? html`
|
||||
<ha-md-menu-item @click=${this._handleReconfigure}>
|
||||
<ha-svg-icon slot="start" .path=${mdiWrench}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.reconfigure"
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-md-menu-item @click=${this._handleSystemOptions} graphic="icon">
|
||||
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.system_options"
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
${item.disabled_by === "user"
|
||||
? html`
|
||||
<ha-md-menu-item @click=${this._handleEnable}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPlayCircleOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize("ui.common.enable")}
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: item.source !== "system"
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
class="warning"
|
||||
@click=${this._handleDisable}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
class="warning"
|
||||
.path=${mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize("ui.common.disable")}
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
${item.source !== "system"
|
||||
? html`
|
||||
<ha-md-menu-item class="warning" @click=${this._handleDelete}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
class="warning"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.delete"
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
</ha-md-button-menu>
|
||||
</ha-md-list-item>
|
||||
${subEntries.map((subEntry) => this._renderSubEntry(item, subEntry))}`;
|
||||
}
|
||||
|
||||
private _renderSubEntry(configEntry: ConfigEntry, subEntry: SubEntry) {
|
||||
const devices = this._getConfigEntryDevices(configEntry).filter((device) =>
|
||||
device.config_entries_subentries[configEntry.entry_id]?.includes(
|
||||
subEntry.subentry_id
|
||||
)
|
||||
);
|
||||
const services = this._getConfigEntryServices(configEntry).filter(
|
||||
(device) =>
|
||||
device.config_entries_subentries[configEntry.entry_id]?.includes(
|
||||
subEntry.subentry_id
|
||||
)
|
||||
);
|
||||
const entities = this._getConfigEntryEntities(configEntry).filter(
|
||||
(entity) => entity.config_subentry_id === subEntry.subentry_id
|
||||
);
|
||||
|
||||
return html`<ha-md-list-item
|
||||
class="sub-entry"
|
||||
data-entry-id=${configEntry.entry_id}
|
||||
.configEntry=${configEntry}
|
||||
.subEntry=${subEntry}
|
||||
>
|
||||
<span slot="headline">${subEntry.title}</span>
|
||||
<span slot="supporting-text"
|
||||
>${this._renderDeviceLine(
|
||||
configEntry,
|
||||
devices,
|
||||
services,
|
||||
entities,
|
||||
subEntry
|
||||
)}</span
|
||||
>
|
||||
${configEntry.supported_subentry_types[subEntry.subentry_type]
|
||||
?.supports_reconfigure
|
||||
? html`
|
||||
<ha-button slot="end" @click=${this._handleReconfigureSub}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.configure"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
<ha-md-button-menu positioning="popover" slot="end">
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
${item.disabled_by && devices.length
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
href=${devices.length === 1
|
||||
? `/config/devices/device/${devices[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDevices} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.devices`,
|
||||
{ count: devices.length }
|
||||
)}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
${item.disabled_by && services.length
|
||||
? html`<ha-md-menu-item
|
||||
href=${services.length === 1
|
||||
? `/config/devices/device/${services[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiHandExtendedOutline}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.services`,
|
||||
{ count: services.length }
|
||||
)}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-menu-item> `
|
||||
: nothing}
|
||||
${item.disabled_by && entities.length
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiShapeOutline}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.entities`,
|
||||
{ count: entities.length }
|
||||
)}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
${!item.disabled_by &&
|
||||
RECOVERABLE_STATES.includes(item.state) &&
|
||||
item.supports_unload &&
|
||||
item.source !== "system"
|
||||
? html`
|
||||
<ha-md-menu-item @click=${this._handleReload}>
|
||||
<ha-svg-icon slot="start" .path=${mdiReload}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.reload"
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-md-menu-item @click=${this._handleRename} graphic="icon">
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
<ha-md-menu-item class="warning" @click=${this._handleDeleteSub}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
class="warning"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.rename"
|
||||
"ui.panel.config.integrations.config_entry.delete"
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
${this._diagnosticHandler && item.state === "loaded"
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
href=${getConfigEntryDiagnosticsDownloadUrl(item.entry_id)}
|
||||
target="_blank"
|
||||
@click=${this._signUrl}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiDownload}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.download_diagnostics"
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
${!item.disabled_by &&
|
||||
item.supports_reconfigure &&
|
||||
item.source !== "system"
|
||||
? html`
|
||||
<ha-md-menu-item @click=${this._handleReconfigure}>
|
||||
<ha-svg-icon slot="start" .path=${mdiWrench}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.reconfigure"
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-md-menu-item @click=${this._handleSystemOptions} graphic="icon">
|
||||
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.system_options"
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
${item.disabled_by === "user"
|
||||
? html`
|
||||
<ha-md-menu-item @click=${this._handleEnable}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPlayCircleOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize("ui.common.enable")}
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: item.source !== "system"
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
class="warning"
|
||||
@click=${this._handleDisable}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
class="warning"
|
||||
.path=${mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize("ui.common.disable")}
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
${item.source !== "system"
|
||||
? html`
|
||||
<ha-md-menu-item class="warning" @click=${this._handleDelete}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
class="warning"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.delete"
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
</ha-md-button-menu>
|
||||
</ha-md-list-item>`;
|
||||
}
|
||||
@@ -1009,6 +1115,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
private _continueFlow(ev) {
|
||||
showConfigFlowDialog(this, {
|
||||
continueFlowId: ev.target.flow.flow_id,
|
||||
navigateToResult: true,
|
||||
dialogClosedCallback: () => {
|
||||
// this._handleFlowUpdated();
|
||||
},
|
||||
@@ -1030,6 +1137,27 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchSubEntries() {
|
||||
const subEntriesPromises = (
|
||||
this._extraConfigEntries || this.configEntries
|
||||
)?.map((entry) =>
|
||||
entry.num_subentries
|
||||
? getSubEntries(this.hass, entry.entry_id).then((subEntries) => ({
|
||||
entry_id: entry.entry_id,
|
||||
subEntries,
|
||||
}))
|
||||
: undefined
|
||||
);
|
||||
if (subEntriesPromises) {
|
||||
const subEntries = await Promise.all(subEntriesPromises);
|
||||
this._subEntries = {};
|
||||
subEntries.forEach((entry) => {
|
||||
if (!entry) return;
|
||||
this._subEntries[entry.entry_id] = entry.subEntries;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchDiagnostics() {
|
||||
if (!this.domain || !isComponentLoaded(this.hass, "diagnostics")) {
|
||||
return;
|
||||
@@ -1177,6 +1305,49 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
}
|
||||
|
||||
private async _handleReconfigureSub(ev: Event): Promise<void> {
|
||||
const configEntry = (
|
||||
(ev.target as HTMLElement).closest(".sub-entry") as any
|
||||
).configEntry;
|
||||
const subEntry = ((ev.target as HTMLElement).closest(".sub-entry") as any)
|
||||
.subEntry;
|
||||
|
||||
showSubConfigFlowDialog(
|
||||
this,
|
||||
configEntry,
|
||||
subEntry.flowType || subEntry.subentry_type,
|
||||
{
|
||||
startFlowHandler: configEntry.entry_id,
|
||||
subEntryId: subEntry.subentry_id,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async _handleDeleteSub(ev: Event): Promise<void> {
|
||||
const configEntry = (
|
||||
(ev.target as HTMLElement).closest(".sub-entry") as any
|
||||
).configEntry;
|
||||
const subEntry = ((ev.target as HTMLElement).closest(".sub-entry") as any)
|
||||
.subEntry;
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.delete_confirm_title",
|
||||
{ title: subEntry.title }
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.delete_confirm_text"
|
||||
),
|
||||
confirmText: this.hass!.localize("ui.common.delete"),
|
||||
dismissText: this.hass!.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
await deleteSubEntry(this.hass, configEntry.entry_id, subEntry.subentry_id);
|
||||
}
|
||||
|
||||
private _handleDisable(ev: Event): void {
|
||||
this._disableIntegration(
|
||||
((ev.target as HTMLElement).closest(".config_entry") as any).configEntry
|
||||
@@ -1384,6 +1555,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
manifest: await fetchIntegrationManifest(this.hass, configEntry.domain),
|
||||
entryId: configEntry.entry_id,
|
||||
navigateToResult: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1454,6 +1626,12 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private async _addSubEntry(ev) {
|
||||
showSubConfigFlowDialog(this, ev.target.entry, ev.target.flowType, {
|
||||
startFlowHandler: ev.target.entry.entry_id,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -1583,6 +1761,9 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
}
|
||||
ha-md-list-item.sub-entry {
|
||||
--md-list-item-leading-space: 50px;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -207,6 +207,8 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
supports_remove_device: false,
|
||||
supports_unload: false,
|
||||
supports_reconfigure: false,
|
||||
supported_subentry_types: {},
|
||||
num_subentries: 0,
|
||||
pref_disable_new_entities: false,
|
||||
pref_disable_polling: false,
|
||||
disabled_by: null,
|
||||
|
||||
@@ -279,6 +279,7 @@ class HaDomainIntegrations extends LitElement {
|
||||
{
|
||||
startFlowHandler: domain,
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
navigateToResult: true,
|
||||
manifest: await fetchIntegrationManifest(this.hass, domain),
|
||||
}
|
||||
);
|
||||
@@ -295,6 +296,7 @@ class HaDomainIntegrations extends LitElement {
|
||||
root instanceof ShadowRoot ? (root.host as HTMLElement) : this,
|
||||
{
|
||||
continueFlowId: flow.flow_id,
|
||||
navigateToResult: true,
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
manifest: await fetchIntegrationManifest(this.hass, flow.handler),
|
||||
}
|
||||
|
||||
@@ -301,10 +301,6 @@ ${type === "object"
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.edit-pane .card-content {
|
||||
user-select: initial;
|
||||
}
|
||||
|
||||
.edit-pane a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import { hasConfigChanged } from "../../common/has-changed";
|
||||
import type { ECOption } from "../../../../resources/echarts";
|
||||
import "../../../../components/ha-card";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { measureTextWidth } from "../../../../util/text";
|
||||
|
||||
@customElement("hui-energy-devices-graph-card")
|
||||
export class HuiEnergyDevicesGraphCard
|
||||
@@ -109,8 +110,11 @@ export class HuiEnergyDevicesGraphCard
|
||||
return `${title}${params.marker} ${params.seriesName}: ${value}`;
|
||||
}
|
||||
|
||||
private _createOptions = memoizeOne(
|
||||
(data: BarSeriesOption[]): ECOption => ({
|
||||
private _createOptions = memoizeOne((data: BarSeriesOption[]): ECOption => {
|
||||
const isMobile = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
return {
|
||||
xAxis: {
|
||||
type: "value",
|
||||
name: "kWh",
|
||||
@@ -124,6 +128,17 @@ export class HuiEnergyDevicesGraphCard
|
||||
axisLabel: {
|
||||
formatter: this._getDeviceName.bind(this),
|
||||
overflow: "truncate",
|
||||
fontSize: 12,
|
||||
margin: 5,
|
||||
width: Math.min(
|
||||
isMobile ? 100 : 200,
|
||||
Math.max(
|
||||
...(data[0]?.data?.map(
|
||||
(d: any) =>
|
||||
measureTextWidth(this._getDeviceName(d.value[1]), 12) + 5
|
||||
) || [])
|
||||
)
|
||||
),
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
@@ -137,8 +152,8 @@ export class HuiEnergyDevicesGraphCard
|
||||
show: true,
|
||||
formatter: this._renderTooltip.bind(this),
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
private _getDeviceName(statisticId: string): string {
|
||||
return (
|
||||
|
||||
@@ -300,6 +300,8 @@ export class HuiEnergyUsageGraphCard
|
||||
type: "bar",
|
||||
stack: "usage",
|
||||
data: [],
|
||||
// @ts-expect-error
|
||||
order: 0,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -315,6 +317,8 @@ export class HuiEnergyUsageGraphCard
|
||||
)
|
||||
);
|
||||
|
||||
// @ts-expect-error
|
||||
datasets.sort((a, b) => a.order - b.order);
|
||||
fillDataGapsAndRoundCaps(datasets);
|
||||
this._chartData = datasets;
|
||||
}
|
||||
@@ -482,7 +486,7 @@ export class HuiEnergyUsageGraphCard
|
||||
this._compareStart!
|
||||
);
|
||||
|
||||
Object.entries(combinedData).forEach(([type, sources]) => {
|
||||
Object.entries(combinedData).forEach(([type, sources], idx) => {
|
||||
Object.entries(sources).forEach(([statId, source]) => {
|
||||
const points: BarSeriesOption["data"] = [];
|
||||
// Process chart data.
|
||||
@@ -513,6 +517,13 @@ export class HuiEnergyUsageGraphCard
|
||||
statId,
|
||||
statisticsMetaData[statId]
|
||||
),
|
||||
// @ts-expect-error
|
||||
order:
|
||||
type === "used_solar"
|
||||
? 1
|
||||
: type === "to_battery"
|
||||
? Object.keys(combinedData).length
|
||||
: idx + 2,
|
||||
barMaxWidth: 50,
|
||||
itemStyle: {
|
||||
borderColor: getEnergyColor(
|
||||
|
||||
@@ -240,6 +240,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
.container.edit-mode {
|
||||
padding: 8px;
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
border-start-end-radius: 0;
|
||||
border: 2px dashed var(--divider-color);
|
||||
min-height: var(--row-height);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiAlertCircleOutline } from "@mdi/js";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-formfield";
|
||||
import "../../components/ha-radio";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/ha-list-item";
|
||||
import type { HaRadio } from "../../components/ha-radio";
|
||||
import "../../components/ha-select";
|
||||
import "../../components/ha-settings-row";
|
||||
@@ -14,8 +17,10 @@ import {
|
||||
DEFAULT_ACCENT_COLOR,
|
||||
DEFAULT_PRIMARY_COLOR,
|
||||
} from "../../resources/styles-data";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, ThemeSettings } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import type { StorageLocation } from "../../state/themes-mixin";
|
||||
import { subscribeSelectedTheme } from "../../data/ws-themes";
|
||||
|
||||
const USE_DEFAULT_THEME = "__USE_DEFAULT_THEME__";
|
||||
const HOME_ASSISTANT_THEME = "default";
|
||||
@@ -26,57 +31,78 @@ export class HaPickThemeRow extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public storageLocation: StorageLocation = "browser";
|
||||
|
||||
@state() _themeNames: string[] = [];
|
||||
|
||||
@state() private _selectedTheme?: ThemeSettings;
|
||||
|
||||
@state() private _loading = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this._loading) {
|
||||
return html`<ha-circular-progress indeterminate></ha-circular-progress>`;
|
||||
}
|
||||
|
||||
const hasThemes =
|
||||
this.hass.themes.themes && Object.keys(this.hass.themes.themes).length;
|
||||
|
||||
const curThemeIsUseDefault = this.hass.selectedTheme?.theme === "";
|
||||
const curTheme = this.hass.selectedTheme?.theme
|
||||
? this.hass.selectedTheme?.theme
|
||||
: this.hass.themes.darkMode
|
||||
const curThemeIsUseDefault = this._selectedTheme?.theme === "";
|
||||
const curTheme = this._selectedTheme?.theme
|
||||
? this._selectedTheme?.theme
|
||||
: this._selectedTheme?.dark
|
||||
? this.hass.themes.default_dark_theme || this.hass.themes.default_theme
|
||||
: this.hass.themes.default_theme;
|
||||
|
||||
const themeSettings = this.hass.selectedTheme;
|
||||
|
||||
return html`
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading"
|
||||
>${this.hass.localize("ui.panel.profile.themes.header")}</span
|
||||
>
|
||||
<span slot="description">
|
||||
${!hasThemes
|
||||
<span
|
||||
slot="description"
|
||||
class=${this.storageLocation === "user" &&
|
||||
this.hass.browserThemeEnabled
|
||||
? "device-info"
|
||||
: ""}
|
||||
>
|
||||
${!hasThemes &&
|
||||
!(this.storageLocation === "user" && this.hass.browserThemeEnabled)
|
||||
? this.hass.localize("ui.panel.profile.themes.error_no_theme")
|
||||
: ""}
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/integrations/frontend/#defining-themes"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize("ui.panel.profile.themes.link_promo")}
|
||||
</a>
|
||||
${this.storageLocation === "user" && this.hass.browserThemeEnabled
|
||||
? html`<ha-svg-icon .path=${mdiAlertCircleOutline}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.themes.device.user_theme_info"
|
||||
)}`
|
||||
: html`<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/integrations/frontend/#defining-themes"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize("ui.panel.profile.themes.link_promo")}
|
||||
</a>`}
|
||||
</span>
|
||||
<ha-select
|
||||
.label=${this.hass.localize("ui.panel.profile.themes.dropdown_label")}
|
||||
.disabled=${!hasThemes}
|
||||
.value=${this.hass.selectedTheme?.theme || USE_DEFAULT_THEME}
|
||||
.value=${this._selectedTheme?.theme || USE_DEFAULT_THEME}
|
||||
@selected=${this._handleThemeSelection}
|
||||
naturalMenuWidth
|
||||
>
|
||||
<mwc-list-item .value=${USE_DEFAULT_THEME}>
|
||||
<ha-list-item .value=${USE_DEFAULT_THEME}>
|
||||
${this.hass.localize("ui.panel.profile.themes.use_default")}
|
||||
</mwc-list-item>
|
||||
<mwc-list-item .value=${HOME_ASSISTANT_THEME}>
|
||||
</ha-list-item>
|
||||
<ha-list-item .value=${HOME_ASSISTANT_THEME}>
|
||||
Home Assistant
|
||||
</mwc-list-item>
|
||||
</ha-list-item>
|
||||
${this._themeNames.map(
|
||||
(theme) => html`
|
||||
<mwc-list-item .value=${theme}>${theme}</mwc-list-item>
|
||||
<ha-list-item .value=${theme}>${theme}</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
@@ -86,7 +112,7 @@ export class HaPickThemeRow extends LitElement {
|
||||
this.hass.themes.default_dark_theme &&
|
||||
this.hass.themes.default_theme) ||
|
||||
this._supportsModeSelection(curTheme)
|
||||
? html` <div class="inputs">
|
||||
? html`<div class="inputs">
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.profile.themes.dark_mode.auto"
|
||||
@@ -96,7 +122,7 @@ export class HaPickThemeRow extends LitElement {
|
||||
@change=${this._handleDarkMode}
|
||||
name="dark_mode"
|
||||
value="auto"
|
||||
.checked=${themeSettings?.dark === undefined}
|
||||
.checked=${this._selectedTheme?.dark === undefined}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
@@ -108,7 +134,7 @@ export class HaPickThemeRow extends LitElement {
|
||||
@change=${this._handleDarkMode}
|
||||
name="dark_mode"
|
||||
value="light"
|
||||
.checked=${themeSettings?.dark === false}
|
||||
.checked=${this._selectedTheme?.dark === false}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
@@ -121,14 +147,14 @@ export class HaPickThemeRow extends LitElement {
|
||||
@change=${this._handleDarkMode}
|
||||
name="dark_mode"
|
||||
value="dark"
|
||||
.checked=${themeSettings?.dark === true}
|
||||
.checked=${this._selectedTheme?.dark === true}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
${curTheme === HOME_ASSISTANT_THEME
|
||||
? html`<div class="color-pickers">
|
||||
<ha-textfield
|
||||
.value=${themeSettings?.primaryColor ||
|
||||
.value=${this._selectedTheme?.primaryColor ||
|
||||
DEFAULT_PRIMARY_COLOR}
|
||||
type="color"
|
||||
.label=${this.hass.localize(
|
||||
@@ -138,7 +164,8 @@ export class HaPickThemeRow extends LitElement {
|
||||
@change=${this._handleColorChange}
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
.value=${themeSettings?.accentColor || DEFAULT_ACCENT_COLOR}
|
||||
.value=${this._selectedTheme?.accentColor ||
|
||||
DEFAULT_ACCENT_COLOR}
|
||||
type="color"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.profile.themes.accent_color"
|
||||
@@ -146,19 +173,36 @@ export class HaPickThemeRow extends LitElement {
|
||||
.name=${"accentColor"}
|
||||
@change=${this._handleColorChange}
|
||||
></ha-textfield>
|
||||
${themeSettings?.primaryColor || themeSettings?.accentColor
|
||||
? html` <mwc-button @click=${this._resetColors}>
|
||||
${this._selectedTheme?.primaryColor ||
|
||||
this._selectedTheme?.accentColor
|
||||
? html`<ha-button @click=${this._resetColors}>
|
||||
${this.hass.localize("ui.panel.profile.themes.reset")}
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
</div>`
|
||||
: ""}
|
||||
: nothing}
|
||||
</div>`
|
||||
: ""}
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
public willUpdate(changedProperties: PropertyValues) {
|
||||
if (!this.hasUpdated) {
|
||||
if (this.storageLocation === "browser") {
|
||||
this._selectedTheme = this.hass.selectedTheme ?? undefined;
|
||||
} else {
|
||||
this._loading = true;
|
||||
this._selectedTheme = undefined;
|
||||
subscribeSelectedTheme(
|
||||
this.hass,
|
||||
(selectedTheme?: ThemeSettings | null) => {
|
||||
this._selectedTheme = selectedTheme ?? undefined;
|
||||
this._loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const oldHass = changedProperties.get("hass") as undefined | HomeAssistant;
|
||||
const themesChanged =
|
||||
changedProperties.has("hass") &&
|
||||
@@ -169,13 +213,34 @@ export class HaPickThemeRow extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _shouldSaveHass() {
|
||||
return (
|
||||
this.storageLocation === "browser" ||
|
||||
(this.storageLocation === "user" && !this.hass.browserThemeEnabled)
|
||||
);
|
||||
}
|
||||
|
||||
private _updateSelectedTheme(updatedTheme: Partial<ThemeSettings>) {
|
||||
this._selectedTheme = {
|
||||
...this._selectedTheme,
|
||||
...updatedTheme,
|
||||
theme: updatedTheme.theme ?? this._selectedTheme?.theme ?? "",
|
||||
};
|
||||
|
||||
fireEvent(this, "settheme", {
|
||||
settings: this._selectedTheme,
|
||||
storageLocation: this.storageLocation,
|
||||
saveHass: this._shouldSaveHass(),
|
||||
});
|
||||
}
|
||||
|
||||
private _handleColorChange(ev: CustomEvent) {
|
||||
const target = ev.target as any;
|
||||
fireEvent(this, "settheme", { [target.name]: target.value });
|
||||
this._updateSelectedTheme({ [target.name]: target.value });
|
||||
}
|
||||
|
||||
private _resetColors() {
|
||||
fireEvent(this, "settheme", {
|
||||
this._updateSelectedTheme({
|
||||
primaryColor: undefined,
|
||||
accentColor: undefined,
|
||||
});
|
||||
@@ -198,18 +263,18 @@ export class HaPickThemeRow extends LitElement {
|
||||
dark = true;
|
||||
break;
|
||||
}
|
||||
fireEvent(this, "settheme", { dark });
|
||||
this._updateSelectedTheme({ dark });
|
||||
}
|
||||
|
||||
private _handleThemeSelection(ev) {
|
||||
const theme = ev.target.value;
|
||||
if (theme === this.hass.selectedTheme?.theme) {
|
||||
if (theme === this._selectedTheme?.theme) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (theme === USE_DEFAULT_THEME) {
|
||||
if (this.hass.selectedTheme?.theme) {
|
||||
fireEvent(this, "settheme", {
|
||||
this._updateSelectedTheme({
|
||||
theme: "",
|
||||
primaryColor: undefined,
|
||||
accentColor: undefined,
|
||||
@@ -217,7 +282,7 @@ export class HaPickThemeRow extends LitElement {
|
||||
}
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "settheme", {
|
||||
this._updateSelectedTheme({
|
||||
theme,
|
||||
primaryColor: undefined,
|
||||
accentColor: undefined,
|
||||
@@ -249,6 +314,12 @@ export class HaPickThemeRow extends LitElement {
|
||||
flex-grow: 1;
|
||||
margin: 0 4px;
|
||||
}
|
||||
.device-info {
|
||||
color: var(--warning-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import "@material/mwc-button";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-card";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-expansion-panel";
|
||||
import "../../components/ha-switch";
|
||||
import "../../layouts/hass-tabs-subpage";
|
||||
import { profileSections } from "./ha-panel-profile";
|
||||
import { isExternal } from "../../data/external";
|
||||
@@ -27,6 +29,7 @@ import "./ha-pick-time-zone-row";
|
||||
import "./ha-push-notifications-row";
|
||||
import "./ha-set-suspend-row";
|
||||
import "./ha-set-vibrate-row";
|
||||
import type { HaSwitch } from "../../components/ha-switch";
|
||||
|
||||
@customElement("ha-profile-section-general")
|
||||
class HaProfileSectionGeneral extends LitElement {
|
||||
@@ -38,6 +41,8 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _browserThemeActivated = false;
|
||||
|
||||
private _unsubCoreData?: UnsubscribeFunc;
|
||||
|
||||
private _getCoreData() {
|
||||
@@ -91,9 +96,9 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
: ""}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button class="warning" @click=${this._handleLogOut}>
|
||||
<ha-button class="warning" @click=${this._handleLogOut}>
|
||||
${this.hass.localize("ui.panel.profile.logout")}
|
||||
</mwc-button>
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card
|
||||
@@ -128,6 +133,12 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-first-weekday-row>
|
||||
<ha-pick-theme-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
this._browserThemeActivated}
|
||||
.storageLocation=${"user"}
|
||||
></ha-pick-theme-row>
|
||||
${this.hass.user!.is_admin
|
||||
? html`
|
||||
<ha-advanced-mode-row
|
||||
@@ -148,10 +159,42 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
<div class="card-content">
|
||||
${this.hass.localize("ui.panel.profile.client_settings_detail")}
|
||||
</div>
|
||||
<ha-pick-theme-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-theme-row>
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
isExternal
|
||||
? "ui.panel.profile.themes.device.mobile_app_header"
|
||||
: "ui.panel.profile.themes.device.browser_header"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.themes.device.description"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
.checked=${this.hass.browserThemeEnabled ||
|
||||
this._browserThemeActivated}
|
||||
@change=${this._toggleBrowserTheme}
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
${this.hass.browserThemeEnabled || this._browserThemeActivated
|
||||
? html`
|
||||
<ha-expansion-panel
|
||||
outlined
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.profile.themes.device.custom_theme"
|
||||
)}
|
||||
expanded
|
||||
>
|
||||
<ha-pick-theme-row
|
||||
class="device-theme"
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-theme-row>
|
||||
</ha-expansion-panel>
|
||||
`
|
||||
: nothing}
|
||||
<ha-pick-dashboard-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
@@ -167,11 +210,11 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
"ui.panel.profile.customize_sidebar.description"
|
||||
)}
|
||||
</span>
|
||||
<mwc-button @click=${this._customizeSidebar}>
|
||||
<ha-button @click=${this._customizeSidebar}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.customize_sidebar.button"
|
||||
)}
|
||||
</mwc-button>
|
||||
</ha-button>
|
||||
</ha-settings-row>
|
||||
${this.hass.dockedSidebar !== "auto" || !this.narrow
|
||||
? html`
|
||||
@@ -225,6 +268,39 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private async _toggleBrowserTheme(ev: Event) {
|
||||
const switchElement = ev.target as HaSwitch;
|
||||
const enabled = switchElement.checked;
|
||||
|
||||
if (!enabled) {
|
||||
if (!this.hass.browserThemeEnabled && this._browserThemeActivated) {
|
||||
// no changed have made, disable without confirmation
|
||||
this._browserThemeActivated = false;
|
||||
} else {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.profile.themes.device.delete_header"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.profile.themes.device.delete_description"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (confirm) {
|
||||
this._browserThemeActivated = false;
|
||||
fireEvent(this, "resetBrowserTheme");
|
||||
} else {
|
||||
// revert switch
|
||||
switchElement.click();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._browserThemeActivated = true;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -251,6 +327,14 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
text-align: center;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-expansion-panel {
|
||||
margin: 0 8px 8px;
|
||||
}
|
||||
.device-theme {
|
||||
display: block;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
+111
-14
@@ -3,18 +3,37 @@ import {
|
||||
invalidateThemeCache,
|
||||
} from "../common/dom/apply_themes_on_element";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import { subscribeThemes } from "../data/ws-themes";
|
||||
import type { Constructor, HomeAssistant } from "../types";
|
||||
import { storeState } from "../util/ha-pref-storage";
|
||||
import {
|
||||
fetchSelectedTheme,
|
||||
saveSelectedTheme,
|
||||
SELECTED_THEME_KEY,
|
||||
subscribeSelectedTheme,
|
||||
subscribeThemes,
|
||||
} from "../data/ws-themes";
|
||||
import type { Constructor, HomeAssistant, ThemeSettings } from "../types";
|
||||
import { clearStateKey, storeState } from "../util/ha-pref-storage";
|
||||
import type { HassBaseEl } from "./hass-base-mixin";
|
||||
|
||||
export type StorageLocation = "user" | "browser";
|
||||
|
||||
interface SetThemeSettings {
|
||||
settings: ThemeSettings;
|
||||
storageLocation: StorageLocation;
|
||||
saveHass: boolean;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// for add event listener
|
||||
interface HTMLElementEventMap {
|
||||
settheme: HASSDomEvent<Partial<HomeAssistant["selectedTheme"]>>;
|
||||
settheme: HASSDomEvent<SetThemeSettings>;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
settheme: Partial<HomeAssistant["selectedTheme"]>;
|
||||
settheme: SetThemeSettings;
|
||||
resetBrowserTheme: undefined;
|
||||
}
|
||||
|
||||
interface FrontendUserData {
|
||||
selectedTheme?: ThemeSettings;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,16 +46,38 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
this.addEventListener("settheme", (ev) => {
|
||||
this._updateHass({
|
||||
selectedTheme: {
|
||||
...this.hass!.selectedTheme!,
|
||||
...ev.detail,
|
||||
},
|
||||
});
|
||||
this._applyTheme(mql.matches);
|
||||
storeState(this.hass!);
|
||||
if (ev.detail.saveHass) {
|
||||
this._updateHass({
|
||||
selectedTheme: ev.detail.settings,
|
||||
browserThemeEnabled: ev.detail.storageLocation === "browser",
|
||||
});
|
||||
this._animateApplyTheme(mql.matches);
|
||||
}
|
||||
|
||||
if (ev.detail.storageLocation === "browser") {
|
||||
storeState(this.hass!);
|
||||
} else {
|
||||
if (ev.detail.saveHass) {
|
||||
clearStateKey(SELECTED_THEME_KEY);
|
||||
}
|
||||
saveSelectedTheme(this.hass!, ev.detail.settings);
|
||||
}
|
||||
});
|
||||
mql.addListener((ev) => this._applyTheme(ev.matches));
|
||||
|
||||
this.addEventListener("resetBrowserTheme", async () => {
|
||||
clearStateKey(SELECTED_THEME_KEY);
|
||||
const selectedTheme = await fetchSelectedTheme(this.hass!);
|
||||
this._updateHass({
|
||||
selectedTheme,
|
||||
browserThemeEnabled: false,
|
||||
});
|
||||
this._animateApplyTheme(mql.matches);
|
||||
});
|
||||
|
||||
mql.addEventListener("change", (ev) =>
|
||||
this._animateApplyTheme(ev.matches)
|
||||
);
|
||||
|
||||
if (!this._themeApplied && mql.matches) {
|
||||
applyThemesOnElement(
|
||||
document.documentElement,
|
||||
@@ -63,6 +104,62 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
invalidateThemeCache();
|
||||
this._applyTheme(mql.matches);
|
||||
});
|
||||
|
||||
subscribeSelectedTheme(
|
||||
this.hass!,
|
||||
(selectedTheme?: ThemeSettings | null) => {
|
||||
if (
|
||||
!window.localStorage.getItem(SELECTED_THEME_KEY) &&
|
||||
selectedTheme
|
||||
) {
|
||||
this._themeApplied = true;
|
||||
this._updateHass({
|
||||
selectedTheme,
|
||||
browserThemeEnabled: false,
|
||||
});
|
||||
this._applyTheme(mql.matches);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async _animateApplyTheme(darkPreferred: boolean) {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!document.startViewTransition || !document.documentElement.animate) {
|
||||
this._applyTheme(darkPreferred);
|
||||
} else {
|
||||
await document.startViewTransition(() => {
|
||||
this._applyTheme(darkPreferred);
|
||||
}).ready;
|
||||
|
||||
const { top, left, width, height } =
|
||||
document.documentElement.getBoundingClientRect();
|
||||
const x = left + width / 2;
|
||||
const y = top + height / 2;
|
||||
const right = window.innerWidth - left;
|
||||
const bottom = window.innerHeight - top;
|
||||
const maxRadius = Math.hypot(
|
||||
Math.max(left, right),
|
||||
Math.max(top, bottom)
|
||||
);
|
||||
|
||||
document.documentElement.animate(
|
||||
{
|
||||
clipPath: [
|
||||
`circle(0px at ${x}px ${y}px)`,
|
||||
`circle(${maxRadius}px at ${x}px ${y}px)`,
|
||||
],
|
||||
},
|
||||
{
|
||||
duration: 500,
|
||||
easing: "linear(0, 0.1, 1)",
|
||||
pseudoElement: "::view-transition-new(root)",
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _applyTheme(darkPreferred: boolean) {
|
||||
|
||||
@@ -7575,7 +7575,16 @@
|
||||
"primary_color": "Primary color",
|
||||
"accent_color": "Accent color",
|
||||
"reset": "Reset",
|
||||
"use_default": "Use default theme"
|
||||
"use_default": "Use default theme",
|
||||
"device": {
|
||||
"browser_header": "Browser theme",
|
||||
"mobile_app_header": "Mobile app theme",
|
||||
"custom_theme": "Custom theme",
|
||||
"description": "Overwrite user theme with custom device settings",
|
||||
"delete_header": "Delete device theme",
|
||||
"delete_description": "Are you sure you want to delete the device specific theme?",
|
||||
"user_theme_info": "Device theme is active. You won't see changes on user theme settings."
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"header": "Dashboard",
|
||||
@@ -8193,9 +8202,53 @@
|
||||
},
|
||||
"restore": {
|
||||
"header": "Restore a backup",
|
||||
"upload_backup": "[%key:ui::panel::config::backup::dialogs::upload::title%]",
|
||||
"unsupported": {
|
||||
"title": "[%key:ui::panel::config::backup::dialogs::upload::unsupported::title%]",
|
||||
"text": "[%key:ui::panel::config::backup::dialogs::upload::unsupported::text%]"
|
||||
},
|
||||
"ok": "[%key:ui::common::ok%]",
|
||||
"upload_input_label": "[%key:ui::panel::config::backup::dialogs::upload::input_label%]",
|
||||
"upload_supports_tar": "[%key:ui::panel::config::backup::dialogs::upload::supports_tar%]",
|
||||
"upload_secondary": "[%key:ui::components::file-upload::secondary%]",
|
||||
"delete": "[%key:ui::common::delete%]",
|
||||
"uploading": "[%key:ui::components::file-upload::uploading%]",
|
||||
"details": {
|
||||
"home_assistant_missing": "This backup does not include your Home Assistant configuration, you cannot use it to restore your instance.",
|
||||
"addons_unsupported": "This backup includes add-ons and folders, which are not supported in this installation method of Home Assistant. You can still restore Home Assistant, but the unsupported files will not be restored.",
|
||||
"summary": {
|
||||
"title": "[%key:ui::panel::config::backup::details::summary::title%]",
|
||||
"size": "[%key:ui::panel::config::backup::details::summary::size%]",
|
||||
"created": "[%key:ui::panel::config::backup::details::summary::created%]",
|
||||
"upload_another": "Upload another",
|
||||
"home": "Home"
|
||||
},
|
||||
"restore": {
|
||||
"title": "[%key:ui::panel::config::backup::details::restore::title%]",
|
||||
"action": "[%key:ui::panel::config::backup::details::restore::action%]",
|
||||
"encryption": {
|
||||
"title": "[%key:ui::panel::config::backup::dialogs::restore::encryption::different_key%]",
|
||||
"incorrect_key": "[%key:ui::panel::config::backup::dialogs::restore::encryption::incorrect_key%]",
|
||||
"input_label": "[%key:ui::panel::config::backup::dialogs::restore::encryption::input_label%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"data_picker": {
|
||||
"settings": "[%key:ui::panel::config::backup::data_picker::settings%]",
|
||||
"settings_and_history": "[%key:ui::panel::config::backup::data_picker::settings_and_history%]",
|
||||
"media": "[%key:ui::panel::config::backup::data_picker::media%]",
|
||||
"share_folder": "[%key:ui::panel::config::backup::data_picker::share_folder%]",
|
||||
"local_addons": "[%key:ui::panel::config::backup::data_picker::local_addons%]",
|
||||
"addons": "[%key:ui::panel::config::backup::data_picker::addons%]",
|
||||
"ssl": "[%key:ui::panel::config::backup::data_picker::ssl%]"
|
||||
},
|
||||
"restore_no_home_assistant": "[%key:supervisor::backup::restore_no_home_assistant%]",
|
||||
"in_progress": "Restore in progress",
|
||||
"in_progress_description": "The restore process is running in the background. Home Assistant will automatically start again once the restore is complete. Please be patient, this can take a while. Do not close or refresh this page.",
|
||||
"failed": "Restore failed",
|
||||
"upload_backup": "[%key:supervisor::backup::upload_backup%]",
|
||||
"failed_status_description": "The backup could not be restored. Please try again.",
|
||||
"failed_description": "The last restore attempt failed. Please try it again.",
|
||||
"failed_wrong_password_description": "The last restore attempt failed, because the encryption key was incorrect. Please try it again.",
|
||||
"upload_supports": "Supports .TAR files",
|
||||
"upload_drop": "[%key:ui::components::file-upload::secondary%]",
|
||||
"show_log": "Show full log",
|
||||
@@ -8203,7 +8256,6 @@
|
||||
"full_backup": "[%key:supervisor::backup::full_backup%]",
|
||||
"partial_backup": "[%key:supervisor::backup::partial_backup%]",
|
||||
"name": "[%key:supervisor::backup::name%]",
|
||||
"type": "[%key:supervisor::backup::type%]",
|
||||
"select_type": "[%key:supervisor::backup::select_type%]",
|
||||
"folders": "[%key:supervisor::backup::folders%]",
|
||||
"addons": "[%key:supervisor::backup::addons%]",
|
||||
@@ -8218,10 +8270,16 @@
|
||||
"close": "[%key:ui::common::close%]",
|
||||
"cancel": "[%key:ui::common::cancel%]",
|
||||
"retry": "Retry",
|
||||
"back": "[%key:ui::common::back%]",
|
||||
"restore_start_failed": "[%key:supervisor::backup::restore_start_failed%]",
|
||||
"no_backup_found": "[%key:supervisor::backup::no_backup_found%]",
|
||||
"restore_no_home_assistant": "[%key:supervisor::backup::restore_no_home_assistant%]",
|
||||
"unnamed_backup": "[%key:supervisor::backup::unnamed_backup%]"
|
||||
"unnamed_backup": "[%key:supervisor::backup::unnamed_backup%]",
|
||||
"cancel_restore": {
|
||||
"title": "Cancel restore process?",
|
||||
"text": "Are you sure you want to cancel the restore process and return to the onboarding?",
|
||||
"yes": "[%key:ui::common::yes%]",
|
||||
"no": "[%key:ui::common::no%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"custom": {
|
||||
|
||||
@@ -223,6 +223,7 @@ export interface HomeAssistant {
|
||||
config: HassConfig;
|
||||
themes: Themes;
|
||||
selectedTheme: ThemeSettings | null;
|
||||
browserThemeEnabled?: boolean;
|
||||
panels: Panels;
|
||||
panelUrl: string;
|
||||
// i18n
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { SELECTED_THEME_KEY } from "../data/ws-themes";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
const STORED_STATE = [
|
||||
"dockedSidebar",
|
||||
"selectedTheme",
|
||||
SELECTED_THEME_KEY,
|
||||
"selectedLanguage",
|
||||
"vibrate",
|
||||
"debugConnection",
|
||||
@@ -11,9 +12,17 @@ const STORED_STATE = [
|
||||
"defaultPanel",
|
||||
];
|
||||
|
||||
const CLEARABLE_STATE = [SELECTED_THEME_KEY];
|
||||
|
||||
export function storeState(hass: HomeAssistant) {
|
||||
try {
|
||||
STORED_STATE.forEach((key) => {
|
||||
const states = [...STORED_STATE];
|
||||
|
||||
if (!hass.browserThemeEnabled) {
|
||||
states.splice(states.indexOf(SELECTED_THEME_KEY), 1);
|
||||
}
|
||||
|
||||
states.forEach((key) => {
|
||||
const value = hass[key];
|
||||
window.localStorage.setItem(
|
||||
key,
|
||||
@@ -32,15 +41,18 @@ export function storeState(hass: HomeAssistant) {
|
||||
}
|
||||
|
||||
export function getState() {
|
||||
const state = {};
|
||||
const state: Partial<HomeAssistant> = {};
|
||||
|
||||
STORED_STATE.forEach((key) => {
|
||||
const storageItem = window.localStorage.getItem(key);
|
||||
if (storageItem !== null) {
|
||||
let value = JSON.parse(storageItem);
|
||||
// selectedTheme went from string to object on 20200718
|
||||
if (key === "selectedTheme" && typeof value === "string") {
|
||||
value = { theme: value };
|
||||
if (key === SELECTED_THEME_KEY) {
|
||||
if (typeof value === "string") {
|
||||
value = { theme: value };
|
||||
}
|
||||
state.browserThemeEnabled = true;
|
||||
}
|
||||
// dockedSidebar went from boolean to enum on 20190720
|
||||
if (key === "dockedSidebar" && typeof value === "boolean") {
|
||||
@@ -55,3 +67,9 @@ export function getState() {
|
||||
export function clearState() {
|
||||
window.localStorage.clear();
|
||||
}
|
||||
|
||||
export function clearStateKey(key: string) {
|
||||
if (CLEARABLE_STATE.includes(key)) {
|
||||
window.localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
formatDateTime,
|
||||
formatDateTimeWithSeconds,
|
||||
formatDateTimeNumeric,
|
||||
formatDateTimeWithBrowserDefaults,
|
||||
} from "../../../src/common/datetime/format_date_time";
|
||||
import {
|
||||
NumberFormat,
|
||||
@@ -49,6 +50,19 @@ describe("formatDateTime", () => {
|
||||
"November 18, 2017 at 23:12"
|
||||
);
|
||||
});
|
||||
|
||||
it("Formats date times without optional params", () => {
|
||||
assert.strictEqual(
|
||||
formatDateTimeWithBrowserDefaults(dateObj),
|
||||
new Intl.DateTimeFormat(undefined, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}).format(dateObj)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatDateTimeWithSeconds", () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ describe("ha-pref-storage", () => {
|
||||
const mockHass = {
|
||||
dockedSidebar: "auto",
|
||||
selectedTheme: { theme: "default" },
|
||||
vibrate: "false",
|
||||
unknownKey: "unknownValue",
|
||||
};
|
||||
|
||||
@@ -24,15 +25,11 @@ describe("ha-pref-storage", () => {
|
||||
window.localStorage.setItem = vi.fn();
|
||||
|
||||
storeState(mockHass as unknown as HomeAssistant);
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledTimes(8);
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledTimes(7);
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledWith(
|
||||
"dockedSidebar",
|
||||
JSON.stringify("auto")
|
||||
);
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledWith(
|
||||
"selectedTheme",
|
||||
JSON.stringify({ theme: "default" })
|
||||
);
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledWith(
|
||||
"selectedLanguage",
|
||||
JSON.stringify(null)
|
||||
@@ -41,13 +38,19 @@ describe("ha-pref-storage", () => {
|
||||
"unknownKey",
|
||||
JSON.stringify("unknownValue")
|
||||
);
|
||||
|
||||
// browserThemeEnabled is not set in mockHass, so selectedTheme should not be stored
|
||||
expect(window.localStorage.setItem).not.toHaveBeenCalledWith(
|
||||
"selectedTheme",
|
||||
JSON.stringify({ theme: "default" })
|
||||
);
|
||||
});
|
||||
|
||||
test("storeState fails", async () => {
|
||||
const { storeState } = await import("../../src/util/ha-pref-storage");
|
||||
|
||||
window.localStorage.setItem = vi.fn((key) => {
|
||||
if (key === "selectedTheme") {
|
||||
if (key === "selectedLanguage") {
|
||||
throw new Error("Test error");
|
||||
}
|
||||
});
|
||||
@@ -65,13 +68,13 @@ describe("ha-pref-storage", () => {
|
||||
JSON.stringify("auto")
|
||||
);
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledWith(
|
||||
"selectedTheme",
|
||||
JSON.stringify({ theme: "default" })
|
||||
);
|
||||
expect(window.localStorage.setItem).not.toHaveBeenCalledWith(
|
||||
"selectedLanguage",
|
||||
JSON.stringify(null)
|
||||
);
|
||||
expect(window.localStorage.setItem).not.toHaveBeenCalledWith(
|
||||
"vibrate",
|
||||
JSON.stringify("false")
|
||||
);
|
||||
// eslint-disable-next-line no-console
|
||||
expect(console.warn).toHaveBeenCalledOnce();
|
||||
// eslint-disable-next-line no-console
|
||||
@@ -87,7 +90,7 @@ describe("ha-pref-storage", () => {
|
||||
|
||||
window.localStorage.setItem("selectedTheme", JSON.stringify("test"));
|
||||
window.localStorage.setItem("dockedSidebar", JSON.stringify(true));
|
||||
window.localStorage.setItem("selectedLanguage", JSON.stringify("german"));
|
||||
window.localStorage.setItem("selectedLanguage", JSON.stringify("de"));
|
||||
|
||||
// should not be in state
|
||||
window.localStorage.setItem("testEntry", JSON.stringify("this is a test"));
|
||||
@@ -96,7 +99,21 @@ describe("ha-pref-storage", () => {
|
||||
expect(state).toEqual({
|
||||
dockedSidebar: "docked",
|
||||
selectedTheme: { theme: "test" },
|
||||
selectedLanguage: "german",
|
||||
browserThemeEnabled: true,
|
||||
selectedLanguage: "de",
|
||||
});
|
||||
});
|
||||
|
||||
test("getState without theme", async () => {
|
||||
const { getState } = await import("../../src/util/ha-pref-storage");
|
||||
|
||||
window.localStorage.setItem("dockedSidebar", JSON.stringify(true));
|
||||
window.localStorage.setItem("selectedLanguage", JSON.stringify("de"));
|
||||
|
||||
const state = getState();
|
||||
expect(state).toEqual({
|
||||
dockedSidebar: "docked",
|
||||
selectedLanguage: "de",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1616,6 +1616,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/core@npm:^0.11.0":
|
||||
version: 0.11.0
|
||||
resolution: "@eslint/core@npm:0.11.0"
|
||||
dependencies:
|
||||
"@types/json-schema": "npm:^7.0.15"
|
||||
checksum: 10/0a055edf516529d19eea2196e3149eefb4c6f0bb30145b08cdb92ec114735630bd27585f76466c7cb6fa1073617d1f5e49b36ad63d4d45e55defd94a3268256d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/eslintrc@npm:^3.2.0":
|
||||
version: 3.2.0
|
||||
resolution: "@eslint/eslintrc@npm:3.2.0"
|
||||
@@ -1633,10 +1642,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/js@npm:9.19.0":
|
||||
version: 9.19.0
|
||||
resolution: "@eslint/js@npm:9.19.0"
|
||||
checksum: 10/d8133a83330676d5f0827713af2e9bbf35530631a93520fb59ead6b827a325c54fdd7ad99f2158f895fb393c47bbc55dfdaa945998a647f3b9230f1d5324a626
|
||||
"@eslint/js@npm:9.20.0":
|
||||
version: 9.20.0
|
||||
resolution: "@eslint/js@npm:9.20.0"
|
||||
checksum: 10/2304cd725700046ba611f06bf9cd0941db7e02e0d602d8fd9e4734c13067699954597b9a3a2b048ce02eb0550373669d2ab7be6edaf6abf7b67eb19d1276b57b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -5105,131 +5114,131 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vaadin/a11y-base@npm:~24.6.3":
|
||||
version: 24.6.3
|
||||
resolution: "@vaadin/a11y-base@npm:24.6.3"
|
||||
"@vaadin/a11y-base@npm:~24.6.4":
|
||||
version: 24.6.4
|
||||
resolution: "@vaadin/a11y-base@npm:24.6.4"
|
||||
dependencies:
|
||||
"@open-wc/dedupe-mixin": "npm:^1.3.0"
|
||||
"@polymer/polymer": "npm:^3.0.0"
|
||||
"@vaadin/component-base": "npm:~24.6.3"
|
||||
"@vaadin/component-base": "npm:~24.6.4"
|
||||
lit: "npm:^3.0.0"
|
||||
checksum: 10/1500447c500946bd4e3423b2117e37fb317fc348d317aee8d786260e1435b9252f5bce90b55a5b1ca8734d987a90ca55124922a5f2ae52caf809f92cf05df4d5
|
||||
checksum: 10/5d0a9261f329fda7b28c5487999774fedd0e2edb339b6f7eecf6749acbddb137569bf730e0701812c52d6e23fe65101d393c0c860931257f5cdfbbedd537c1cd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vaadin/combo-box@npm:24.6.3":
|
||||
version: 24.6.3
|
||||
resolution: "@vaadin/combo-box@npm:24.6.3"
|
||||
"@vaadin/combo-box@npm:24.6.4":
|
||||
version: 24.6.4
|
||||
resolution: "@vaadin/combo-box@npm:24.6.4"
|
||||
dependencies:
|
||||
"@open-wc/dedupe-mixin": "npm:^1.3.0"
|
||||
"@polymer/polymer": "npm:^3.0.0"
|
||||
"@vaadin/a11y-base": "npm:~24.6.3"
|
||||
"@vaadin/component-base": "npm:~24.6.3"
|
||||
"@vaadin/field-base": "npm:~24.6.3"
|
||||
"@vaadin/input-container": "npm:~24.6.3"
|
||||
"@vaadin/item": "npm:~24.6.3"
|
||||
"@vaadin/lit-renderer": "npm:~24.6.3"
|
||||
"@vaadin/overlay": "npm:~24.6.3"
|
||||
"@vaadin/vaadin-lumo-styles": "npm:~24.6.3"
|
||||
"@vaadin/vaadin-material-styles": "npm:~24.6.3"
|
||||
"@vaadin/vaadin-themable-mixin": "npm:~24.6.3"
|
||||
"@vaadin/a11y-base": "npm:~24.6.4"
|
||||
"@vaadin/component-base": "npm:~24.6.4"
|
||||
"@vaadin/field-base": "npm:~24.6.4"
|
||||
"@vaadin/input-container": "npm:~24.6.4"
|
||||
"@vaadin/item": "npm:~24.6.4"
|
||||
"@vaadin/lit-renderer": "npm:~24.6.4"
|
||||
"@vaadin/overlay": "npm:~24.6.4"
|
||||
"@vaadin/vaadin-lumo-styles": "npm:~24.6.4"
|
||||
"@vaadin/vaadin-material-styles": "npm:~24.6.4"
|
||||
"@vaadin/vaadin-themable-mixin": "npm:~24.6.4"
|
||||
lit: "npm:^3.0.0"
|
||||
checksum: 10/54dbcde231f262c68abf6756ad7ceb89ed3c424b08f9995cc321920d93b8a8c3be69d0fc0d67262bd81714ea491eeb6ac60400950ea2cf6ea90bd8271a88548b
|
||||
checksum: 10/9c014ab3647a1c887e7cf2f707681c453bedcfc619580900341730b1de2588ecc139ac6dd79b614a13bd1a9fa07d8499cf8bddddffd7d4eac9f30094dca152a1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vaadin/component-base@npm:~24.6.3":
|
||||
version: 24.6.3
|
||||
resolution: "@vaadin/component-base@npm:24.6.3"
|
||||
"@vaadin/component-base@npm:~24.6.4":
|
||||
version: 24.6.4
|
||||
resolution: "@vaadin/component-base@npm:24.6.4"
|
||||
dependencies:
|
||||
"@open-wc/dedupe-mixin": "npm:^1.3.0"
|
||||
"@polymer/polymer": "npm:^3.0.0"
|
||||
"@vaadin/vaadin-development-mode-detector": "npm:^2.0.0"
|
||||
"@vaadin/vaadin-usage-statistics": "npm:^2.1.0"
|
||||
lit: "npm:^3.0.0"
|
||||
checksum: 10/5fb9032e37448257d9dad897f45bc80230e736b0c06fe7ac1f73db831dbaa0da29589b81cc3bfb6be395de49e2a99865110f99562bff0da35ac6149e63e6549d
|
||||
checksum: 10/e3fe5e74b06fa9496b5a448a1328c03385ac078b25125a610fc330a05bf26b82234a0264e4d4c9b2399e207b14c6a6ba65bd908afa05073053059b4a01115995
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vaadin/field-base@npm:~24.6.3":
|
||||
version: 24.6.3
|
||||
resolution: "@vaadin/field-base@npm:24.6.3"
|
||||
"@vaadin/field-base@npm:~24.6.4":
|
||||
version: 24.6.4
|
||||
resolution: "@vaadin/field-base@npm:24.6.4"
|
||||
dependencies:
|
||||
"@open-wc/dedupe-mixin": "npm:^1.3.0"
|
||||
"@polymer/polymer": "npm:^3.0.0"
|
||||
"@vaadin/a11y-base": "npm:~24.6.3"
|
||||
"@vaadin/component-base": "npm:~24.6.3"
|
||||
"@vaadin/a11y-base": "npm:~24.6.4"
|
||||
"@vaadin/component-base": "npm:~24.6.4"
|
||||
lit: "npm:^3.0.0"
|
||||
checksum: 10/19261611de19b85eb87047053353de85c41690066474d6e4476af4f6ae5056ac187fc94b0d9bb6c388d63d9030362aafc9cd46b3eb9b7d3f563f5ca2d6ae920b
|
||||
checksum: 10/ede2a08cf332e639117835dfcb2ee274b9df8c5f9af0a1611638719090cdeb29f925d11244183219c346d3bf4b6929042f8008087afe38c26ea6c6ed7d849491
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vaadin/icon@npm:~24.6.3":
|
||||
version: 24.6.3
|
||||
resolution: "@vaadin/icon@npm:24.6.3"
|
||||
"@vaadin/icon@npm:~24.6.4":
|
||||
version: 24.6.4
|
||||
resolution: "@vaadin/icon@npm:24.6.4"
|
||||
dependencies:
|
||||
"@open-wc/dedupe-mixin": "npm:^1.3.0"
|
||||
"@polymer/polymer": "npm:^3.0.0"
|
||||
"@vaadin/component-base": "npm:~24.6.3"
|
||||
"@vaadin/vaadin-lumo-styles": "npm:~24.6.3"
|
||||
"@vaadin/vaadin-themable-mixin": "npm:~24.6.3"
|
||||
"@vaadin/component-base": "npm:~24.6.4"
|
||||
"@vaadin/vaadin-lumo-styles": "npm:~24.6.4"
|
||||
"@vaadin/vaadin-themable-mixin": "npm:~24.6.4"
|
||||
lit: "npm:^3.0.0"
|
||||
checksum: 10/6b4e3e051296c2abcef01c0bbd9581058a4b530272668d3db3e3a5e97f60fbb3ff608f64ea1679550f162d5b3682b62f63e98fb232a12dadd2e038be911edb80
|
||||
checksum: 10/bf611bbb43801cba77d6cebf2eb9c7de9939b238a111f1364f183d0f96775639b4312bf2e7d866684b829e028af7dca4d55b1277b780267e104b424101270daa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vaadin/input-container@npm:~24.6.3":
|
||||
version: 24.6.3
|
||||
resolution: "@vaadin/input-container@npm:24.6.3"
|
||||
"@vaadin/input-container@npm:~24.6.4":
|
||||
version: 24.6.4
|
||||
resolution: "@vaadin/input-container@npm:24.6.4"
|
||||
dependencies:
|
||||
"@polymer/polymer": "npm:^3.0.0"
|
||||
"@vaadin/component-base": "npm:~24.6.3"
|
||||
"@vaadin/vaadin-lumo-styles": "npm:~24.6.3"
|
||||
"@vaadin/vaadin-material-styles": "npm:~24.6.3"
|
||||
"@vaadin/vaadin-themable-mixin": "npm:~24.6.3"
|
||||
"@vaadin/component-base": "npm:~24.6.4"
|
||||
"@vaadin/vaadin-lumo-styles": "npm:~24.6.4"
|
||||
"@vaadin/vaadin-material-styles": "npm:~24.6.4"
|
||||
"@vaadin/vaadin-themable-mixin": "npm:~24.6.4"
|
||||
lit: "npm:^3.0.0"
|
||||
checksum: 10/ebe5e58bd7355aa13ea5d2ab249125a8cbfe44f42bc882facdcce80f7e3b528fbfbb54c9dda04c27853d0809dcfc1cc5359498d2724608d684a2a4e556f5697e
|
||||
checksum: 10/7805a861e9f0a4644586e3b106f8a71b9c888194a212453b4856465442365c880a1655f8dbbd060d9dd5705f59ca0011dda7efdf93b4d47508d58ebcea42e4f4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vaadin/item@npm:~24.6.3":
|
||||
version: 24.6.3
|
||||
resolution: "@vaadin/item@npm:24.6.3"
|
||||
"@vaadin/item@npm:~24.6.4":
|
||||
version: 24.6.4
|
||||
resolution: "@vaadin/item@npm:24.6.4"
|
||||
dependencies:
|
||||
"@open-wc/dedupe-mixin": "npm:^1.3.0"
|
||||
"@polymer/polymer": "npm:^3.0.0"
|
||||
"@vaadin/a11y-base": "npm:~24.6.3"
|
||||
"@vaadin/component-base": "npm:~24.6.3"
|
||||
"@vaadin/vaadin-lumo-styles": "npm:~24.6.3"
|
||||
"@vaadin/vaadin-material-styles": "npm:~24.6.3"
|
||||
"@vaadin/vaadin-themable-mixin": "npm:~24.6.3"
|
||||
"@vaadin/a11y-base": "npm:~24.6.4"
|
||||
"@vaadin/component-base": "npm:~24.6.4"
|
||||
"@vaadin/vaadin-lumo-styles": "npm:~24.6.4"
|
||||
"@vaadin/vaadin-material-styles": "npm:~24.6.4"
|
||||
"@vaadin/vaadin-themable-mixin": "npm:~24.6.4"
|
||||
lit: "npm:^3.0.0"
|
||||
checksum: 10/d8d8968d3378e439fd966e210ac2445da24a807c7ff795f09576ff2681d236309331d814b12bd3d7c7ced3bfdf68dfefe67edd2d3019282deaa0e00f5612c185
|
||||
checksum: 10/d44f766a92aeca1a8d96dccb40fd432846c00feeb25787962a5726a5c4c44937121537e6d6b5b1cb69baba4b905faefeec2a34e5f7bd4d405508f955398d61de
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vaadin/lit-renderer@npm:~24.6.3":
|
||||
version: 24.6.3
|
||||
resolution: "@vaadin/lit-renderer@npm:24.6.3"
|
||||
"@vaadin/lit-renderer@npm:~24.6.4":
|
||||
version: 24.6.4
|
||||
resolution: "@vaadin/lit-renderer@npm:24.6.4"
|
||||
dependencies:
|
||||
lit: "npm:^3.0.0"
|
||||
checksum: 10/21dd3cadc33a696b8f8661d2a458c6e92918a4f3c0565bc2d5a9026ed63688cd59f5404fc051c3a558181c89ea381aaa80142d0df58b221c256a0d3ff5d4437e
|
||||
checksum: 10/ab99999f9e7d140abc62b3182ca5c4905b8ba794be84d9842fb4238d67400620dbe14e924f304cb2493e3bf7435f70fc9719b8f19c6161b32a59ead5b8bf7aa9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vaadin/overlay@npm:~24.6.3":
|
||||
version: 24.6.3
|
||||
resolution: "@vaadin/overlay@npm:24.6.3"
|
||||
"@vaadin/overlay@npm:~24.6.4":
|
||||
version: 24.6.4
|
||||
resolution: "@vaadin/overlay@npm:24.6.4"
|
||||
dependencies:
|
||||
"@open-wc/dedupe-mixin": "npm:^1.3.0"
|
||||
"@polymer/polymer": "npm:^3.0.0"
|
||||
"@vaadin/a11y-base": "npm:~24.6.3"
|
||||
"@vaadin/component-base": "npm:~24.6.3"
|
||||
"@vaadin/vaadin-lumo-styles": "npm:~24.6.3"
|
||||
"@vaadin/vaadin-material-styles": "npm:~24.6.3"
|
||||
"@vaadin/vaadin-themable-mixin": "npm:~24.6.3"
|
||||
"@vaadin/a11y-base": "npm:~24.6.4"
|
||||
"@vaadin/component-base": "npm:~24.6.4"
|
||||
"@vaadin/vaadin-lumo-styles": "npm:~24.6.4"
|
||||
"@vaadin/vaadin-material-styles": "npm:~24.6.4"
|
||||
"@vaadin/vaadin-themable-mixin": "npm:~24.6.4"
|
||||
lit: "npm:^3.0.0"
|
||||
checksum: 10/e36eeeb5b66ea4a9104fb6254edfe5f5b47743277e5c2926e0c41cdbe77580e7c5de8abf6002be40a1a00211f4484d41184f98621aa384a880035bd57bd4b9fb
|
||||
checksum: 10/c237994571696c3fb5d2ac1661befabc301a5020cbf77302eb2c00c4b1e05483c96bd1d4f202efe2a25adab47889d5e851a80373787b9904987f0edd4c045fc1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -5240,36 +5249,36 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vaadin/vaadin-lumo-styles@npm:~24.6.3":
|
||||
version: 24.6.3
|
||||
resolution: "@vaadin/vaadin-lumo-styles@npm:24.6.3"
|
||||
"@vaadin/vaadin-lumo-styles@npm:~24.6.4":
|
||||
version: 24.6.4
|
||||
resolution: "@vaadin/vaadin-lumo-styles@npm:24.6.4"
|
||||
dependencies:
|
||||
"@polymer/polymer": "npm:^3.0.0"
|
||||
"@vaadin/component-base": "npm:~24.6.3"
|
||||
"@vaadin/icon": "npm:~24.6.3"
|
||||
"@vaadin/vaadin-themable-mixin": "npm:~24.6.3"
|
||||
checksum: 10/825587a7996aea3ea48dfdb1e09ac9c90df8b68c8a3204beed0e7828705ce128cf0124e21622e00400b6746ed7997c225b533ee05c3aa2b7a1962b97050efe65
|
||||
"@vaadin/component-base": "npm:~24.6.4"
|
||||
"@vaadin/icon": "npm:~24.6.4"
|
||||
"@vaadin/vaadin-themable-mixin": "npm:~24.6.4"
|
||||
checksum: 10/35ae45a433b864236f1bdec27111c0b25feffa16f199205c896add86d4ef1d53487cab1ef13573d60365f5b3d6cf2e0b7bcd56787a4fbe7de53d5fcfdc74e46f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vaadin/vaadin-material-styles@npm:~24.6.3":
|
||||
version: 24.6.3
|
||||
resolution: "@vaadin/vaadin-material-styles@npm:24.6.3"
|
||||
"@vaadin/vaadin-material-styles@npm:~24.6.4":
|
||||
version: 24.6.4
|
||||
resolution: "@vaadin/vaadin-material-styles@npm:24.6.4"
|
||||
dependencies:
|
||||
"@polymer/polymer": "npm:^3.0.0"
|
||||
"@vaadin/component-base": "npm:~24.6.3"
|
||||
"@vaadin/vaadin-themable-mixin": "npm:~24.6.3"
|
||||
checksum: 10/d274ac0b23e6aed125c5a3ddaf4860537abf63db9969072f184dcb7cdaff6110e013f19afb8cfc52f379595b527977f0159b268f9936bc0e8b8cb6a03456fec7
|
||||
"@vaadin/component-base": "npm:~24.6.4"
|
||||
"@vaadin/vaadin-themable-mixin": "npm:~24.6.4"
|
||||
checksum: 10/6ce2c3017819ca03ade65107f664b3e305b4642c151f7012f769aefef50c467d5ae7f2ff92fb3d7ced9981bf8d881cf9b0c30aef561263b0348b7f215505ecb6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vaadin/vaadin-themable-mixin@npm:24.6.3, @vaadin/vaadin-themable-mixin@npm:~24.6.3":
|
||||
version: 24.6.3
|
||||
resolution: "@vaadin/vaadin-themable-mixin@npm:24.6.3"
|
||||
"@vaadin/vaadin-themable-mixin@npm:24.6.4, @vaadin/vaadin-themable-mixin@npm:~24.6.4":
|
||||
version: 24.6.4
|
||||
resolution: "@vaadin/vaadin-themable-mixin@npm:24.6.4"
|
||||
dependencies:
|
||||
"@open-wc/dedupe-mixin": "npm:^1.3.0"
|
||||
lit: "npm:^3.0.0"
|
||||
checksum: 10/bad0a95ab223a864251e055cff31c1129a77e8e60d868979212679d16b2f62caea63c8627e80059baf0c6e57468bdc5dbef2e2ac3065146ec2c90edb6c9fb8b6
|
||||
checksum: 10/05a8bee2b254dd5c30829ebfc17a04f86e53b0fac163b7e2b53a677b8487cac859afa905a1624dc7a25e15f88bb81572de601aaa4b921fc4b99dd27df7d4a249
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8077,16 +8086,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint@npm:9.19.0":
|
||||
version: 9.19.0
|
||||
resolution: "eslint@npm:9.19.0"
|
||||
"eslint@npm:9.20.0":
|
||||
version: 9.20.0
|
||||
resolution: "eslint@npm:9.20.0"
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils": "npm:^4.2.0"
|
||||
"@eslint-community/regexpp": "npm:^4.12.1"
|
||||
"@eslint/config-array": "npm:^0.19.0"
|
||||
"@eslint/core": "npm:^0.10.0"
|
||||
"@eslint/core": "npm:^0.11.0"
|
||||
"@eslint/eslintrc": "npm:^3.2.0"
|
||||
"@eslint/js": "npm:9.19.0"
|
||||
"@eslint/js": "npm:9.20.0"
|
||||
"@eslint/plugin-kit": "npm:^0.2.5"
|
||||
"@humanfs/node": "npm:^0.16.6"
|
||||
"@humanwhocodes/module-importer": "npm:^1.0.1"
|
||||
@@ -8122,7 +8131,7 @@ __metadata:
|
||||
optional: true
|
||||
bin:
|
||||
eslint: bin/eslint.js
|
||||
checksum: 10/850d19fd6a34702d1e3d9bdad6aef84a20a5c2de006a8fa6380843384b13944b180232ddd74b8725ffcdf8f296399037f0e8eb4783d5f7393f13c059112b843d
|
||||
checksum: 10/6a7686fbda51a0dfe812ef39c251e2c2b0e828196059732d280be6aa375a1f4b5b0b34fed98de30ee363c3bf1b7e8d2a4eccf20b4112f3ad6709f067404a815c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9359,8 +9368,8 @@ __metadata:
|
||||
"@types/webspeechapi": "npm:0.0.29"
|
||||
"@typescript-eslint/eslint-plugin": "npm:8.23.0"
|
||||
"@typescript-eslint/parser": "npm:8.23.0"
|
||||
"@vaadin/combo-box": "npm:24.6.3"
|
||||
"@vaadin/vaadin-themable-mixin": "npm:24.6.3"
|
||||
"@vaadin/combo-box": "npm:24.6.4"
|
||||
"@vaadin/vaadin-themable-mixin": "npm:24.6.4"
|
||||
"@vibrant/color": "npm:4.0.0"
|
||||
"@vitest/coverage-v8": "npm:3.0.5"
|
||||
"@vue/web-component-wrapper": "npm:1.3.0"
|
||||
@@ -9383,7 +9392,7 @@ __metadata:
|
||||
dialog-polyfill: "npm:0.5.6"
|
||||
echarts: "npm:5.6.0"
|
||||
element-internals-polyfill: "npm:1.3.13"
|
||||
eslint: "npm:9.19.0"
|
||||
eslint: "npm:9.20.0"
|
||||
eslint-config-airbnb-base: "npm:15.0.0"
|
||||
eslint-config-prettier: "npm:10.0.1"
|
||||
eslint-import-resolver-webpack: "npm:0.13.10"
|
||||
|
||||
Reference in New Issue
Block a user