mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-27 05:49:39 +00:00
Compare commits
1 Commits
improve_ne
...
climate_to
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0e3f2dc017 |
@@ -5,7 +5,7 @@
|
||||
"context": ".."
|
||||
},
|
||||
"appPort": "8124:8123",
|
||||
"postCreateCommand": "./.devcontainer/post_create.sh",
|
||||
"postCreateCommand": "sudo apt update && sudo apt upgrade -y && sudo apt install -y libpcap-dev",
|
||||
"postStartCommand": "script/bootstrap",
|
||||
"containerEnv": {
|
||||
"DEV_CONTAINER": "1",
|
||||
|
@@ -1,22 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script will run after the container is created
|
||||
|
||||
# add github cli
|
||||
(type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) \
|
||||
&& sudo mkdir -p -m 755 /etc/apt/keyrings \
|
||||
&& out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
||||
&& cat $out | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
|
||||
&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
|
||||
|
||||
# Update package lists
|
||||
sudo apt-get update
|
||||
|
||||
sudo apt upgrade -y
|
||||
|
||||
# Install necessary packages
|
||||
sudo apt-get install -y libpcap-dev gh
|
||||
|
||||
# Display a message
|
||||
echo "Post-create script has been executed successfully."
|
42
.vscode/tasks.json
vendored
42
.vscode/tasks.json
vendored
@@ -1,42 +1,6 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Develop and serve Frontend",
|
||||
"type": "shell",
|
||||
"command": "script/develop_and_serve -c ${input:coreUrl}",
|
||||
// Sync changes here to other tasks until issue resolved
|
||||
// https://github.com/Microsoft/vscode/issues/61497
|
||||
"problemMatcher": {
|
||||
"owner": "ha-build",
|
||||
"source": "ha-build",
|
||||
"fileLocation": "absolute",
|
||||
"severity": "error",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "(SyntaxError): (.+): (.+) \\((\\d+):(\\d+)\\)",
|
||||
"severity": 1,
|
||||
"file": 2,
|
||||
"message": 3,
|
||||
"line": 4,
|
||||
"column": 5
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": "Changes detected. Starting compilation",
|
||||
"endsPattern": "Build done @"
|
||||
}
|
||||
},
|
||||
"isBackground": true,
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"runOptions": {
|
||||
"instanceLimit": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Develop Frontend",
|
||||
"type": "gulp",
|
||||
@@ -277,12 +241,6 @@
|
||||
"id": "supervisorToken",
|
||||
"type": "promptString",
|
||||
"description": "The token for the Remote API proxy add-on"
|
||||
},
|
||||
{
|
||||
"id": "coreUrl",
|
||||
"type": "promptString",
|
||||
"description": "The URL of the Home Assistant Core instance",
|
||||
"default": "http://127.0.0.1:8123"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@@ -1,16 +1,16 @@
|
||||
// @ts-check
|
||||
|
||||
import tseslint from "typescript-eslint";
|
||||
import rootConfig from "../eslint.config.mjs";
|
||||
|
||||
export default tseslint.config(...rootConfig, {
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
"import/no-extraneous-dependencies": "off",
|
||||
"import/extensions": "off",
|
||||
"import/no-dynamic-require": "off",
|
||||
"global-require": "off",
|
||||
"@typescript-eslint/no-require-imports": "off",
|
||||
"prefer-arrow-callback": "off",
|
||||
export default [
|
||||
...rootConfig,
|
||||
{
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
"import/no-extraneous-dependencies": "off",
|
||||
"import/extensions": "off",
|
||||
"import/no-dynamic-require": "off",
|
||||
"global-require": "off",
|
||||
"@typescript-eslint/no-require-imports": "off",
|
||||
"prefer-arrow-callback": "off",
|
||||
},
|
||||
},
|
||||
});
|
||||
];
|
||||
|
@@ -90,14 +90,6 @@ function copyMapPanel(staticDir) {
|
||||
npmPath("leaflet/dist/leaflet.css"),
|
||||
staticPath("images/leaflet/")
|
||||
);
|
||||
copyFileDir(
|
||||
npmPath("leaflet.markercluster/dist/MarkerCluster.css"),
|
||||
staticPath("images/leaflet/")
|
||||
);
|
||||
copyFileDir(
|
||||
npmPath("leaflet.markercluster/dist/MarkerCluster.Default.css"),
|
||||
staticPath("images/leaflet/")
|
||||
);
|
||||
fs.copySync(
|
||||
npmPath("leaflet/dist/images"),
|
||||
staticPath("images/leaflet/images/")
|
||||
|
@@ -65,7 +65,6 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
mockEntityRegistry(hass, [
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
config_subentry_id: null,
|
||||
device_id: "co2signal",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
@@ -86,7 +85,6 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
},
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
config_subentry_id: null,
|
||||
device_id: "co2signal",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
|
@@ -11,7 +11,6 @@ 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,
|
||||
|
@@ -1,16 +1,11 @@
|
||||
// @ts-check
|
||||
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import unusedImports from "eslint-plugin-unused-imports";
|
||||
import globals from "globals";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import js from "@eslint/js";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import tseslint from "typescript-eslint";
|
||||
import eslintConfigPrettier from "eslint-config-prettier";
|
||||
import { configs as litConfigs } from "eslint-plugin-lit";
|
||||
import { configs as wcConfigs } from "eslint-plugin-wc";
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = path.dirname(_filename);
|
||||
@@ -20,14 +15,17 @@ const compat = new FlatCompat({
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
export default tseslint.config(
|
||||
...compat.extends("airbnb-base", "plugin:lit-a11y/recommended"),
|
||||
eslintConfigPrettier,
|
||||
litConfigs["flat/all"],
|
||||
tseslint.configs.recommended,
|
||||
tseslint.configs.strict,
|
||||
tseslint.configs.stylistic,
|
||||
wcConfigs["flat/recommended"],
|
||||
export default [
|
||||
...compat.extends(
|
||||
"airbnb-base",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/strict",
|
||||
"plugin:@typescript-eslint/stylistic",
|
||||
"plugin:wc/recommended",
|
||||
"plugin:lit/all",
|
||||
"plugin:lit-a11y/recommended",
|
||||
"prettier"
|
||||
),
|
||||
{
|
||||
plugins: {
|
||||
"unused-imports": unusedImports,
|
||||
@@ -45,7 +43,7 @@ export default tseslint.config(
|
||||
Polymer: true,
|
||||
},
|
||||
|
||||
parser: tseslint.parser,
|
||||
parser: tsParser,
|
||||
ecmaVersion: 2020,
|
||||
sourceType: "module",
|
||||
|
||||
@@ -186,5 +184,5 @@ export default tseslint.config(
|
||||
],
|
||||
"no-use-before-define": "off",
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
];
|
||||
|
@@ -1,10 +1,10 @@
|
||||
// @ts-check
|
||||
|
||||
import tseslint from "typescript-eslint";
|
||||
import rootConfig from "../eslint.config.mjs";
|
||||
|
||||
export default tseslint.config(...rootConfig, {
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
export default [
|
||||
...rootConfig,
|
||||
{
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
},
|
||||
},
|
||||
});
|
||||
];
|
||||
|
@@ -48,7 +48,6 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: "bedroom",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_1"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
@@ -72,7 +71,6 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: "backyard",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_2"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
@@ -96,7 +94,6 @@ 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,7 +47,6 @@ 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,7 +70,6 @@ const DEVICES: DeviceRegistryEntry[] = [
|
||||
area_id: "backyard",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_2"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
@@ -95,7 +93,6 @@ 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,8 +32,6 @@ 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,
|
||||
@@ -190,7 +188,6 @@ const createEntityRegistryEntries = (
|
||||
): EntityRegistryEntry[] => [
|
||||
{
|
||||
config_entry_id: item.entry_id,
|
||||
config_subentry_id: null,
|
||||
device_id: "mock-device-id",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
@@ -217,7 +214,6 @@ 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 {
|
||||
"hassio-backup-uploaded": { backup: HassioBackup };
|
||||
"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, "hassio-backup-uploaded", { backup: backup.data });
|
||||
fireEvent(this, "backup-uploaded", { backup: backup.data });
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: "Upload failed",
|
||||
|
@@ -5,6 +5,7 @@ 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";
|
||||
@@ -18,10 +19,13 @@ 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 } from "../../../src/types";
|
||||
import type { HomeAssistant, TranslationDict } 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;
|
||||
@@ -63,6 +67,8 @@ 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;
|
||||
@@ -109,6 +115,10 @@ 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;
|
||||
@@ -122,8 +132,8 @@ export class SupervisorBackupContent extends LitElement {
|
||||
${this.backup
|
||||
? html`<div class="details">
|
||||
${this.backup.type === "full"
|
||||
? this.supervisor?.localize("backup.full_backup")
|
||||
: this.supervisor?.localize("backup.partial_backup")}
|
||||
? this._localize("full_backup")
|
||||
: this._localize("partial_backup")}
|
||||
(${Math.ceil(this.backup.size * 10) / 10 + " MB"})<br />
|
||||
${this.hass
|
||||
? formatDateTime(
|
||||
@@ -135,7 +145,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
</div>`
|
||||
: html`<ha-textfield
|
||||
name="backupName"
|
||||
.label=${this.supervisor?.localize("backup.name")}
|
||||
.label=${this._localize("name")}
|
||||
.value=${this.backupName}
|
||||
@change=${this._handleTextValueChanged}
|
||||
>
|
||||
@@ -143,13 +153,11 @@ export class SupervisorBackupContent extends LitElement {
|
||||
${!this.backup || this.backup.type === "full"
|
||||
? html`<div class="sub-header">
|
||||
${!this.backup
|
||||
? this.supervisor?.localize("backup.type")
|
||||
: this.supervisor?.localize("backup.select_type")}
|
||||
? this._localize("type")
|
||||
: this._localize("select_type")}
|
||||
</div>
|
||||
<div class="backup-types">
|
||||
<ha-formfield
|
||||
.label=${this.supervisor?.localize("backup.full_backup")}
|
||||
>
|
||||
<ha-formfield .label=${this._localize("full_backup")}>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="full"
|
||||
@@ -158,9 +166,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.supervisor?.localize("backup.partial_backup")}
|
||||
>
|
||||
<ha-formfield .label=${this._localize("partial_backup")}>
|
||||
<ha-radio
|
||||
@change=${this._handleRadioValueChanged}
|
||||
value="partial"
|
||||
@@ -196,7 +202,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
.label=${this.supervisor?.localize("backup.folders")}
|
||||
.label=${this._localize("folders")}
|
||||
.iconPath=${mdiFolder}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
@@ -216,7 +222,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
.label=${this.supervisor?.localize("backup.addons")}
|
||||
.label=${this._localize("addons")}
|
||||
.iconPath=${mdiPuzzle}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
@@ -241,7 +247,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
${!this.backup
|
||||
? html`<ha-formfield
|
||||
class="password"
|
||||
.label=${this.supervisor?.localize("backup.password_protection")}
|
||||
.label=${this._localize("password_protection")}
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this.backupHasPassword}
|
||||
@@ -253,7 +259,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
${this.backupHasPassword
|
||||
? html`
|
||||
<ha-password-field
|
||||
.label=${this.supervisor?.localize("backup.password")}
|
||||
.label=${this._localize("password")}
|
||||
name="backupPassword"
|
||||
.value=${this.backupPassword}
|
||||
@change=${this._handleTextValueChanged}
|
||||
@@ -261,7 +267,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
</ha-password-field>
|
||||
${!this.backup
|
||||
? html`<ha-password-field
|
||||
.label=${this.supervisor?.localize("backup.confirm_password")}
|
||||
.label=${this._localize("confirm_password")}
|
||||
name="confirmBackupPassword"
|
||||
.value=${this.confirmBackupPassword}
|
||||
@change=${this._handleTextValueChanged}
|
||||
|
@@ -72,7 +72,7 @@ export class DialogHassioBackupUpload
|
||||
</ha-header-bar>
|
||||
</div>
|
||||
<hassio-upload-backup
|
||||
@hassio-backup-uploaded=${this._backupUploaded}
|
||||
@backup-uploaded=${this._backupUploaded}
|
||||
.hass=${this.hass}
|
||||
></hassio-upload-backup>
|
||||
</ha-dialog>
|
||||
|
@@ -35,6 +35,7 @@ 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")
|
||||
@@ -42,7 +43,7 @@ class HassioBackupDialog
|
||||
extends LitElement
|
||||
implements HassDialog<HassioBackupDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@@ -61,13 +62,9 @@ class HassioBackupDialog
|
||||
this._dialogParams = dialogParams;
|
||||
this._backup = await fetchHassioBackupInfo(this.hass, dialogParams.slug);
|
||||
if (!this._backup) {
|
||||
this._error = this._dialogParams.supervisor?.localize(
|
||||
"backup.no_backup_found"
|
||||
);
|
||||
this._error = this._localize("no_backup_found");
|
||||
} else if (this._dialogParams.onboarding && !this._backup.homeassistant) {
|
||||
this._error = this._dialogParams.supervisor?.localize(
|
||||
"backup.restore_no_home_assistant"
|
||||
);
|
||||
this._error = this._localize("restore_no_home_assistant");
|
||||
}
|
||||
this._restoringBackup = false;
|
||||
}
|
||||
@@ -85,6 +82,13 @@ 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;
|
||||
@@ -98,7 +102,7 @@ class HassioBackupDialog
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this._dialogParams.supervisor?.localize("backup.close")}
|
||||
.label=${this._localize("close")}
|
||||
.path=${mdiClose}
|
||||
@click=${this.closeDialog}
|
||||
.disabled=${this._restoringBackup}
|
||||
@@ -146,6 +150,7 @@ class HassioBackupDialog
|
||||
.supervisor=${this._dialogParams.supervisor}
|
||||
.backup=${this._backup}
|
||||
.onboarding=${this._dialogParams.onboarding || false}
|
||||
.localize=${this._dialogParams.localize}
|
||||
dialogInitialFocus
|
||||
>
|
||||
</supervisor-backup-content>
|
||||
@@ -156,7 +161,7 @@ class HassioBackupDialog
|
||||
.disabled=${this._restoringBackup || !!this._error}
|
||||
@click=${this._restoreClicked}
|
||||
>
|
||||
${this._dialogParams.supervisor?.localize("backup.restore")}
|
||||
${this._localize("restore")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
@@ -191,22 +196,18 @@ class HassioBackupDialog
|
||||
}
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: supervisor?.localize(
|
||||
`backup.${
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_title"
|
||||
: "confirm_restore_partial_backup_title"
|
||||
}`
|
||||
title: this._localize(
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_title"
|
||||
: "confirm_restore_partial_backup_title"
|
||||
),
|
||||
text: supervisor?.localize(
|
||||
`backup.${
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_text"
|
||||
: "confirm_restore_partial_backup_text"
|
||||
}`
|
||||
text: this._localize(
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_text"
|
||||
: "confirm_restore_partial_backup_text"
|
||||
),
|
||||
confirmText: supervisor?.localize("backup.restore"),
|
||||
dismissText: supervisor?.localize("backup.cancel"),
|
||||
confirmText: this._localize("restore"),
|
||||
dismissText: this._localize("cancel"),
|
||||
}))
|
||||
) {
|
||||
this._restoringBackup = false;
|
||||
@@ -226,8 +227,7 @@ class HassioBackupDialog
|
||||
this.closeDialog();
|
||||
} catch (error: any) {
|
||||
this._error =
|
||||
error?.body?.message ||
|
||||
supervisor?.localize("backup.restore_start_failed");
|
||||
error?.body?.message || this._localize("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: supervisor?.localize("backup.cancel"),
|
||||
dismissText: this._localize("cancel"),
|
||||
});
|
||||
if (!confirm) {
|
||||
return;
|
||||
@@ -302,7 +302,7 @@ class HassioBackupDialog
|
||||
private get _computeName() {
|
||||
return this._backup
|
||||
? this._backup.name || this._backup.slug
|
||||
: this._dialogParams!.supervisor?.localize("backup.unnamed_backup") || "";
|
||||
: this._localize("unnamed_backup");
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -1,4 +1,5 @@
|
||||
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 {
|
||||
@@ -7,6 +8,7 @@ export interface HassioBackupDialogParams {
|
||||
onRestoring?: () => void;
|
||||
onboarding?: boolean;
|
||||
supervisor?: Supervisor;
|
||||
localize?: LocalizeFunc;
|
||||
}
|
||||
|
||||
export const showHassioBackupDialog = (
|
||||
|
4
hassio/src/util/translations.ts
Normal file
4
hassio/src/util/translations.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import type { TranslationDict } from "../../../src/types";
|
||||
|
||||
export type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] &
|
||||
keyof TranslationDict["ui"]["panel"]["page-onboarding"]["restore"];
|
71
package.json
71
package.json
@@ -26,25 +26,25 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.26.9",
|
||||
"@babel/runtime": "7.26.7",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@codemirror/autocomplete": "6.18.6",
|
||||
"@codemirror/autocomplete": "6.18.4",
|
||||
"@codemirror/commands": "6.8.0",
|
||||
"@codemirror/language": "6.10.8",
|
||||
"@codemirror/legacy-modes": "6.4.3",
|
||||
"@codemirror/search": "6.5.9",
|
||||
"@codemirror/legacy-modes": "6.4.2",
|
||||
"@codemirror/search": "6.5.8",
|
||||
"@codemirror/state": "6.5.2",
|
||||
"@codemirror/view": "6.36.2",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.17.3",
|
||||
"@formatjs/intl-displaynames": "6.8.10",
|
||||
"@formatjs/intl-durationformat": "0.7.3",
|
||||
"@formatjs/intl-datetimeformat": "6.17.2",
|
||||
"@formatjs/intl-displaynames": "6.8.9",
|
||||
"@formatjs/intl-durationformat": "0.7.2",
|
||||
"@formatjs/intl-getcanonicallocales": "2.5.4",
|
||||
"@formatjs/intl-listformat": "7.7.10",
|
||||
"@formatjs/intl-locale": "4.2.10",
|
||||
"@formatjs/intl-numberformat": "8.15.3",
|
||||
"@formatjs/intl-pluralrules": "5.4.3",
|
||||
"@formatjs/intl-relativetimeformat": "11.4.10",
|
||||
"@formatjs/intl-listformat": "7.7.9",
|
||||
"@formatjs/intl-locale": "4.2.9",
|
||||
"@formatjs/intl-numberformat": "8.15.2",
|
||||
"@formatjs/intl-pluralrules": "5.4.2",
|
||||
"@formatjs/intl-relativetimeformat": "11.4.9",
|
||||
"@fullcalendar/core": "6.1.15",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"@fullcalendar/interaction": "6.1.15",
|
||||
@@ -53,9 +53,9 @@
|
||||
"@fullcalendar/timegrid": "6.1.15",
|
||||
"@lezer/highlight": "1.2.1",
|
||||
"@lit-labs/context": "0.4.1",
|
||||
"@lit-labs/motion": "1.0.8",
|
||||
"@lit-labs/observers": "2.0.5",
|
||||
"@lit-labs/virtualizer": "2.1.0",
|
||||
"@lit-labs/motion": "1.0.7",
|
||||
"@lit-labs/observers": "2.0.4",
|
||||
"@lit-labs/virtualizer": "2.0.15",
|
||||
"@lrnwebcomponents/simple-tooltip": "8.0.2",
|
||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||
@@ -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.5",
|
||||
"@vaadin/vaadin-themable-mixin": "24.6.5",
|
||||
"@vaadin/combo-box": "24.6.3",
|
||||
"@vaadin/vaadin-themable-mixin": "24.6.3",
|
||||
"@vibrant/color": "4.0.0",
|
||||
"@vue/web-component-wrapper": "1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
||||
@@ -116,18 +116,16 @@
|
||||
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
||||
"home-assistant-js-websocket": "9.4.0",
|
||||
"idb-keyval": "6.2.1",
|
||||
"intl-messageformat": "10.7.15",
|
||||
"intl-messageformat": "10.7.14",
|
||||
"js-yaml": "4.1.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
"lit": "2.8.0",
|
||||
"lit-html": "2.8.0",
|
||||
"luxon": "3.5.0",
|
||||
"marked": "15.0.7",
|
||||
"marked": "15.0.6",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.3",
|
||||
"object-hash": "3.0.0",
|
||||
"punycode": "2.3.1",
|
||||
"qr-scanner": "1.4.2",
|
||||
"qrcode": "1.5.4",
|
||||
@@ -139,7 +137,7 @@
|
||||
"tinykeys": "3.0.0",
|
||||
"tsparticles-engine": "2.12.0",
|
||||
"tsparticles-preset-links": "2.12.0",
|
||||
"ua-parser-js": "2.0.2",
|
||||
"ua-parser-js": "2.0.1",
|
||||
"vis-data": "7.1.9",
|
||||
"vis-network": "9.1.9",
|
||||
"vue": "2.7.16",
|
||||
@@ -154,20 +152,20 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.26.9",
|
||||
"@babel/core": "7.26.7",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.3",
|
||||
"@babel/plugin-proposal-decorators": "7.25.9",
|
||||
"@babel/plugin-transform-runtime": "7.26.9",
|
||||
"@babel/preset-env": "7.26.9",
|
||||
"@babel/plugin-transform-runtime": "7.25.9",
|
||||
"@babel/preset-env": "7.26.7",
|
||||
"@babel/preset-typescript": "7.26.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.18.2",
|
||||
"@lokalise/node-api": "13.1.0",
|
||||
"@octokit/auth-oauth-device": "7.1.3",
|
||||
"@octokit/plugin-retry": "7.1.4",
|
||||
"@octokit/rest": "21.1.1",
|
||||
"@lokalise/node-api": "13.0.0",
|
||||
"@octokit/auth-oauth-device": "7.1.2",
|
||||
"@octokit/plugin-retry": "7.1.3",
|
||||
"@octokit/rest": "21.1.0",
|
||||
"@rsdoctor/rspack-plugin": "0.4.13",
|
||||
"@rspack/cli": "1.2.3",
|
||||
"@rspack/core": "1.2.3",
|
||||
"@rspack/cli": "1.2.2",
|
||||
"@rspack/core": "1.2.2",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.21",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
@@ -177,7 +175,6 @@
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/leaflet": "1.9.16",
|
||||
"@types/leaflet-draw": "1.0.11",
|
||||
"@types/leaflet.markercluster": "1.5.5",
|
||||
"@types/lodash.merge": "4.6.9",
|
||||
"@types/luxon": "3.4.2",
|
||||
"@types/mocha": "10.0.10",
|
||||
@@ -186,12 +183,14 @@
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "8.23.0",
|
||||
"@typescript-eslint/parser": "8.23.0",
|
||||
"@vitest/coverage-v8": "3.0.5",
|
||||
"babel-loader": "9.2.1",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.0",
|
||||
"eslint": "9.20.1",
|
||||
"eslint": "9.19.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.0.1",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
@@ -216,8 +215,9 @@
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.5.0",
|
||||
"map-stream": "0.0.7",
|
||||
"object-hash": "3.0.0",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.5.1",
|
||||
"prettier": "3.4.2",
|
||||
"rspack-manifest-plugin": "5.0.3",
|
||||
"serve": "14.2.4",
|
||||
"sinon": "19.0.2",
|
||||
@@ -225,7 +225,6 @@
|
||||
"terser-webpack-plugin": "5.3.11",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.7.3",
|
||||
"typescript-eslint": "8.24.1",
|
||||
"vitest": "3.0.5",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
@@ -240,7 +239,7 @@
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "1.6.3",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"globals": "15.15.0",
|
||||
"globals": "15.14.0",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"packageManager": "yarn@4.6.0"
|
||||
|
@@ -136,18 +136,11 @@ export function theme2hex(themeColor: string): string {
|
||||
}
|
||||
|
||||
const rgbFromColorName = colors[themeColor];
|
||||
if (rgbFromColorName) {
|
||||
return rgb2hex(rgbFromColorName);
|
||||
if (!rgbFromColorName) {
|
||||
// We have a named color, and there's nothing in the table,
|
||||
// so nothing further we can do with it.
|
||||
// Compare/border/background color will all be the same.
|
||||
return themeColor;
|
||||
}
|
||||
|
||||
const rgbMatch = themeColor.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
||||
if (rgbMatch) {
|
||||
const [, r, g, b] = rgbMatch.map(Number);
|
||||
return rgb2hex([r, g, b]);
|
||||
}
|
||||
|
||||
// We have a named color, and there's nothing in the table,
|
||||
// so nothing further we can do with it.
|
||||
// Compare/border/background color will all be the same.
|
||||
return themeColor;
|
||||
return rgb2hex(rgbFromColorName);
|
||||
}
|
||||
|
@@ -99,6 +99,7 @@ export const DOMAINS_TOGGLE = new Set([
|
||||
"switch",
|
||||
"group",
|
||||
"automation",
|
||||
"climate",
|
||||
"humidifier",
|
||||
"valve",
|
||||
]);
|
||||
|
@@ -26,20 +26,6 @@ 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,
|
||||
|
@@ -16,30 +16,11 @@ export const setupLeafletMap = async (
|
||||
const Leaflet = (await import("leaflet")).default as LeafletModuleType;
|
||||
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
|
||||
|
||||
await import("leaflet.markercluster");
|
||||
|
||||
const map = Leaflet.map(mapElement);
|
||||
const style = document.createElement("link");
|
||||
style.setAttribute("href", "/static/images/leaflet/leaflet.css");
|
||||
style.setAttribute("rel", "stylesheet");
|
||||
mapElement.parentNode.appendChild(style);
|
||||
|
||||
const markerClusterStyle = document.createElement("link");
|
||||
markerClusterStyle.setAttribute(
|
||||
"href",
|
||||
"/static/images/leaflet/MarkerCluster.css"
|
||||
);
|
||||
markerClusterStyle.setAttribute("rel", "stylesheet");
|
||||
mapElement.parentNode.appendChild(markerClusterStyle);
|
||||
|
||||
const defaultMarkerClusterStyle = document.createElement("link");
|
||||
defaultMarkerClusterStyle.setAttribute(
|
||||
"href",
|
||||
"/static/images/leaflet/MarkerCluster.Default.css"
|
||||
);
|
||||
defaultMarkerClusterStyle.setAttribute("rel", "stylesheet");
|
||||
mapElement.parentNode.appendChild(defaultMarkerClusterStyle);
|
||||
|
||||
map.setView([52.3731339, 4.8903147], 13);
|
||||
|
||||
const tileLayer = createTileLayer(Leaflet).addTo(map);
|
||||
|
@@ -1,9 +1,6 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
import { UNAVAILABLE_STATES } from "../../data/entity";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { stringCompare } from "../string/compare";
|
||||
|
||||
export const FIXED_DOMAIN_STATES = {
|
||||
alarm_control_panel: [
|
||||
@@ -240,7 +237,6 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
|
||||
};
|
||||
|
||||
export const getStates = (
|
||||
hass: HomeAssistant,
|
||||
state: HassEntity,
|
||||
attribute: string | undefined = undefined
|
||||
): string[] => {
|
||||
@@ -273,19 +269,7 @@ export const getStates = (
|
||||
case "device_tracker":
|
||||
case "person":
|
||||
if (!attribute) {
|
||||
result.push(
|
||||
...Object.entries(hass.states)
|
||||
.filter(
|
||||
([entityId, stateObj]) =>
|
||||
computeDomain(entityId) === "zone" &&
|
||||
entityId !== "zone.home" &&
|
||||
stateObj.attributes.friendly_name
|
||||
)
|
||||
.map(([_entityId, stateObj]) => stateObj.attributes.friendly_name!)
|
||||
.sort((zone1, zone2) =>
|
||||
stringCompare(zone1, zone2, hass.locale.language)
|
||||
)
|
||||
);
|
||||
result.push("home", "not_home");
|
||||
}
|
||||
break;
|
||||
case "event":
|
||||
|
@@ -1,32 +0,0 @@
|
||||
import type { LatLngExpression, Layer, Map, MarkerOptions } from "leaflet";
|
||||
import { Marker } from "leaflet";
|
||||
|
||||
export class DecoratedMarker extends Marker {
|
||||
decorationLayer: Layer | undefined;
|
||||
|
||||
constructor(
|
||||
latlng: LatLngExpression,
|
||||
decorationLayer?: Layer,
|
||||
options?: MarkerOptions
|
||||
) {
|
||||
super(latlng, options);
|
||||
|
||||
this.decorationLayer = decorationLayer;
|
||||
}
|
||||
|
||||
onAdd(map: Map) {
|
||||
super.onAdd(map);
|
||||
|
||||
// If decoration has been provided, add it to the map as well
|
||||
this.decorationLayer?.addTo(map);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
onRemove(map: Map) {
|
||||
// If decoration has been provided, remove it from the map as well
|
||||
this.decorationLayer?.remove();
|
||||
|
||||
return super.onRemove(map);
|
||||
}
|
||||
}
|
@@ -41,7 +41,7 @@ export class HaProgressButton extends LitElement {
|
||||
indeterminate
|
||||
></ha-circular-progress>
|
||||
`
|
||||
: nothing}
|
||||
: ""}
|
||||
</div>
|
||||
`}
|
||||
`;
|
||||
@@ -117,9 +117,6 @@ export class HaProgressButton extends LitElement {
|
||||
mwc-button.error slot {
|
||||
visibility: hidden;
|
||||
}
|
||||
:host([destructive]) {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -24,7 +24,6 @@ import type { HomeAssistant } from "../../types";
|
||||
import { isMac } from "../../util/is_mac";
|
||||
import "../ha-icon-button";
|
||||
import { formatTimeLabel } from "./axis-label";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
|
||||
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
||||
|
||||
@@ -68,16 +67,12 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
private _listeners: (() => void)[] = [];
|
||||
|
||||
private _originalZrFlush?: () => void;
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
while (this._listeners.length) {
|
||||
this._listeners.pop()!();
|
||||
}
|
||||
this.chart?.dispose();
|
||||
this.chart = undefined;
|
||||
this._originalZrFlush = undefined;
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
@@ -88,19 +83,19 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
this._listeners.push(
|
||||
listenMediaQuery("(prefers-reduced-motion)", (matches) => {
|
||||
if (this._reducedMotion !== matches) {
|
||||
this._reducedMotion = matches;
|
||||
this._setChartOptions({ animation: !this._reducedMotion });
|
||||
}
|
||||
this._reducedMotion = matches;
|
||||
this.chart?.setOption({ animation: !this._reducedMotion });
|
||||
})
|
||||
);
|
||||
|
||||
// Add keyboard event listeners
|
||||
const handleKeyDown = (ev: KeyboardEvent) => {
|
||||
if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
|
||||
if ((isMac && ev.metaKey) || (!isMac && ev.ctrlKey)) {
|
||||
this._modifierPressed = true;
|
||||
if (!this.options?.dataZoom) {
|
||||
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
|
||||
this.chart?.setOption({
|
||||
dataZoom: this._getDataZoomConfig(),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -109,7 +104,9 @@ export class HaChartBase extends LitElement {
|
||||
if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
|
||||
this._modifierPressed = false;
|
||||
if (!this.options?.dataZoom) {
|
||||
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
|
||||
this.chart?.setOption({
|
||||
dataZoom: this._getDataZoomConfig(),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -127,24 +124,27 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues): void {
|
||||
if (!this.chart) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this.hasUpdated || !this.chart) {
|
||||
return;
|
||||
}
|
||||
if (changedProps.has("_themes")) {
|
||||
this._setupChart();
|
||||
return;
|
||||
}
|
||||
let chartOptions: ECOption = {};
|
||||
if (changedProps.has("data")) {
|
||||
chartOptions.series = this.data;
|
||||
this.chart.setOption(
|
||||
{ series: this.data },
|
||||
{ lazyUpdate: true, replaceMerge: ["series"] }
|
||||
);
|
||||
}
|
||||
if (changedProps.has("options")) {
|
||||
chartOptions = { ...chartOptions, ...this._createOptions() };
|
||||
} else if (this._isTouchDevice && changedProps.has("_isZoomed")) {
|
||||
chartOptions.dataZoom = this._getDataZoomConfig();
|
||||
}
|
||||
if (Object.keys(chartOptions).length > 0) {
|
||||
this._setChartOptions(chartOptions);
|
||||
if (changedProps.has("options") || changedProps.has("_isZoomed")) {
|
||||
this.chart.setOption(this._createOptions(), {
|
||||
lazyUpdate: true,
|
||||
// if we replace the whole object, it will reset the dataZoom
|
||||
replaceMerge: ["grid"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +158,7 @@ export class HaChartBase extends LitElement {
|
||||
style=${styleMap({
|
||||
height: this.height ?? `${this._getDefaultHeight()}px`,
|
||||
})}
|
||||
@wheel=${this._handleWheel}
|
||||
>
|
||||
<div class="chart"></div>
|
||||
${this._isZoomed
|
||||
@@ -239,8 +240,8 @@ export class HaChartBase extends LitElement {
|
||||
type: "inside",
|
||||
orient: "horizontal",
|
||||
filterMode: "none",
|
||||
moveOnMouseMove: !this._isTouchDevice || this._isZoomed,
|
||||
preventDefaultMouseMove: !this._isTouchDevice || this._isZoomed,
|
||||
moveOnMouseMove: this._isZoomed,
|
||||
preventDefaultMouseMove: this._isZoomed,
|
||||
zoomLock: !this._isTouchDevice && !this._modifierPressed,
|
||||
};
|
||||
}
|
||||
@@ -511,33 +512,25 @@ export class HaChartBase extends LitElement {
|
||||
return Math.max(this.clientWidth / 2, 200);
|
||||
}
|
||||
|
||||
private _setChartOptions(options: ECOption) {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
if (!this._originalZrFlush) {
|
||||
const dataSize = ensureArray(this.data).reduce(
|
||||
(acc, series) => acc + (series.data as any[]).length,
|
||||
0
|
||||
);
|
||||
if (dataSize > 10000) {
|
||||
// for large datasets zr.flush takes 30-40% of the render time
|
||||
// so we delay it a bit to avoid blocking the main thread
|
||||
const zr = this.chart.getZr();
|
||||
this._originalZrFlush = zr.flush.bind(zr);
|
||||
zr.flush = () => {
|
||||
setTimeout(() => {
|
||||
this._originalZrFlush?.();
|
||||
}, 10);
|
||||
};
|
||||
}
|
||||
}
|
||||
const replaceMerge = options.series ? ["series"] : [];
|
||||
this.chart.setOption(options, { replaceMerge });
|
||||
}
|
||||
|
||||
private _handleZoomReset() {
|
||||
this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 });
|
||||
this._modifierPressed = false;
|
||||
}
|
||||
|
||||
private _handleWheel(e: WheelEvent) {
|
||||
// if the window is not focused, we don't receive the keydown events but scroll still works
|
||||
if (!this.options?.dataZoom) {
|
||||
const modifierPressed = (isMac && e.metaKey) || (!isMac && e.ctrlKey);
|
||||
if (modifierPressed) {
|
||||
e.preventDefault();
|
||||
}
|
||||
if (modifierPressed !== this._modifierPressed) {
|
||||
this._modifierPressed = modifierPressed;
|
||||
this.chart?.setOption({
|
||||
dataZoom: this._getDataZoomConfig(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
|
@@ -75,8 +75,6 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
@state() private _yWidth = 25;
|
||||
|
||||
@state() private _visualMap?: VisualMapComponentOption[];
|
||||
|
||||
private _chartTime: Date = new Date();
|
||||
|
||||
protected render() {
|
||||
@@ -94,7 +92,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTooltip = (params: any) => {
|
||||
private _renderTooltip(params: any) {
|
||||
const time = params[0].axisValue;
|
||||
const title =
|
||||
formatDateTimeWithSeconds(
|
||||
@@ -117,7 +115,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
return;
|
||||
}
|
||||
// If the datapoint is not found, we need to find the last datapoint before the current time
|
||||
let lastData: any;
|
||||
let lastData;
|
||||
const data = dataset.data || [];
|
||||
for (let i = data.length - 1; i >= 0; i--) {
|
||||
const point = data[i];
|
||||
@@ -177,7 +175,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
})
|
||||
.join("<br>")
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private _datasetHidden(ev: CustomEvent) {
|
||||
this._hiddenStats.add(ev.detail.name);
|
||||
@@ -210,8 +208,8 @@ export class StateHistoryChartLine extends LitElement {
|
||||
changedProps.has("minYAxis") ||
|
||||
changedProps.has("maxYAxis") ||
|
||||
changedProps.has("fitYData") ||
|
||||
changedProps.has("_chartData") ||
|
||||
changedProps.has("paddingYAxis") ||
|
||||
changedProps.has("_visualMap") ||
|
||||
changedProps.has("_yWidth")
|
||||
) {
|
||||
const rtl = computeRTL(this.hass);
|
||||
@@ -282,11 +280,37 @@ export class StateHistoryChartLine extends LitElement {
|
||||
right: rtl ? Math.max(this.paddingYAxis, this._yWidth) : 1,
|
||||
bottom: 30,
|
||||
},
|
||||
visualMap: this._visualMap,
|
||||
visualMap: this._chartData
|
||||
.map((_, seriesIndex) => {
|
||||
const dataIndex = this._datasetToDataIndex[seriesIndex];
|
||||
const data = this.data[dataIndex];
|
||||
if (!data.statistics || data.statistics.length === 0) {
|
||||
return false;
|
||||
}
|
||||
// render stat data with a slightly transparent line
|
||||
const firstStateTS =
|
||||
data.states[0]?.last_changed ?? this.endTime.getTime();
|
||||
return {
|
||||
show: false,
|
||||
seriesIndex,
|
||||
dimension: 0,
|
||||
pieces: [
|
||||
{
|
||||
max: firstStateTS - 0.01,
|
||||
colorAlpha: 0.5,
|
||||
},
|
||||
{
|
||||
min: firstStateTS,
|
||||
colorAlpha: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as VisualMapComponentOption[],
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
appendTo: document.body,
|
||||
formatter: this._renderTooltip,
|
||||
formatter: this._renderTooltip.bind(this),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -701,33 +725,6 @@ export class StateHistoryChartLine extends LitElement {
|
||||
this._chartData = datasets;
|
||||
this._entityIds = entityIds;
|
||||
this._datasetToDataIndex = datasetToDataIndex;
|
||||
const visualMap: VisualMapComponentOption[] = [];
|
||||
this._chartData.forEach((_, seriesIndex) => {
|
||||
const dataIndex = this._datasetToDataIndex[seriesIndex];
|
||||
const data = this.data[dataIndex];
|
||||
if (!data.statistics || data.statistics.length === 0) {
|
||||
return;
|
||||
}
|
||||
// render stat data with a slightly transparent line
|
||||
const firstStateTS =
|
||||
data.states[0]?.last_changed ?? this.endTime.getTime();
|
||||
visualMap.push({
|
||||
show: false,
|
||||
seriesIndex,
|
||||
dimension: 0,
|
||||
pieces: [
|
||||
{
|
||||
max: firstStateTS - 0.01,
|
||||
colorAlpha: 0.5,
|
||||
},
|
||||
{
|
||||
min: firstStateTS,
|
||||
colorAlpha: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
this._visualMap = visualMap.length > 0 ? visualMap : undefined;
|
||||
}
|
||||
|
||||
private _clampYAxis(value?: number | ((values: any) => number)) {
|
||||
|
@@ -273,13 +273,11 @@ export class StatisticsChart extends LitElement {
|
||||
this._chartOptions = {
|
||||
xAxis: [
|
||||
{
|
||||
id: "xAxis",
|
||||
type: "time",
|
||||
min: startTime,
|
||||
max: this.endTime,
|
||||
max: endTime,
|
||||
},
|
||||
{
|
||||
id: "hiddenAxis",
|
||||
type: "time",
|
||||
show: false,
|
||||
},
|
||||
@@ -370,6 +368,7 @@ export class StatisticsChart extends LitElement {
|
||||
if (endTime > new Date()) {
|
||||
endTime = new Date();
|
||||
}
|
||||
this.endTime = endTime;
|
||||
|
||||
let unit: string | undefined | null;
|
||||
|
||||
|
@@ -448,7 +448,6 @@ export class HaDataTable extends LitElement {
|
||||
)}
|
||||
@click=${this._handleHeaderClick}
|
||||
.columnId=${key}
|
||||
title=${ifDefined(column.title)}
|
||||
>
|
||||
${column.sortable
|
||||
? html`
|
||||
|
@@ -57,7 +57,7 @@ class HaEntityStatePicker extends LitElement {
|
||||
(this._comboBox as any).items = [
|
||||
...(this.extraOptions ?? []),
|
||||
...(this.entityId && stateObj
|
||||
? getStates(this.hass, stateObj, this.attribute).map((key) => ({
|
||||
? getStates(stateObj, this.attribute).map((key) => ({
|
||||
value: key,
|
||||
label: !this.attribute
|
||||
? this.hass.formatEntityState(stateObj, key)
|
||||
|
@@ -5,16 +5,15 @@ import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiCalendar } from "@mdi/js";
|
||||
import {
|
||||
addDays,
|
||||
subHours,
|
||||
endOfDay,
|
||||
endOfMonth,
|
||||
endOfWeek,
|
||||
endOfYear,
|
||||
isThisYear,
|
||||
startOfDay,
|
||||
startOfMonth,
|
||||
startOfWeek,
|
||||
startOfYear,
|
||||
isThisYear,
|
||||
} from "date-fns";
|
||||
import { fromZonedTime, toZonedTime } from "date-fns-tz";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
@@ -179,96 +178,6 @@ export class HaDateRangePicker extends LitElement {
|
||||
weekStartsOn,
|
||||
}),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.now-1h"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
1
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
0
|
||||
),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.now-12h"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
12
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
0
|
||||
),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.now-24h"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
24
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
0
|
||||
),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.now-7d"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
24 * 7
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
0
|
||||
),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.now-30d"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
24 * 30
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
0
|
||||
),
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
@@ -486,55 +395,44 @@ export class HaDateRangePicker extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-icon-button {
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.date-range-inputs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
ha-icon-button {
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.date-range-ranges {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
}
|
||||
.date-range-inputs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-range-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 8px;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
.date-range-ranges {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.date-range-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 8px;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
ha-textarea {
|
||||
display: inline-block;
|
||||
width: 340px;
|
||||
}
|
||||
@media only screen and (max-width: 460px) {
|
||||
ha-textarea {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
width: 340px;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 800px) {
|
||||
.date-range-ranges {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-height: 940px) and (max-width: 800px) {
|
||||
.date-range-ranges {
|
||||
overflow: auto;
|
||||
max-height: calc(70vh - 330px);
|
||||
min-height: 160px;
|
||||
@media only screen and (max-width: 460px) {
|
||||
ha-textarea {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
:host([header-position]) .date-range-ranges {
|
||||
max-height: calc(90vh - 430px);
|
||||
@media only screen and (max-width: 800px) {
|
||||
.date-range-ranges {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -11,7 +11,6 @@ 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 {
|
||||
@@ -24,8 +23,6 @@ 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;
|
||||
@@ -34,10 +31,6 @@ 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;
|
||||
@@ -80,22 +73,23 @@ 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.uploadingLabel || this.value
|
||||
? localize("ui.components.file-upload.uploading_name", {
|
||||
name: this._name,
|
||||
})
|
||||
: localize("ui.components.file-upload.uploading")}</span
|
||||
>${this.value
|
||||
? this.hass?.localize(
|
||||
"ui.components.file-upload.uploading_name",
|
||||
{ name: this._name }
|
||||
)
|
||||
: this.hass?.localize(
|
||||
"ui.components.file-upload.uploading"
|
||||
)}</span
|
||||
>
|
||||
${this.progress
|
||||
? html`<div class="progress">
|
||||
${this.progress}${this.hass &&
|
||||
blankBeforePercent(this.hass!.locale)}%
|
||||
${this.progress}${blankBeforePercent(this.hass!.locale)}%
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
@@ -122,11 +116,14 @@ export class HaFileUpload extends LitElement {
|
||||
.path=${this.icon || mdiFileUpload}
|
||||
></ha-svg-icon>
|
||||
<ha-button unelevated @click=${this._openFilePicker}>
|
||||
${this.label || localize("ui.components.file-upload.label")}
|
||||
${this.label ||
|
||||
this.hass?.localize("ui.components.file-upload.label")}
|
||||
</ha-button>
|
||||
<span class="secondary"
|
||||
>${this.secondary ||
|
||||
localize("ui.components.file-upload.secondary")}</span
|
||||
this.hass?.localize(
|
||||
"ui.components.file-upload.secondary"
|
||||
)}</span
|
||||
>
|
||||
<span class="supports">${this.supports}</span>`
|
||||
: typeof this.value === "string"
|
||||
@@ -139,7 +136,8 @@ export class HaFileUpload extends LitElement {
|
||||
</div>
|
||||
<ha-icon-button
|
||||
@click=${this._clearValue}
|
||||
.label=${this.deleteLabel || localize("ui.common.delete")}
|
||||
.label=${this.hass?.localize("ui.common.delete") ||
|
||||
"Delete"}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
@@ -157,8 +155,8 @@ export class HaFileUpload extends LitElement {
|
||||
</div>
|
||||
<ha-icon-button
|
||||
@click=${this._clearValue}
|
||||
.label=${this.deleteLabel ||
|
||||
localize("ui.common.delete")}
|
||||
.label=${this.hass?.localize("ui.common.delete") ||
|
||||
"Delete"}
|
||||
.path=${mdiDelete}
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
@@ -240,10 +238,6 @@ 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));
|
||||
|
@@ -1,12 +1,7 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import hash from "object-hash";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { renderMarkdown } from "../resources/render-markdown";
|
||||
import { CacheManager } from "../util/cache-manager";
|
||||
|
||||
const markdownCache = new CacheManager<string>(1000);
|
||||
|
||||
const _gitHubMarkdownAlerts = {
|
||||
reType:
|
||||
@@ -31,16 +26,6 @@ class HaMarkdownElement extends ReactiveElement {
|
||||
@property({ type: Boolean, attribute: "lazy-images" }) public lazyImages =
|
||||
false;
|
||||
|
||||
@property({ type: Boolean }) public cache = false;
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this.cache) {
|
||||
const key = this._computeCacheKey();
|
||||
markdownCache.set(key, this.innerHTML);
|
||||
}
|
||||
}
|
||||
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
@@ -52,24 +37,6 @@ class HaMarkdownElement extends ReactiveElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected willUpdate(_changedProperties: PropertyValues): void {
|
||||
if (!this.innerHTML && this.cache) {
|
||||
const key = this._computeCacheKey();
|
||||
if (markdownCache.has(key)) {
|
||||
this.innerHTML = markdownCache.get(key)!;
|
||||
this._resize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _computeCacheKey() {
|
||||
return hash({
|
||||
content: this.content,
|
||||
allowSvg: this.allowSvg,
|
||||
breaks: this.breaks,
|
||||
});
|
||||
}
|
||||
|
||||
private async _render() {
|
||||
this.innerHTML = await renderMarkdown(
|
||||
String(this.content),
|
||||
|
@@ -13,8 +13,6 @@ export class HaMarkdown extends LitElement {
|
||||
@property({ type: Boolean, attribute: "lazy-images" }) public lazyImages =
|
||||
false;
|
||||
|
||||
@property({ type: Boolean }) public cache = false;
|
||||
|
||||
protected render() {
|
||||
if (!this.content) {
|
||||
return nothing;
|
||||
@@ -25,7 +23,6 @@ export class HaMarkdown extends LitElement {
|
||||
.allowSvg=${this.allowSvg}
|
||||
.breaks=${this.breaks}
|
||||
.lazyImages=${this.lazyImages}
|
||||
.cache=${this.cache}
|
||||
></ha-markdown-element>`;
|
||||
}
|
||||
|
||||
|
@@ -8,10 +8,9 @@ import type {
|
||||
Map,
|
||||
Marker,
|
||||
Polyline,
|
||||
MarkerClusterGroup,
|
||||
} from "leaflet";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, ReactiveElement } from "lit";
|
||||
import { ReactiveElement, css } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { formatDateTime } from "../../common/datetime/format_date_time";
|
||||
@@ -27,7 +26,6 @@ import type { HomeAssistant, ThemeMode } from "../../types";
|
||||
import { isTouch } from "../../util/is_touch";
|
||||
import "../ha-icon-button";
|
||||
import "./ha-entity-marker";
|
||||
import { DecoratedMarker } from "../../common/map/decorated_marker";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
@@ -86,9 +84,6 @@ export class HaMap extends ReactiveElement {
|
||||
|
||||
@property({ type: Number }) public zoom = 14;
|
||||
|
||||
@property({ attribute: "cluster-markers", type: Boolean })
|
||||
public clusterMarkers = true;
|
||||
|
||||
@state() private _loaded = false;
|
||||
|
||||
public leafletMap?: Map;
|
||||
@@ -101,12 +96,10 @@ export class HaMap extends ReactiveElement {
|
||||
|
||||
private _mapFocusItems: (Marker | Circle)[] = [];
|
||||
|
||||
private _mapZones: DecoratedMarker[] = [];
|
||||
private _mapZones: (Marker | Circle)[] = [];
|
||||
|
||||
private _mapFocusZones: (Marker | Circle)[] = [];
|
||||
|
||||
private _mapCluster: MarkerClusterGroup | undefined;
|
||||
|
||||
private _mapPaths: (Polyline | CircleMarker)[] = [];
|
||||
|
||||
private _clickCount = 0;
|
||||
@@ -158,10 +151,6 @@ export class HaMap extends ReactiveElement {
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has("clusterMarkers")) {
|
||||
this._drawEntities();
|
||||
}
|
||||
|
||||
if (changedProps.has("_loaded") || changedProps.has("paths")) {
|
||||
this._drawPaths();
|
||||
}
|
||||
@@ -186,7 +175,6 @@ export class HaMap extends ReactiveElement {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateMapStyle();
|
||||
}
|
||||
|
||||
@@ -438,11 +426,6 @@ export class HaMap extends ReactiveElement {
|
||||
this._mapFocusZones = [];
|
||||
}
|
||||
|
||||
if (this._mapCluster) {
|
||||
this._mapCluster.remove();
|
||||
this._mapCluster = undefined;
|
||||
}
|
||||
|
||||
if (!this.entities) {
|
||||
return;
|
||||
}
|
||||
@@ -498,24 +481,26 @@ export class HaMap extends ReactiveElement {
|
||||
iconHTML = el.outerHTML;
|
||||
}
|
||||
|
||||
// create marker with the icon
|
||||
this._mapZones.push(
|
||||
Leaflet.marker([latitude, longitude], {
|
||||
icon: Leaflet.divIcon({
|
||||
html: iconHTML,
|
||||
iconSize: [24, 24],
|
||||
className,
|
||||
}),
|
||||
interactive: this.interactiveZones,
|
||||
title,
|
||||
})
|
||||
);
|
||||
|
||||
// create circle around it
|
||||
const circle = Leaflet.circle([latitude, longitude], {
|
||||
interactive: false,
|
||||
color: passive ? passiveZoneColor : zoneColor,
|
||||
radius,
|
||||
});
|
||||
|
||||
const marker = new DecoratedMarker([latitude, longitude], circle, {
|
||||
icon: Leaflet.divIcon({
|
||||
html: iconHTML,
|
||||
iconSize: [24, 24],
|
||||
className,
|
||||
}),
|
||||
interactive: this.interactiveZones,
|
||||
title,
|
||||
});
|
||||
|
||||
this._mapZones.push(marker);
|
||||
this._mapZones.push(circle);
|
||||
if (
|
||||
this.fitZones &&
|
||||
(typeof entity === "string" || entity.focus !== false)
|
||||
@@ -553,7 +538,7 @@ export class HaMap extends ReactiveElement {
|
||||
}
|
||||
|
||||
// create marker with the icon
|
||||
const marker = new DecoratedMarker([latitude, longitude], undefined, {
|
||||
const marker = Leaflet.marker([latitude, longitude], {
|
||||
icon: Leaflet.divIcon({
|
||||
html: entityMarker,
|
||||
iconSize: [48, 48],
|
||||
@@ -561,33 +546,24 @@ export class HaMap extends ReactiveElement {
|
||||
}),
|
||||
title: title,
|
||||
});
|
||||
this._mapItems.push(marker);
|
||||
if (typeof entity === "string" || entity.focus !== false) {
|
||||
this._mapFocusItems.push(marker);
|
||||
}
|
||||
|
||||
// create circle around if entity has accuracy
|
||||
if (gpsAccuracy) {
|
||||
marker.decorationLayer = Leaflet.circle([latitude, longitude], {
|
||||
interactive: false,
|
||||
color: darkPrimaryColor,
|
||||
radius: gpsAccuracy,
|
||||
});
|
||||
this._mapItems.push(
|
||||
Leaflet.circle([latitude, longitude], {
|
||||
interactive: false,
|
||||
color: darkPrimaryColor,
|
||||
radius: gpsAccuracy,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this._mapItems.push(marker);
|
||||
}
|
||||
|
||||
if (this.clusterMarkers) {
|
||||
this._mapCluster = Leaflet.markerClusterGroup({
|
||||
showCoverageOnHover: false,
|
||||
removeOutsideVisibleBounds: false,
|
||||
});
|
||||
this._mapCluster.addLayers(this._mapItems);
|
||||
map.addLayer(this._mapCluster);
|
||||
} else {
|
||||
this._mapItems.forEach((marker) => map.addLayer(marker));
|
||||
}
|
||||
|
||||
this._mapItems.forEach((marker) => map.addLayer(marker));
|
||||
this._mapZones.forEach((marker) => map.addLayer(marker));
|
||||
}
|
||||
|
||||
|
@@ -1,81 +1,25 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement } from "lit/decorators";
|
||||
import "../ha-icon";
|
||||
import "../ha-svg-icon";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
|
||||
export type TileIconImageStyle = "square" | "rounded-square" | "circle";
|
||||
|
||||
export const DEFAULT_TILE_ICON_BORDER_STYLE = "circle";
|
||||
|
||||
@customElement("ha-tile-icon")
|
||||
export class HaTileIcon extends LitElement {
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public interactive = false;
|
||||
|
||||
@property({ attribute: "border-style", type: String })
|
||||
public imageStyle?: TileIconImageStyle;
|
||||
|
||||
@property({ attribute: false })
|
||||
public imageUrl?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this.imageUrl) {
|
||||
const imageStyle = this.imageStyle || DEFAULT_TILE_ICON_BORDER_STYLE;
|
||||
return html`
|
||||
<div class="container ${classMap({ [imageStyle]: this.imageUrl })}">
|
||||
<img alt="" src=${this.imageUrl} />
|
||||
</div>
|
||||
<slot></slot>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="container ${this.interactive ? "background" : ""}">
|
||||
<slot name="icon"></slot>
|
||||
<div class="shape">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<slot></slot>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
--tile-icon-color: var(--disabled-color);
|
||||
--tile-icon-opacity: 0.2;
|
||||
--tile-icon-hover-opacity: 0.35;
|
||||
--mdc-icon-size: 24px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
transition: transform 180ms ease-in-out;
|
||||
--mdc-icon-size: 22px;
|
||||
}
|
||||
:host([interactive]:active) {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
:host([interactive]:hover) {
|
||||
--tile-icon-opacity: var(--tile-icon-hover-opacity);
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
transition: box-shadow 180ms ease-in-out;
|
||||
}
|
||||
:host([interactive]:focus-visible) .container {
|
||||
box-shadow: 0 0 0 2px var(--tile-icon-color);
|
||||
}
|
||||
.container.rounded-square {
|
||||
border-radius: 8px;
|
||||
}
|
||||
.container.square {
|
||||
border-radius: 0;
|
||||
}
|
||||
.container.background::before {
|
||||
.shape::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -83,21 +27,24 @@ export class HaTileIcon extends LitElement {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--tile-icon-color);
|
||||
transition:
|
||||
background-color 180ms ease-in-out,
|
||||
opacity 180ms ease-in-out;
|
||||
opacity: var(--tile-icon-opacity);
|
||||
transition: background-color 180ms ease-in-out;
|
||||
opacity: 0.2;
|
||||
}
|
||||
.container ::slotted([slot="icon"]) {
|
||||
.shape {
|
||||
position: relative;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: color 180ms ease-in-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
.shape ::slotted(*) {
|
||||
display: flex;
|
||||
color: var(--tile-icon-color);
|
||||
transition: color 180ms ease-in-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
.container img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
53
src/components/tile/ha-tile-image.ts
Normal file
53
src/components/tile/ha-tile-image.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
|
||||
export type TileImageStyle = "square" | "rounded-square" | "circle";
|
||||
@customElement("ha-tile-image")
|
||||
export class HaTileImage extends LitElement {
|
||||
@property({ attribute: false }) public imageUrl?: string;
|
||||
|
||||
@property({ attribute: false }) public imageAlt?: string;
|
||||
|
||||
@property({ attribute: false }) public imageStyle: TileImageStyle = "circle";
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="image ${this.imageStyle}">
|
||||
${this.imageUrl
|
||||
? html`<img alt=${ifDefined(this.imageAlt)} src=${this.imageUrl} />`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.image {
|
||||
position: relative;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
.image.rounded-square {
|
||||
border-radius: 8%;
|
||||
}
|
||||
.image.square {
|
||||
border-radius: 0;
|
||||
}
|
||||
.image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-tile-image": HaTileImage;
|
||||
}
|
||||
}
|
@@ -2,6 +2,7 @@ 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,
|
||||
@@ -12,9 +13,6 @@ import type { HomeAssistant } from "../types";
|
||||
import { fileDownload } from "../util/file_download";
|
||||
import { domainToName } from "./integration";
|
||||
import type { FrontendLocaleData } from "./translation";
|
||||
import type { BackupManagerState, ManagerStateEvent } from "./backup_manager";
|
||||
import checkValidDate from "../common/datetime/check_valid_date";
|
||||
import { handleFetchPromise } from "../util/hass-call-api";
|
||||
|
||||
export const enum BackupScheduleRecurrence {
|
||||
NEVER = "never",
|
||||
@@ -131,13 +129,7 @@ export interface BackupContentExtended extends BackupContent, BackupData {}
|
||||
|
||||
export interface BackupInfo {
|
||||
backups: BackupContent[];
|
||||
agent_errors: Record<string, string>;
|
||||
last_attempted_automatic_backup: string | null;
|
||||
last_completed_automatic_backup: string | null;
|
||||
last_non_idle_event: ManagerStateEvent | null;
|
||||
next_automatic_backup: string | null;
|
||||
next_automatic_backup_additional: boolean;
|
||||
state: BackupManagerState;
|
||||
backing_up: boolean;
|
||||
}
|
||||
|
||||
export interface BackupDetails {
|
||||
@@ -239,23 +231,27 @@ export const restoreBackup = (
|
||||
export const uploadBackup = async (
|
||||
hass: HomeAssistant,
|
||||
file: File,
|
||||
agentIds: string[]
|
||||
): Promise<{ backup_id: string }> => {
|
||||
agent_ids: string[]
|
||||
): Promise<void> => {
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
const params = agent_ids.reduce((acc, agent_id) => {
|
||||
acc.append("agent_id", agent_id);
|
||||
return acc;
|
||||
}, new URLSearchParams());
|
||||
|
||||
agentIds.forEach((agentId) => {
|
||||
params.append("agent_id", agentId);
|
||||
});
|
||||
|
||||
return handleFetchPromise(
|
||||
hass.fetchWithAuth(`/api/backup/upload?${params.toString()}`, {
|
||||
const resp = await 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[]) => {
|
||||
@@ -453,13 +449,3 @@ 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,
|
||||
};
|
||||
|
@@ -1,66 +0,0 @@
|
||||
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,8 +19,6 @@ 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;
|
||||
@@ -29,30 +27,6 @@ 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,11 +2,7 @@ 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"
|
||||
| "config_subentries_flow"
|
||||
| "options_flow"
|
||||
| "repair_flow";
|
||||
export type FlowType = "config_flow" | "options_flow" | "repair_flow";
|
||||
|
||||
export interface DataEntryFlowProgressedEvent {
|
||||
type: "data_entry_flow_progressed";
|
||||
|
@@ -17,7 +17,6 @@ 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,7 +50,6 @@ 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[];
|
||||
|
@@ -1,5 +1,6 @@
|
||||
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";
|
||||
|
||||
@@ -81,24 +82,34 @@ export const fetchHassioBackups = async (
|
||||
};
|
||||
|
||||
export const fetchHassioBackupInfo = async (
|
||||
hass: HomeAssistant,
|
||||
hass: HomeAssistant | undefined,
|
||||
backup: string
|
||||
): Promise<HassioBackupDetail> => {
|
||||
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",
|
||||
});
|
||||
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`
|
||||
)
|
||||
);
|
||||
}
|
||||
// When called from onboarding we don't have hass
|
||||
return hassioApiResultExtractor(
|
||||
await hass.callApi<HassioResponse<HassioBackupDetail>>(
|
||||
"GET",
|
||||
`hassio/${
|
||||
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
|
||||
}/${backup}/info`
|
||||
await handleFetchPromise(
|
||||
fetch(`/api/hassio/backups/${backup}/info`, {
|
||||
method: "GET",
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -229,15 +240,24 @@ export const uploadBackup = async (
|
||||
};
|
||||
|
||||
export const restoreBackup = async (
|
||||
hass: HomeAssistant,
|
||||
hass: HomeAssistant | undefined,
|
||||
type: HassioBackupDetail["type"],
|
||||
backupSlug: string,
|
||||
backupDetails: HassioPartialBackupCreateParams | HassioFullBackupCreateParams,
|
||||
useBackupUrl: boolean
|
||||
useSnapshotUrl: boolean
|
||||
): Promise<void> => {
|
||||
await hass.callApi<HassioResponse<{ job_id: string }>>(
|
||||
"POST",
|
||||
`hassio/${useBackupUrl ? "backups" : "snapshots"}/${backupSlug}/restore/${type}`,
|
||||
backupDetails
|
||||
);
|
||||
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),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@@ -1,46 +0,0 @@
|
||||
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,7 +63,6 @@ export type TranslationCategory =
|
||||
| "entity_component"
|
||||
| "exceptions"
|
||||
| "config"
|
||||
| "config_subentries"
|
||||
| "config_panel"
|
||||
| "options"
|
||||
| "device_automation"
|
||||
|
@@ -282,8 +282,6 @@ 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 !== "config_flow" && this.flowType !== "options_flow") {
|
||||
if (this.flowType === "repair_flow") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
@@ -147,7 +147,7 @@ class FlowPreviewTemplate extends LitElement {
|
||||
(await this._unsub)();
|
||||
this._unsub = undefined;
|
||||
}
|
||||
if (this.flowType !== "config_flow" && this.flowType !== "options_flow") {
|
||||
if (this.flowType === "repair_flow") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
@@ -16,9 +16,7 @@ export const loadConfigFlowDialog = loadDataEntryFlowDialog;
|
||||
|
||||
export const showConfigFlowDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: Omit<DataEntryFlowDialogParams, "flowConfig"> & {
|
||||
entryId?: string;
|
||||
}
|
||||
dialogParams: Omit<DataEntryFlowDialogParams, "flowConfig">
|
||||
): 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");
|
||||
|
@@ -1,275 +0,0 @@
|
||||
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,7 +60,6 @@ class StepFlowAbort extends LitElement {
|
||||
dialogClosedCallback: this.params.dialogClosedCallback,
|
||||
startFlowHandler: this.domain,
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
navigateToResult: this.params.navigateToResult,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@@ -29,8 +29,6 @@ class StepFlowCreateEntry extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry;
|
||||
|
||||
navigateToResult = false;
|
||||
|
||||
private _devices = memoizeOne(
|
||||
(
|
||||
showDevices: boolean,
|
||||
@@ -85,7 +83,6 @@ class StepFlowCreateEntry extends LitElement {
|
||||
assistSatelliteSupportsSetupFlow(this.hass.states[satellite.entity_id])
|
||||
)
|
||||
) {
|
||||
this.navigateToResult = false;
|
||||
this._flowDone();
|
||||
showVoiceAssistantSetupDialog(this, {
|
||||
deviceId: devices[0].id,
|
||||
@@ -156,7 +153,7 @@ class StepFlowCreateEntry extends LitElement {
|
||||
|
||||
private _flowDone(): void {
|
||||
fireEvent(this, "flow-update", { step: undefined });
|
||||
if (this.step.result && this.navigateToResult) {
|
||||
if (this.step.result) {
|
||||
navigate(
|
||||
`/config/integrations/integration/${this.step.result.domain}#config_entry=${this.step.result.entry_id}`
|
||||
);
|
||||
|
@@ -49,8 +49,6 @@ class LightRgbColorPicker extends LitElement {
|
||||
|
||||
@state() private _hsPickerValue?: [number, number];
|
||||
|
||||
@state() private _isInteracting?: boolean;
|
||||
|
||||
protected render() {
|
||||
if (!this.stateObj) {
|
||||
return nothing;
|
||||
@@ -213,10 +211,7 @@ class LightRgbColorPicker extends LitElement {
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (
|
||||
this._isInteracting ||
|
||||
(!changedProps.has("entityId") && !changedProps.has("hass"))
|
||||
) {
|
||||
if (!changedProps.has("entityId") && !changedProps.has("hass")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -224,13 +219,10 @@ class LightRgbColorPicker extends LitElement {
|
||||
}
|
||||
|
||||
private _hsColorCursorMoved(ev: CustomEvent) {
|
||||
const color = ev.detail.value;
|
||||
this._isInteracting = color !== undefined;
|
||||
|
||||
if (color === undefined) {
|
||||
if (!ev.detail.value) {
|
||||
return;
|
||||
}
|
||||
this._hsPickerValue = color;
|
||||
this._hsPickerValue = ev.detail.value;
|
||||
|
||||
this._throttleUpdateColor();
|
||||
}
|
||||
|
@@ -22,6 +22,7 @@ import { DOMAIN_ATTRIBUTES_UNITS } from "../../../../data/entity_attributes";
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"color-changed": LightColor;
|
||||
"color-hovered": LightColor | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,8 +54,6 @@ class LightColorTempPicker extends LitElement {
|
||||
|
||||
@state() private _ctPickerValue?: number;
|
||||
|
||||
@state() private _isInteracting?: boolean;
|
||||
|
||||
protected render() {
|
||||
if (!this.stateObj) {
|
||||
return nothing;
|
||||
@@ -114,7 +113,7 @@ class LightColorTempPicker extends LitElement {
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (this._isInteracting || !changedProps.has("stateObj")) {
|
||||
if (!changedProps.has("stateObj")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -124,14 +123,16 @@ class LightColorTempPicker extends LitElement {
|
||||
private _ctColorCursorMoved(ev: CustomEvent) {
|
||||
const ct = ev.detail.value;
|
||||
|
||||
this._isInteracting = ct !== undefined;
|
||||
|
||||
if (isNaN(ct) || this._ctPickerValue === ct) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._ctPickerValue = ct;
|
||||
|
||||
fireEvent(this, "color-hovered", {
|
||||
color_temp_kelvin: ct,
|
||||
});
|
||||
|
||||
this._throttleUpdateColorTemp();
|
||||
}
|
||||
|
||||
@@ -142,6 +143,8 @@ class LightColorTempPicker extends LitElement {
|
||||
private _ctColorChanged(ev: CustomEvent) {
|
||||
const ct = ev.detail.value;
|
||||
|
||||
fireEvent(this, "color-hovered", undefined);
|
||||
|
||||
if (isNaN(ct) || this._ctPickerValue === ct) {
|
||||
return;
|
||||
}
|
||||
|
@@ -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,7 +41,6 @@ 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";
|
||||
@@ -158,9 +157,8 @@ 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>`;
|
||||
}
|
||||
@@ -168,6 +166,8 @@ 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,6 +272,10 @@ 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(
|
||||
@@ -450,7 +454,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
subscribeOne(conn, subscribeUser),
|
||||
]);
|
||||
this.initializeHass(auth, conn);
|
||||
if (this.language !== this.hass!.language) {
|
||||
if (this.language && this.language !== this.hass!.language) {
|
||||
this._updateHass({
|
||||
locale: { ...this.hass!.locale, language: this.language },
|
||||
language: this.language,
|
||||
|
@@ -1,336 +1,136 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
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 { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup";
|
||||
import "../../hassio/src/components/hassio-upload-backup";
|
||||
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;
|
||||
|
||||
@property({ type: Boolean }) public supervisor = false;
|
||||
@state() private _restoring = false;
|
||||
|
||||
@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;
|
||||
@state() private _backupSlug?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${
|
||||
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._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
|
||||
${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}
|
||||
.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
|
||||
}
|
||||
></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.localize("ui.panel.page-onboarding.restore.restore")}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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 _loadBackupInfo() {
|
||||
let onboardingInfo: BackupOnboardingConfig;
|
||||
try {
|
||||
onboardingInfo = await fetchBackupOnboardingInfo();
|
||||
} catch (err: any) {
|
||||
if (this._restoreRunning) {
|
||||
private async _checkRestoreStatus(): Promise<void> {
|
||||
if (this._restoring) {
|
||||
try {
|
||||
await fetchInstallationType();
|
||||
} catch (err: any) {
|
||||
if (
|
||||
err.error === "Request error" ||
|
||||
// core can restart but haven't loaded the backup integration yet
|
||||
(err.status_code === 500 && err.body?.error === "backup_disabled")
|
||||
(err as Error).message === "unauthorized" ||
|
||||
(err as Error).message === "not_found"
|
||||
) {
|
||||
// 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 _restore(ev: CustomEvent) {
|
||||
if (!this._backup || !ev.detail.selectedData) {
|
||||
return;
|
||||
}
|
||||
this._selectedData = ev.detail.selectedData;
|
||||
|
||||
this._view = "confirm_restore";
|
||||
private _scheduleCheckRestoreStatus(): void {
|
||||
setTimeout(() => this._checkRestoreStatus(), 1000);
|
||||
}
|
||||
|
||||
private _reupload() {
|
||||
this._backup = undefined;
|
||||
this._backupId = undefined;
|
||||
this._view = "upload";
|
||||
private _showBackupDialog(): void {
|
||||
showHassioBackupDialog(this, {
|
||||
slug: this._backupSlug!,
|
||||
onboarding: true,
|
||||
localize: this.localize,
|
||||
onRestoring: () => {
|
||||
this._restoring = true;
|
||||
this._scheduleCheckRestoreStatus();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
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%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type { HomeAssistant } from "../types";
|
||||
@@ -13,6 +13,8 @@ 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>
|
||||
@@ -22,9 +24,11 @@ class OnboardingWelcome extends LitElement {
|
||||
${this.localize("ui.panel.page-onboarding.welcome.start")}
|
||||
</ha-button>
|
||||
|
||||
<ha-button @click=${this._restoreBackup}>
|
||||
${this.localize("ui.panel.page-onboarding.welcome.restore_backup")}
|
||||
</ha-button>
|
||||
${this.supervisor
|
||||
? html`<ha-button @click=${this._restoreBackup}>
|
||||
${this.localize("ui.panel.page-onboarding.welcome.restore_backup")}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -1,57 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@@ -1,175 +0,0 @@
|
||||
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";
|
||||
}
|
||||
}
|
@@ -1,119 +0,0 @@
|
||||
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";
|
||||
}
|
||||
}
|
@@ -1,126 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
import { consume } from "@lit-labs/context";
|
||||
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import {
|
||||
mdiAlertCircleCheck,
|
||||
mdiArrowDown,
|
||||
@@ -26,9 +27,7 @@ import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-menu-item";
|
||||
import "../../../../components/ha-md-divider";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
@@ -241,104 +240,89 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
</div> `
|
||||
: nothing}
|
||||
|
||||
<ha-md-button-menu
|
||||
<ha-button-menu
|
||||
slot="icons"
|
||||
@action=${this._handleAction}
|
||||
@click=${preventDefault}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
positioning="fixed"
|
||||
fixed
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-md-menu-item .clickAction=${this._runAction}>
|
||||
<ha-list-item graphic="icon">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.run"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiPlay}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPlay}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._renameAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<li divider role="separator"></li>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._duplicateAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.duplicate"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
slot="graphic"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._copyAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.copy"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._cutAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.cut"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveUp}
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled || this.first}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-list-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveDown}
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled || this.last}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-list-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${!this._uiModeAvailable}
|
||||
>
|
||||
<ha-list-item graphic="icon" .disabled=${!this._uiModeAvailable}>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlaylistEdit}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<li divider role="separator"></li>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDisable}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
${this.action.enabled === false
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.enable"
|
||||
@@ -347,15 +331,15 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
"ui.panel.config.automation.editor.actions.disable"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
slot="graphic"
|
||||
.path=${this.action.enabled === false
|
||||
? mdiPlayCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
</ha-list-item>
|
||||
<ha-list-item
|
||||
class="warning"
|
||||
.clickAction=${this._onDelete}
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
@@ -363,11 +347,11 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="start"
|
||||
slot="graphic"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
|
||||
<div
|
||||
class=${classMap({
|
||||
@@ -440,6 +424,47 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._runAction();
|
||||
break;
|
||||
case 1:
|
||||
await this._renameAction();
|
||||
break;
|
||||
case 2:
|
||||
fireEvent(this, "duplicate");
|
||||
break;
|
||||
case 3:
|
||||
this._setClipboard();
|
||||
break;
|
||||
case 4:
|
||||
this._setClipboard();
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
break;
|
||||
case 5:
|
||||
fireEvent(this, "move-up");
|
||||
break;
|
||||
case 6:
|
||||
fireEvent(this, "move-down");
|
||||
break;
|
||||
case 7:
|
||||
if (this._yamlMode) {
|
||||
this._switchUiMode();
|
||||
} else {
|
||||
this._switchYamlMode();
|
||||
}
|
||||
this.expand();
|
||||
break;
|
||||
case 8:
|
||||
this._onDisable();
|
||||
break;
|
||||
case 9:
|
||||
this._onDelete();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _setClipboard() {
|
||||
this._clipboard = {
|
||||
...this._clipboard,
|
||||
@@ -447,16 +472,16 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private _onDisable = () => {
|
||||
private _onDisable() {
|
||||
const enabled = !(this.action.enabled ?? true);
|
||||
const value = { ...this.action, enabled };
|
||||
fireEvent(this, "value-changed", { value });
|
||||
if (this._yamlMode) {
|
||||
this._yamlEditor?.setValue(value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _runAction = async () => {
|
||||
private async _runAction() {
|
||||
const validated = await validateConfig(this.hass, {
|
||||
actions: this.action,
|
||||
});
|
||||
@@ -488,9 +513,9 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
"ui.panel.config.automation.editor.actions.run_action_success"
|
||||
),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private _onDelete = () => {
|
||||
private _onDelete() {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.delete_confirm_title"
|
||||
@@ -505,7 +530,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private _onYamlChange(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
@@ -536,7 +561,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
this._yamlMode = true;
|
||||
}
|
||||
|
||||
private _renameAction = async (): Promise<void> => {
|
||||
private async _renameAction(): Promise<void> {
|
||||
const alias = await showPromptDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.change_alias"
|
||||
@@ -573,37 +598,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
this._yamlEditor?.setValue(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private _duplicateAction = () => {
|
||||
fireEvent(this, "duplicate");
|
||||
};
|
||||
|
||||
private _copyAction = () => {
|
||||
this._setClipboard();
|
||||
};
|
||||
|
||||
private _cutAction = () => {
|
||||
this._setClipboard();
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
};
|
||||
|
||||
private _moveUp = () => {
|
||||
fireEvent(this, "move-up");
|
||||
};
|
||||
|
||||
private _moveDown = () => {
|
||||
fireEvent(this, "move-down");
|
||||
};
|
||||
|
||||
private _toggleYamlMode = () => {
|
||||
if (this._yamlMode) {
|
||||
this._switchUiMode();
|
||||
} else {
|
||||
this._switchYamlMode();
|
||||
}
|
||||
this.expand();
|
||||
};
|
||||
}
|
||||
|
||||
public expand() {
|
||||
this.updateComplete.then(() => {
|
||||
@@ -615,6 +610,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
ha-button-menu,
|
||||
ha-icon-button {
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
}
|
||||
@@ -653,11 +649,18 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
border-top-right-radius: var(--ha-card-border-radius, 12px);
|
||||
border-top-left-radius: var(--ha-card-border-radius, 12px);
|
||||
}
|
||||
|
||||
mwc-list-item[disabled] {
|
||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||
}
|
||||
mwc-list-item.hidden {
|
||||
display: none;
|
||||
}
|
||||
.warning ul {
|
||||
margin: 4px 0;
|
||||
}
|
||||
ha-md-menu-item > ha-svg-icon {
|
||||
--mdc-icon-size: 24px;
|
||||
li[role="separator"] {
|
||||
border-bottom-color: var(--divider-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { consume } from "@lit-labs/context";
|
||||
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import {
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
@@ -23,12 +24,11 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-menu-item";
|
||||
import "../../../../components/ha-md-divider";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-list-item";
|
||||
import type {
|
||||
AutomationClipboard,
|
||||
Condition,
|
||||
@@ -141,12 +141,12 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
<ha-md-button-menu
|
||||
<ha-button-menu
|
||||
slot="icons"
|
||||
@action=${this._handleAction}
|
||||
@click=${preventDefault}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
positioning="fixed"
|
||||
fixed
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
@@ -155,91 +155,76 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
>
|
||||
</ha-icon-button>
|
||||
|
||||
<ha-md-menu-item .clickAction=${this._testCondition}>
|
||||
<ha-list-item graphic="icon">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.test"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiFlask}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._renameCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiFlask}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<li divider role="separator"></li>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._duplicateCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.duplicate"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
slot="graphic"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._copyCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.copy"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._cutCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.cut"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveUp}
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled || this.first}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-list-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveDown}
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled || this.last}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-list-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${this._warnings}
|
||||
>
|
||||
<ha-list-item graphic="icon" .disabled=${this._warnings}>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlaylistEdit}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<li divider role="separator"></li>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDisable}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
${this.condition.enabled === false
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.enable"
|
||||
@@ -248,15 +233,15 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
"ui.panel.config.automation.editor.actions.disable"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
slot="graphic"
|
||||
.path=${this.condition.enabled === false
|
||||
? mdiPlayCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
</ha-list-item>
|
||||
<ha-list-item
|
||||
class="warning"
|
||||
.clickAction=${this._onDelete}
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
@@ -264,11 +249,11 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="start"
|
||||
slot="graphic"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
|
||||
<div
|
||||
class=${classMap({
|
||||
@@ -340,6 +325,47 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
await this._testCondition();
|
||||
break;
|
||||
case 1:
|
||||
await this._renameCondition();
|
||||
break;
|
||||
case 2:
|
||||
fireEvent(this, "duplicate");
|
||||
break;
|
||||
case 3:
|
||||
this._setClipboard();
|
||||
break;
|
||||
case 4:
|
||||
this._setClipboard();
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
break;
|
||||
case 5:
|
||||
fireEvent(this, "move-up");
|
||||
break;
|
||||
case 6:
|
||||
fireEvent(this, "move-down");
|
||||
break;
|
||||
case 7:
|
||||
if (this._yamlMode) {
|
||||
this._switchUiMode();
|
||||
} else {
|
||||
this._switchYamlMode();
|
||||
}
|
||||
this.expand();
|
||||
break;
|
||||
case 8:
|
||||
this._onDisable();
|
||||
break;
|
||||
case 9:
|
||||
this._onDelete();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _setClipboard() {
|
||||
this._clipboard = {
|
||||
...this._clipboard,
|
||||
@@ -347,13 +373,13 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private _onDisable = () => {
|
||||
private _onDisable() {
|
||||
const enabled = !(this.condition.enabled ?? true);
|
||||
const value = { ...this.condition, enabled };
|
||||
fireEvent(this, "value-changed", { value });
|
||||
};
|
||||
}
|
||||
|
||||
private _onDelete = () => {
|
||||
private _onDelete() {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.delete_confirm_title"
|
||||
@@ -368,7 +394,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private _switchUiMode() {
|
||||
this._warnings = undefined;
|
||||
@@ -380,7 +406,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
this._yamlMode = true;
|
||||
}
|
||||
|
||||
private _testCondition = async () => {
|
||||
private async _testCondition() {
|
||||
if (this._testing) {
|
||||
return;
|
||||
}
|
||||
@@ -435,9 +461,9 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
this._testing = false;
|
||||
}, 2500);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _renameCondition = async (): Promise<void> => {
|
||||
private async _renameCondition(): Promise<void> {
|
||||
const alias = await showPromptDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.change_alias"
|
||||
@@ -463,37 +489,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
value,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private _duplicateCondition = () => {
|
||||
fireEvent(this, "duplicate");
|
||||
};
|
||||
|
||||
private _copyCondition = () => {
|
||||
this._setClipboard();
|
||||
};
|
||||
|
||||
private _cutCondition = () => {
|
||||
this._setClipboard();
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
};
|
||||
|
||||
private _moveUp = () => {
|
||||
fireEvent(this, "move-up");
|
||||
};
|
||||
|
||||
private _moveDown = () => {
|
||||
fireEvent(this, "move-down");
|
||||
};
|
||||
|
||||
private _toggleYamlMode = () => {
|
||||
if (this._yamlMode) {
|
||||
this._switchUiMode();
|
||||
} else {
|
||||
this._switchYamlMode();
|
||||
}
|
||||
this.expand();
|
||||
};
|
||||
}
|
||||
|
||||
public expand() {
|
||||
this.updateComplete.then(() => {
|
||||
@@ -505,6 +501,9 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
ha-button-menu {
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
}
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
@@ -540,6 +539,12 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
border-top-right-radius: var(--ha-card-border-radius, 12px);
|
||||
border-top-left-radius: var(--ha-card-border-radius, 12px);
|
||||
}
|
||||
ha-list-item[disabled] {
|
||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||
}
|
||||
ha-list-item.hidden {
|
||||
display: none;
|
||||
}
|
||||
.testing {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
@@ -566,8 +571,8 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
.testing.pass {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
ha-md-menu-item > ha-svg-icon {
|
||||
--mdc-icon-size: 24px;
|
||||
li[role="separator"] {
|
||||
border-bottom-color: var(--divider-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -339,8 +339,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
hidden: narrow,
|
||||
title: "",
|
||||
type: "overflow",
|
||||
title: this.hass.localize("ui.panel.config.automation.picker.state"),
|
||||
label: this.hass.localize("ui.panel.config.automation.picker.state"),
|
||||
template: (automation) => html`
|
||||
<ha-entity-toggle
|
||||
.stateObj=${automation}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { consume } from "@lit-labs/context";
|
||||
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import {
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
@@ -27,9 +28,7 @@ import { capitalizeFirstLetter } from "../../../../common/string/capitalize-firs
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import { debounce } from "../../../../common/util/debounce";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-menu-item";
|
||||
import "../../../../components/ha-md-divider";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
@@ -170,12 +169,12 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
<ha-md-button-menu
|
||||
<ha-button-menu
|
||||
slot="icons"
|
||||
@action=${this._handleAction}
|
||||
@click=${preventDefault}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
positioning="fixed"
|
||||
fixed
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
@@ -183,93 +182,84 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._renameTrigger}
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._showTriggerId}
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.edit_id"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiIdentifier}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiIdentifier}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<li divider role="separator"></li>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._duplicateTrigger}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.duplicate"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
slot="graphic"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._copyTrigger}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.copy"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._cutTrigger}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.cut"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveUp}
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled || this.first}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-list-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveDown}
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled || this.last}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-list-item>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${!supported}
|
||||
>
|
||||
<ha-list-item graphic="icon" .disabled=${!supported}>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlaylistEdit}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<li divider role="separator"></li>
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDisable}
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${"enabled" in this.trigger && this.trigger.enabled === false
|
||||
@@ -280,16 +270,16 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
"ui.panel.config.automation.editor.actions.disable"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
slot="graphic"
|
||||
.path=${"enabled" in this.trigger &&
|
||||
this.trigger.enabled === false
|
||||
? mdiPlayCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDelete}
|
||||
</ha-list-item>
|
||||
<ha-list-item
|
||||
class="warning"
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
@@ -297,11 +287,11 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="start"
|
||||
slot="graphic"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
|
||||
<div
|
||||
class=${classMap({
|
||||
@@ -474,6 +464,48 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
await this._renameTrigger();
|
||||
break;
|
||||
case 1:
|
||||
this._requestShowId = true;
|
||||
this.expand();
|
||||
break;
|
||||
case 2:
|
||||
fireEvent(this, "duplicate");
|
||||
break;
|
||||
case 3:
|
||||
this._setClipboard();
|
||||
break;
|
||||
case 4:
|
||||
this._setClipboard();
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
break;
|
||||
case 5:
|
||||
fireEvent(this, "move-up");
|
||||
break;
|
||||
case 6:
|
||||
fireEvent(this, "move-down");
|
||||
break;
|
||||
case 7:
|
||||
if (this._yamlMode) {
|
||||
this._switchUiMode();
|
||||
} else {
|
||||
this._switchYamlMode();
|
||||
}
|
||||
this.expand();
|
||||
break;
|
||||
case 8:
|
||||
this._onDisable();
|
||||
break;
|
||||
case 9:
|
||||
this._onDelete();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _setClipboard() {
|
||||
this._clipboard = {
|
||||
...this._clipboard,
|
||||
@@ -481,7 +513,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private _onDelete = () => {
|
||||
private _onDelete() {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.delete_confirm_title"
|
||||
@@ -496,9 +528,9 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private _onDisable = () => {
|
||||
private _onDisable() {
|
||||
if (isTriggerList(this.trigger)) return;
|
||||
const enabled = !(this.trigger.enabled ?? true);
|
||||
const value = { ...this.trigger, enabled };
|
||||
@@ -506,7 +538,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
if (this._yamlMode) {
|
||||
this._yamlEditor?.setValue(value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _idChanged(ev: CustomEvent) {
|
||||
if (isTriggerList(this.trigger)) return;
|
||||
@@ -573,7 +605,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _renameTrigger = async (): Promise<void> => {
|
||||
private async _renameTrigger(): Promise<void> {
|
||||
if (isTriggerList(this.trigger)) return;
|
||||
const alias = await showPromptDialog(this, {
|
||||
title: this.hass.localize(
|
||||
@@ -604,42 +636,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
this._yamlEditor?.setValue(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private _showTriggerId = () => {
|
||||
this._requestShowId = true;
|
||||
this.expand();
|
||||
};
|
||||
|
||||
private _duplicateTrigger = () => {
|
||||
fireEvent(this, "duplicate");
|
||||
};
|
||||
|
||||
private _copyTrigger = () => {
|
||||
this._setClipboard();
|
||||
};
|
||||
|
||||
private _cutTrigger = () => {
|
||||
this._setClipboard();
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
};
|
||||
|
||||
private _moveUp = () => {
|
||||
fireEvent(this, "move-up");
|
||||
};
|
||||
|
||||
private _moveDown = () => {
|
||||
fireEvent(this, "move-down");
|
||||
};
|
||||
|
||||
private _toggleYamlMode = () => {
|
||||
if (this._yamlMode) {
|
||||
this._switchUiMode();
|
||||
} else {
|
||||
this._switchYamlMode();
|
||||
}
|
||||
this.expand();
|
||||
};
|
||||
}
|
||||
|
||||
public expand() {
|
||||
this.updateComplete.then(() => {
|
||||
@@ -651,6 +648,9 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
ha-button-menu {
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
}
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
@@ -714,12 +714,18 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
background-color: var(--accent-color);
|
||||
color: var(--text-accent-color, var(--text-primary-color));
|
||||
}
|
||||
ha-list-item[disabled] {
|
||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||
}
|
||||
ha-list-item.hidden {
|
||||
display: none;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
ha-md-menu-item > ha-svg-icon {
|
||||
--mdc-icon-size: 24px;
|
||||
li[role="separator"] {
|
||||
border-bottom-color: var(--divider-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
@@ -23,6 +22,7 @@ import {
|
||||
import type { CloudStatus } from "../../../../../data/cloud";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { brandsUrl } from "../../../../../util/brands-url";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
|
||||
const DEFAULT_AGENTS = [];
|
||||
|
||||
|
@@ -46,7 +46,7 @@ enum BackupScheduleTime {
|
||||
}
|
||||
|
||||
interface RetentionData {
|
||||
type: "copies" | "days" | "forever";
|
||||
type: "copies" | "days";
|
||||
value: number;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ const RETENTION_PRESETS: Record<
|
||||
RetentionData
|
||||
> = {
|
||||
copies_3: { type: "copies", value: 3 },
|
||||
forever: { type: "forever", value: 0 },
|
||||
forever: { type: "days", value: 0 },
|
||||
};
|
||||
|
||||
const SCHEDULE_OPTIONS = [
|
||||
@@ -79,10 +79,7 @@ const computeRetentionPreset = (
|
||||
data: RetentionData
|
||||
): RetentionPreset | undefined => {
|
||||
for (const [key, value] of Object.entries(RETENTION_PRESETS)) {
|
||||
if (
|
||||
value.type === data.type &&
|
||||
(value.type === RetentionPreset.FOREVER || value.value === data.value)
|
||||
) {
|
||||
if (value.type === data.type && value.value === data.value) {
|
||||
return key as RetentionPreset;
|
||||
}
|
||||
}
|
||||
@@ -95,7 +92,7 @@ interface FormData {
|
||||
time?: string | null;
|
||||
days: BackupDay[];
|
||||
retention: {
|
||||
type: "copies" | "days" | "forever";
|
||||
type: "copies" | "days";
|
||||
value: number;
|
||||
};
|
||||
}
|
||||
@@ -145,12 +142,7 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
? config.schedule.days
|
||||
: [],
|
||||
retention: {
|
||||
type:
|
||||
config.retention.days === null && config.retention.copies === null
|
||||
? "forever"
|
||||
: config.retention.days != null
|
||||
? "days"
|
||||
: "copies",
|
||||
type: config.retention.days != null ? "days" : "copies",
|
||||
value: config.retention.days ?? config.retention.copies ?? 3,
|
||||
},
|
||||
};
|
||||
@@ -168,11 +160,9 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
: [],
|
||||
},
|
||||
retention:
|
||||
data.retention.type === "forever"
|
||||
? { days: null, copies: null }
|
||||
: data.retention.type === "days"
|
||||
? { days: data.retention.value, copies: null }
|
||||
: { copies: data.retention.value, days: null },
|
||||
data.retention.type === "days"
|
||||
? { days: data.retention.value, copies: null }
|
||||
: { copies: data.retention.value, days: null },
|
||||
};
|
||||
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
@@ -491,19 +481,9 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
private _retentionPresetChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
const target = ev.currentTarget as HaMdSelect;
|
||||
let value = target.value as RetentionPreset;
|
||||
|
||||
// custom needs to have a type of days or copies, set it to default copies 3
|
||||
if (
|
||||
value === RetentionPreset.CUSTOM &&
|
||||
this._retentionPreset === RetentionPreset.FOREVER
|
||||
) {
|
||||
this._retentionPreset = value;
|
||||
value = RetentionPreset.COPIES_3;
|
||||
} else {
|
||||
this._retentionPreset = value;
|
||||
}
|
||||
const value = target.value as RetentionPreset;
|
||||
|
||||
this._retentionPreset = value;
|
||||
if (value !== RetentionPreset.CUSTOM) {
|
||||
const data = this._getData(this.value);
|
||||
const retention = RETENTION_PRESETS[value];
|
||||
@@ -513,7 +493,7 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
}
|
||||
this._setData({
|
||||
...data,
|
||||
retention,
|
||||
retention: RETENTION_PRESETS[value],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -524,7 +504,6 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
const value = parseInt(target.value);
|
||||
const clamped = clamp(value, MIN_VALUE, MAX_VALUE);
|
||||
const data = this._getData(this.value);
|
||||
target.value = clamped.toString();
|
||||
this._setData({
|
||||
...data,
|
||||
retention: {
|
||||
|
@@ -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,32 +47,23 @@ 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 (this.hass && isComponentLoaded(this.hass, "hassio")) {
|
||||
if (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,
|
||||
@@ -83,14 +74,16 @@ 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: localize(
|
||||
`ui.panel.${this.translationKeyPanel}.data_picker.${data.database_included ? "settings_and_history" : "settings"}`
|
||||
),
|
||||
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"),
|
||||
id: "config",
|
||||
version: data.homeassistant_version,
|
||||
});
|
||||
@@ -106,22 +99,18 @@ export class HaBackupDataPicker extends LitElement {
|
||||
);
|
||||
|
||||
private _localizeFolder(folder: string): string {
|
||||
const localize = this.localize || this.hass!.localize;
|
||||
|
||||
switch (folder) {
|
||||
case "media":
|
||||
return localize(
|
||||
`ui.panel.${this.translationKeyPanel}.data_picker.media`
|
||||
);
|
||||
return this.hass.localize("ui.panel.config.backup.data_picker.media");
|
||||
case "share":
|
||||
return localize(
|
||||
`ui.panel.${this.translationKeyPanel}.data_picker.share_folder`
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.backup.data_picker.share_folder"
|
||||
);
|
||||
case "ssl":
|
||||
return localize(`ui.panel.${this.translationKeyPanel}.data_picker.ssl`);
|
||||
return this.hass.localize("ui.panel.config.backup.data_picker.ssl");
|
||||
case "addons/local":
|
||||
return localize(
|
||||
`ui.panel.${this.translationKeyPanel}.data_picker.local_addons`
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.backup.data_picker.local_addons"
|
||||
);
|
||||
}
|
||||
return capitalizeFirstLetter(folder);
|
||||
@@ -226,13 +215,14 @@ export class HaBackupDataPicker extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const localize = this.localize || this.hass!.localize;
|
||||
|
||||
const homeAssistantItems = this._homeAssistantItems(this.data, localize);
|
||||
const homeAssistantItems = this._homeAssistantItems(
|
||||
this.data,
|
||||
this.hass.localize
|
||||
);
|
||||
|
||||
const addonsItems = this._addonsItems(
|
||||
this.data,
|
||||
localize,
|
||||
this.hass.localize,
|
||||
this._addonIcons
|
||||
);
|
||||
|
||||
@@ -257,7 +247,6 @@ 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">
|
||||
@@ -277,7 +266,6 @@ export class HaBackupDataPicker extends LitElement {
|
||||
item.id
|
||||
)}
|
||||
@change=${this._homeassistantChanged}
|
||||
.disabled=${this.requiredItems.includes(item.id)}
|
||||
></ha-checkbox>
|
||||
</ha-formfield>
|
||||
`
|
||||
@@ -292,8 +280,8 @@ export class HaBackupDataPicker extends LitElement {
|
||||
<ha-formfield>
|
||||
<ha-backup-formfield-label
|
||||
slot="label"
|
||||
.label=${localize(
|
||||
`ui.panel.${this.translationKeyPanel}.data_picker.addons`
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.backup.data_picker.addons"
|
||||
)}
|
||||
.iconPath=${mdiPuzzle}
|
||||
>
|
||||
|
@@ -1,148 +0,0 @@
|
||||
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 };
|
||||
}
|
||||
}
|
@@ -1,153 +0,0 @@
|
||||
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,10 +3,7 @@ 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,
|
||||
type HASSDomEvent,
|
||||
} from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
@@ -17,10 +14,7 @@ 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";
|
||||
@@ -28,6 +22,16 @@ 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
|
||||
@@ -41,13 +45,13 @@ export class DialogUploadBackup
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _formData?: BackupUploadFileFormData;
|
||||
@state() private _formData?: FormData;
|
||||
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
|
||||
public async showDialog(params: UploadBackupDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._formData = INITIAL_UPLOAD_FORM_DATA;
|
||||
this._formData = INITIAL_DATA;
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
@@ -74,18 +78,13 @@ export class DialogUploadBackup
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-md-dialog
|
||||
open
|
||||
@closed=${this._dialogClosed}
|
||||
.disableCancelAction=${this._uploading}
|
||||
>
|
||||
<ha-md-dialog open @closed=${this._dialogClosed}>
|
||||
<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">
|
||||
@@ -100,8 +99,7 @@ export class DialogUploadBackup
|
||||
.hass=${this.hass}
|
||||
.uploading=${this._uploading}
|
||||
.icon=${mdiFolderUpload}
|
||||
.accept=${SUPPORTED_UPLOAD_FORMAT}
|
||||
.localize=${this.hass.localize}
|
||||
accept=${SUPPORTED_FORMAT}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.upload.input_label"
|
||||
)}
|
||||
@@ -109,17 +107,13 @@ 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} .disabled=${this._uploading}
|
||||
<ha-button @click=${this.closeDialog}
|
||||
>${this.hass.localize("ui.common.cancel")}</ha-button
|
||||
>
|
||||
<ha-button
|
||||
@click=${this._upload}
|
||||
.disabled=${!this._formValid() || this._uploading}
|
||||
>
|
||||
<ha-button @click=${this._upload} .disabled=${!this._formValid()}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.upload.action"
|
||||
)}
|
||||
@@ -129,7 +123,7 @@ export class DialogUploadBackup
|
||||
`;
|
||||
}
|
||||
|
||||
private _filePicked(ev: HASSDomEvent<{ files: File[] }>) {
|
||||
private async _filePicked(ev: CustomEvent<{ files: File[] }>): Promise<void> {
|
||||
this._error = undefined;
|
||||
const file = ev.detail.files[0];
|
||||
|
||||
@@ -139,14 +133,9 @@ 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_UPLOAD_FORMAT) {
|
||||
if (!file || file.type !== SUPPORTED_FORMAT) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.upload.unsupported.title"
|
||||
@@ -165,7 +154,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,7 @@ import {
|
||||
mdiUpload,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
@@ -27,7 +27,6 @@ import type {
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-filter-states";
|
||||
import "../../../components/ha-icon";
|
||||
@@ -461,17 +460,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
extended
|
||||
@click=${this._newBackup}
|
||||
>
|
||||
${backupInProgress
|
||||
? html`<div slot="icon">
|
||||
<ha-circular-progress
|
||||
.size=${"small"}
|
||||
indeterminate
|
||||
></ha-circular-progress>
|
||||
</div>`
|
||||
: html`<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>`}
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
`
|
||||
: nothing}
|
||||
@@ -616,14 +605,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
ha-circular-progress {
|
||||
--md-sys-color-primary: var(--mdc-theme-on-secondary);
|
||||
}
|
||||
`,
|
||||
];
|
||||
return haStyle;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -8,6 +8,7 @@ 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";
|
||||
@@ -24,20 +25,24 @@ 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";
|
||||
@@ -88,6 +93,10 @@ class HaConfigBackupDetails extends LitElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _selectedData?: BackupData;
|
||||
|
||||
@state() private _addonsInfo?: HassioAddonInfo[];
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
@@ -148,18 +157,81 @@ class HaConfigBackupDetails extends LitElement {
|
||||
: !this._backup
|
||||
? html`<ha-circular-progress active></ha-circular-progress>`
|
||||
: html`
|
||||
<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(
|
||||
"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-card>
|
||||
<div class="card-header">
|
||||
${this.hass.localize(
|
||||
@@ -288,13 +360,30 @@ class HaConfigBackupDetails extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _restore(ev: CustomEvent) {
|
||||
if (!this._backup || !ev.detail.selectedData) {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
showRestoreBackupDialog(this, {
|
||||
backup: this._backup,
|
||||
selectedData: ev.detail.selectedData,
|
||||
selectedData: this._selectedData,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -380,6 +469,13 @@ 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);
|
||||
}
|
||||
@@ -389,6 +485,9 @@ 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;
|
||||
|
@@ -8,7 +8,6 @@ import "../../../components/ha-button";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
@@ -18,10 +17,8 @@ import type {
|
||||
BackupAgent,
|
||||
BackupConfig,
|
||||
BackupContent,
|
||||
BackupInfo,
|
||||
} from "../../../data/backup";
|
||||
import {
|
||||
computeBackupAgentName,
|
||||
generateBackup,
|
||||
generateBackupWithAutomaticSettings,
|
||||
} from "../../../data/backup";
|
||||
@@ -53,8 +50,6 @@ class HaConfigBackupOverview extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public manager!: ManagerStateEvent;
|
||||
|
||||
@property({ attribute: false }) public info?: BackupInfo;
|
||||
|
||||
@property({ attribute: false }) public backups: BackupContent[] = [];
|
||||
|
||||
@property({ attribute: false }) public fetching = false;
|
||||
@@ -156,26 +151,6 @@ class HaConfigBackupOverview extends LitElement {
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
<div class="content">
|
||||
${this.info && Object.keys(this.info.agent_errors).length
|
||||
? html`${Object.entries(this.info.agent_errors).map(
|
||||
([agentId, error]) =>
|
||||
html`<ha-alert
|
||||
alert-type="error"
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.backup.overview.agent_error",
|
||||
{
|
||||
name: computeBackupAgentName(
|
||||
this.hass.localize,
|
||||
agentId,
|
||||
this.agents
|
||||
),
|
||||
}
|
||||
)}
|
||||
>
|
||||
${error}
|
||||
</ha-alert>`
|
||||
)}`
|
||||
: nothing}
|
||||
${backupInProgress
|
||||
? html`
|
||||
<ha-backup-overview-progress
|
||||
@@ -229,14 +204,7 @@ class HaConfigBackupOverview extends LitElement {
|
||||
extended
|
||||
@click=${this._newBackup}
|
||||
>
|
||||
${backupInProgress
|
||||
? html`<div slot="icon">
|
||||
<ha-circular-progress
|
||||
.size=${"small"}
|
||||
indeterminate
|
||||
></ha-circular-progress>
|
||||
</div>`
|
||||
: html`<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>`}
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
</hass-subpage>
|
||||
`;
|
||||
@@ -263,9 +231,6 @@ class HaConfigBackupOverview extends LitElement {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
ha-circular-progress {
|
||||
--md-sys-color-primary: var(--mdc-theme-on-secondary);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { mdiDotsVertical, mdiHarddisk, mdiOpenInNew } from "@mdi/js";
|
||||
import { mdiDotsVertical, mdiHarddisk } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -28,7 +28,6 @@ import "./components/config/ha-backup-config-encryption-key";
|
||||
import "./components/config/ha-backup-config-schedule";
|
||||
import type { BackupConfigSchedule } from "./components/config/ha-backup-config-schedule";
|
||||
import { showLocalBackupLocationDialog } from "./dialogs/show-dialog-local-backup-location";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
|
||||
@customElement("ha-config-backup-settings")
|
||||
class HaConfigBackupSettings extends LitElement {
|
||||
@@ -99,8 +98,6 @@ class HaConfigBackupSettings extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const supervisor = isComponentLoaded(this.hass, "hassio");
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/backup"
|
||||
@@ -108,7 +105,7 @@ class HaConfigBackupSettings extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.backup.settings.header")}
|
||||
>
|
||||
${supervisor
|
||||
${isComponentLoaded(this.hass, "hassio")
|
||||
? html`
|
||||
<ha-button-menu slot="toolbar-icon">
|
||||
<ha-icon-button
|
||||
@@ -206,29 +203,6 @@ class HaConfigBackupSettings extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a
|
||||
href=${documentationUrl(this.hass, "/integrations/#backup")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ha-button>
|
||||
<ha-svg-icon slot="icon" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.settings.locations.more_locations"
|
||||
)}
|
||||
</ha-button>
|
||||
</a>
|
||||
${supervisor
|
||||
? html`<a href="/config/storage">
|
||||
<ha-button>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.settings.locations.manage_network_storage"
|
||||
)}
|
||||
</ha-button>
|
||||
</a>`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
@@ -368,9 +342,6 @@ class HaConfigBackupSettings extends LitElement {
|
||||
.card-content {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -4,7 +4,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import type {
|
||||
BackupAgent,
|
||||
BackupConfig,
|
||||
BackupInfo,
|
||||
BackupContent,
|
||||
} from "../../../data/backup";
|
||||
import {
|
||||
compareAgents,
|
||||
@@ -44,7 +44,7 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
|
||||
|
||||
@state() private _manager: ManagerStateEvent = DEFAULT_MANAGER_STATE;
|
||||
|
||||
@state() private _info?: BackupInfo;
|
||||
@state() private _backups: BackupContent[] = [];
|
||||
|
||||
@state() private _agents: BackupAgent[] = [];
|
||||
|
||||
@@ -87,7 +87,8 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
|
||||
}
|
||||
|
||||
private async _fetchBackupInfo() {
|
||||
this._info = await fetchBackupInfo(this.hass);
|
||||
const info = await fetchBackupInfo(this.hass);
|
||||
this._backups = info.backups;
|
||||
}
|
||||
|
||||
private async _fetchBackupConfig() {
|
||||
@@ -133,8 +134,7 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
|
||||
pageEl.narrow = this.narrow;
|
||||
pageEl.cloudStatus = this.cloudStatus;
|
||||
pageEl.manager = this._manager;
|
||||
pageEl.info = this._info;
|
||||
pageEl.backups = this._info?.backups || [];
|
||||
pageEl.backups = this._backups;
|
||||
pageEl.config = this._config;
|
||||
pageEl.agents = this._agents;
|
||||
pageEl.fetching = this._fetching;
|
||||
|
@@ -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, SubEntry } from "../../../data/config_entries";
|
||||
import { getSubEntries, sortConfigEntries } from "../../../data/config_entries";
|
||||
import type { ConfigEntry } from "../../../data/config_entries";
|
||||
import { sortConfigEntries } from "../../../data/config_entries";
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
import type { DataTableFilters } from "../../../data/data_table_filters";
|
||||
import {
|
||||
@@ -108,8 +108,6 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ attribute: false }) public entries!: ConfigEntry[];
|
||||
|
||||
@state() private _subEntries?: SubEntry[];
|
||||
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
entities!: EntityRegistryEntry[];
|
||||
@@ -221,7 +219,6 @@ 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) {
|
||||
@@ -246,10 +243,6 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
value: configEntry ? [configEntry] : [],
|
||||
items: undefined,
|
||||
},
|
||||
sub_entry: {
|
||||
value: subEntry ? [subEntry] : [],
|
||||
items: undefined,
|
||||
},
|
||||
};
|
||||
this._filterLabel();
|
||||
}
|
||||
@@ -341,32 +334,6 @@ 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) &&
|
||||
@@ -788,15 +755,7 @@ 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]}${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}
|
||||
)?.title || this._filters.config_entry.value[0]}
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
<ha-filter-floor-areas
|
||||
@@ -929,10 +888,6 @@ 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, SubEntry } from "../../../data/config_entries";
|
||||
import { getConfigEntries, getSubEntries } from "../../../data/config_entries";
|
||||
import type { ConfigEntry } from "../../../data/config_entries";
|
||||
import { getConfigEntries } from "../../../data/config_entries";
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
import type {
|
||||
DataTableFiltersItems,
|
||||
@@ -146,8 +146,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _entries?: ConfigEntry[];
|
||||
|
||||
@state() private _subEntries?: SubEntry[];
|
||||
|
||||
@state() private _manifests?: IntegrationManifest[];
|
||||
|
||||
@state()
|
||||
@@ -355,8 +353,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
showNarrow: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
minWidth: "80px",
|
||||
maxWidth: "80px",
|
||||
template: (entry) =>
|
||||
entry.unavailable ||
|
||||
entry.disabled_by ||
|
||||
@@ -526,27 +522,6 @@ 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) &&
|
||||
@@ -929,22 +904,14 @@ ${
|
||||
</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]}${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}
|
||||
)?.title || this._filters.config_entry[0]}
|
||||
</ha-alert>`
|
||||
: nothing
|
||||
}
|
||||
@@ -1057,7 +1024,6 @@ ${
|
||||
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) {
|
||||
@@ -1070,7 +1036,6 @@ ${
|
||||
"ha-filter-states": [],
|
||||
"ha-filter-integrations": domain ? [domain] : [],
|
||||
config_entry: configEntry ? [configEntry] : [],
|
||||
sub_entry: subEntry ? [subEntry] : [],
|
||||
};
|
||||
this._filterLabel();
|
||||
}
|
||||
@@ -1128,7 +1093,6 @@ ${
|
||||
hidden_by: null,
|
||||
area_id: null,
|
||||
config_entry_id: null,
|
||||
config_subentry_id: null,
|
||||
device_id: null,
|
||||
icon: null,
|
||||
readonly: true,
|
||||
@@ -1420,10 +1384,6 @@ ${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(
|
||||
|
@@ -346,11 +346,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
groupable: true,
|
||||
},
|
||||
editable: {
|
||||
title: localize("ui.panel.config.helpers.picker.headers.editable"),
|
||||
title: "",
|
||||
label: localize("ui.panel.config.helpers.picker.headers.editable"),
|
||||
type: "icon",
|
||||
sortable: true,
|
||||
minWidth: "88px",
|
||||
maxWidth: "88px",
|
||||
showNarrow: true,
|
||||
template: (helper) => html`
|
||||
${!helper.editable
|
||||
|
@@ -654,7 +654,6 @@ class AddIntegrationDialog extends LitElement {
|
||||
startFlowHandler: domain,
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
manifest,
|
||||
navigateToResult: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -70,7 +70,7 @@ export class HaConfigFlowCard extends LitElement {
|
||||
? html`<a
|
||||
href=${this.flow.context.configuration_url.replace(
|
||||
/^homeassistant:\/\//,
|
||||
"/"
|
||||
""
|
||||
)}
|
||||
rel="noreferrer"
|
||||
target=${this.flow.context.configuration_url.startsWith(
|
||||
@@ -137,7 +137,6 @@ export class HaConfigFlowCard extends LitElement {
|
||||
}
|
||||
showConfigFlowDialog(this, {
|
||||
continueFlowId: this.flow.flow_id,
|
||||
navigateToResult: true,
|
||||
dialogClosedCallback: () => {
|
||||
this._handleFlowUpdated();
|
||||
},
|
||||
|
@@ -16,7 +16,6 @@ import {
|
||||
mdiOpenInNew,
|
||||
mdiPackageVariant,
|
||||
mdiPlayCircleOutline,
|
||||
mdiPlus,
|
||||
mdiProgressHelper,
|
||||
mdiReload,
|
||||
mdiReloadAlert,
|
||||
@@ -53,17 +52,14 @@ 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";
|
||||
@@ -110,7 +106,6 @@ 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,
|
||||
@@ -177,8 +172,6 @@ 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(
|
||||
@@ -221,18 +214,11 @@ 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() {
|
||||
@@ -587,7 +573,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}
|
||||
@@ -687,73 +673,6 @@ 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;
|
||||
@@ -801,299 +720,274 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
)}.`);
|
||||
}
|
||||
} else {
|
||||
devicesLine = this._renderDeviceLine(item, devices, services, entities);
|
||||
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],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
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>
|
||||
`
|
||||
: 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>
|
||||
</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
|
||||
? html`
|
||||
<ha-button slot="end" @click=${this._showOptions}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.configure"
|
||||
)}
|
||||
</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>
|
||||
</ha-button>
|
||||
`
|
||||
: 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>
|
||||
<ha-md-menu-item class="warning" @click=${this._handleDeleteSub}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
class="warning"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
${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.delete"
|
||||
"ui.panel.config.integrations.config_entry.rename"
|
||||
)}
|
||||
</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>`;
|
||||
}
|
||||
@@ -1115,7 +1009,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
private _continueFlow(ev) {
|
||||
showConfigFlowDialog(this, {
|
||||
continueFlowId: ev.target.flow.flow_id,
|
||||
navigateToResult: true,
|
||||
dialogClosedCallback: () => {
|
||||
// this._handleFlowUpdated();
|
||||
},
|
||||
@@ -1137,27 +1030,6 @@ 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;
|
||||
@@ -1305,49 +1177,6 @@ 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
|
||||
@@ -1555,7 +1384,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
manifest: await fetchIntegrationManifest(this.hass, configEntry.domain),
|
||||
entryId: configEntry.entry_id,
|
||||
navigateToResult: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1626,12 +1454,6 @@ 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,
|
||||
@@ -1761,9 +1583,6 @@ 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,8 +207,6 @@ 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,7 +279,6 @@ class HaDomainIntegrations extends LitElement {
|
||||
{
|
||||
startFlowHandler: domain,
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
navigateToResult: true,
|
||||
manifest: await fetchIntegrationManifest(this.hass, domain),
|
||||
}
|
||||
);
|
||||
@@ -296,7 +295,6 @@ 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),
|
||||
}
|
||||
|
@@ -76,8 +76,6 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
@state()
|
||||
private _statistics?: ZWaveJSControllerStatisticsUpdatedMessage;
|
||||
|
||||
private _dialogOpen = false;
|
||||
|
||||
protected async firstUpdated() {
|
||||
if (this.hass) {
|
||||
await this._fetchData();
|
||||
@@ -106,17 +104,11 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
),
|
||||
subscribeS2Inclusion(this.hass, this.configEntryId, (message) => {
|
||||
if (!this._dialogOpen) {
|
||||
showZWaveJSAddNodeDialog(this, {
|
||||
entry_id: this.configEntryId,
|
||||
dsk: message.dsk,
|
||||
onStop: () => {
|
||||
setTimeout(() => this._fetchData(), 100);
|
||||
this._dialogOpen = false;
|
||||
},
|
||||
});
|
||||
this._dialogOpen = true;
|
||||
}
|
||||
showZWaveJSAddNodeDialog(this, {
|
||||
entry_id: this.configEntryId,
|
||||
dsk: message.dsk,
|
||||
onStop: () => setTimeout(() => this._fetchData(), 100),
|
||||
});
|
||||
}),
|
||||
];
|
||||
}
|
||||
@@ -578,17 +570,11 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private async _addNodeClicked() {
|
||||
if (!this._dialogOpen) {
|
||||
showZWaveJSAddNodeDialog(this, {
|
||||
entry_id: this.configEntryId!,
|
||||
// refresh the data after the dialog is closed. add a small delay for the inclusion state to update
|
||||
onStop: () => {
|
||||
setTimeout(() => this._fetchData(), 100);
|
||||
this._dialogOpen = false;
|
||||
},
|
||||
});
|
||||
this._dialogOpen = true;
|
||||
}
|
||||
showZWaveJSAddNodeDialog(this, {
|
||||
entry_id: this.configEntryId!,
|
||||
// refresh the data after the dialog is closed. add a small delay for the inclusion state to update
|
||||
onStop: () => setTimeout(() => this._fetchData(), 100),
|
||||
});
|
||||
}
|
||||
|
||||
private async _removeNodeClicked() {
|
||||
|
@@ -706,7 +706,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
--data-table-border-width: 0;
|
||||
}
|
||||
:host(:not([narrow])) ha-data-table {
|
||||
height: calc(100vh - 1px - var(--header-height) - 48px);
|
||||
height: calc(100vh - 1px - var(--header-height));
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
@@ -301,6 +301,10 @@ ${type === "object"
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.edit-pane .card-content {
|
||||
user-select: initial;
|
||||
}
|
||||
|
||||
.edit-pane a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
@@ -20,31 +20,40 @@ export class HuiCardFeatures extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
${this.features.map(
|
||||
(feature) => html`
|
||||
<hui-card-feature
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
.color=${this.color}
|
||||
.feature=${feature}
|
||||
></hui-card-feature>
|
||||
`
|
||||
)}
|
||||
<div class="container">
|
||||
${this.features.map(
|
||||
(feature) => html`
|
||||
<hui-card-feature
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
.color=${this.color}
|
||||
.feature=${feature}
|
||||
></hui-card-feature>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
--feature-color: var(--state-icon-color);
|
||||
--feature-padding: 12px;
|
||||
--feature-height: 42px;
|
||||
--feature-border-radius: 12px;
|
||||
--feature-button-spacing: 12px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: var(--feature-padding);
|
||||
padding-top: 0px;
|
||||
gap: var(--feature-padding);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
@@ -327,19 +327,17 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
);
|
||||
|
||||
const untrackedConsumption: BarSeriesOption["data"] = [];
|
||||
Object.keys(consumptionData.total)
|
||||
.sort((a, b) => Number(a) - Number(b))
|
||||
.forEach((time) => {
|
||||
const ts = Number(time);
|
||||
const value =
|
||||
consumptionData.total[time] - (totalDeviceConsumption[time] || 0);
|
||||
const dataPoint: number[] = [ts, value];
|
||||
if (compare) {
|
||||
dataPoint[2] = dataPoint[0];
|
||||
dataPoint[0] = compareTransform(new Date(ts)).getTime();
|
||||
}
|
||||
untrackedConsumption.push(dataPoint);
|
||||
});
|
||||
Object.keys(consumptionData.total).forEach((time) => {
|
||||
const ts = Number(time);
|
||||
const value =
|
||||
consumptionData.total[time] - (totalDeviceConsumption[time] || 0);
|
||||
const dataPoint: number[] = [ts, value];
|
||||
if (compare) {
|
||||
dataPoint[2] = dataPoint[0];
|
||||
dataPoint[0] = compareTransform(new Date(ts)).getTime();
|
||||
}
|
||||
untrackedConsumption.push(dataPoint);
|
||||
});
|
||||
// random id to always add untracked at the end
|
||||
const order = Date.now();
|
||||
const dataset: BarSeriesOption = {
|
||||
@@ -450,15 +448,7 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
});
|
||||
});
|
||||
return sorted_devices
|
||||
.map(
|
||||
(device) =>
|
||||
data.find((d) => {
|
||||
const id = (d.id as string)
|
||||
.replace(/^compare-/, "") // Remove compare- prefix
|
||||
.replace(/-\d+$/, ""); // Remove numeric suffix
|
||||
return id === device;
|
||||
})!
|
||||
)
|
||||
.map((device) => data.find((d) => (d.id as string).includes(device))!)
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
|
@@ -27,7 +27,6 @@ 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
|
||||
@@ -110,11 +109,8 @@ export class HuiEnergyDevicesGraphCard
|
||||
return `${title}${params.marker} ${params.seriesName}: ${value}`;
|
||||
}
|
||||
|
||||
private _createOptions = memoizeOne((data: BarSeriesOption[]): ECOption => {
|
||||
const isMobile = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
return {
|
||||
private _createOptions = memoizeOne(
|
||||
(data: BarSeriesOption[]): ECOption => ({
|
||||
xAxis: {
|
||||
type: "value",
|
||||
name: "kWh",
|
||||
@@ -128,17 +124,6 @@ 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: {
|
||||
@@ -152,8 +137,8 @@ export class HuiEnergyDevicesGraphCard
|
||||
show: true,
|
||||
formatter: this._renderTooltip.bind(this),
|
||||
},
|
||||
};
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
private _getDeviceName(statisticId: string): string {
|
||||
return (
|
||||
|
@@ -300,8 +300,6 @@ export class HuiEnergyUsageGraphCard
|
||||
type: "bar",
|
||||
stack: "usage",
|
||||
data: [],
|
||||
// @ts-expect-error
|
||||
order: 0,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -317,8 +315,6 @@ export class HuiEnergyUsageGraphCard
|
||||
)
|
||||
);
|
||||
|
||||
// @ts-expect-error
|
||||
datasets.sort((a, b) => a.order - b.order);
|
||||
fillDataGapsAndRoundCaps(datasets);
|
||||
this._chartData = datasets;
|
||||
}
|
||||
@@ -486,7 +482,7 @@ export class HuiEnergyUsageGraphCard
|
||||
this._compareStart!
|
||||
);
|
||||
|
||||
Object.entries(combinedData).forEach(([type, sources], idx) => {
|
||||
Object.entries(combinedData).forEach(([type, sources]) => {
|
||||
Object.entries(sources).forEach(([statId, source]) => {
|
||||
const points: BarSeriesOption["data"] = [];
|
||||
// Process chart data.
|
||||
@@ -517,13 +513,6 @@ 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(
|
||||
|
@@ -256,7 +256,6 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
hui-card-features {
|
||||
width: 100%;
|
||||
flex: none;
|
||||
padding: 0 12px 12px 12px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
mdiDotsHexagon,
|
||||
mdiGoogleCirclesCommunities,
|
||||
mdiImageFilterCenterFocus,
|
||||
} from "@mdi/js";
|
||||
import { mdiImageFilterCenterFocus } from "@mdi/js";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import type { LatLngTuple } from "leaflet";
|
||||
import type { PropertyValues } from "lit";
|
||||
@@ -76,8 +72,6 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@state() private _error?: { code: string; message: string };
|
||||
|
||||
@state() private _clusterMarkers = true;
|
||||
|
||||
private _subscribed?: Promise<(() => Promise<void>) | undefined>;
|
||||
|
||||
public setConfig(config: MapCardConfig): void {
|
||||
@@ -176,32 +170,18 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
.autoFit=${this._config.auto_fit || false}
|
||||
.fitZones=${this._config.fit_zones}
|
||||
.themeMode=${themeMode}
|
||||
.clusterMarkers=${this._clusterMarkers}
|
||||
interactive-zones
|
||||
render-passive
|
||||
></ha-map>
|
||||
<div id="buttons">
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.map.toggle_grouping"
|
||||
)}
|
||||
.path=${this._clusterMarkers
|
||||
? mdiGoogleCirclesCommunities
|
||||
: mdiDotsHexagon}
|
||||
style=${isDarkMode ? "color:#ffffff" : "color:#000000"}
|
||||
@click=${this._toggleClusterMarkers}
|
||||
tabindex="0"
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.map.reset_focus"
|
||||
)}
|
||||
.path=${mdiImageFilterCenterFocus}
|
||||
style=${isDarkMode ? "color:#ffffff" : "color:#000000"}
|
||||
@click=${this._fitMap}
|
||||
tabindex="0"
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.map.reset_focus"
|
||||
)}
|
||||
.path=${mdiImageFilterCenterFocus}
|
||||
style=${isDarkMode ? "color:#ffffff" : "color:#000000"}
|
||||
@click=${this._fitMap}
|
||||
tabindex="0"
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
@@ -340,10 +320,6 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
this._map?.fitMap();
|
||||
}
|
||||
|
||||
private _toggleClusterMarkers() {
|
||||
this._clusterMarkers = !this._clusterMarkers;
|
||||
}
|
||||
|
||||
private _getColor(entityId: string): string {
|
||||
let color = this._colorDict[entityId];
|
||||
if (color) {
|
||||
@@ -488,12 +464,11 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#buttons {
|
||||
ha-icon-button {
|
||||
position: absolute;
|
||||
top: 75px;
|
||||
left: 3px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#root {
|
||||
|
@@ -3,21 +3,17 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import hash from "object-hash";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-alert";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/ha-alert";
|
||||
import type { RenderTemplateResult } from "../../../data/ws-templates";
|
||||
import { subscribeRenderTemplate } from "../../../data/ws-templates";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { CacheManager } from "../../../util/cache-manager";
|
||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import type { MarkdownCardConfig } from "./types";
|
||||
|
||||
const templateCache = new CacheManager<RenderTemplateResult>(1000);
|
||||
|
||||
@customElement("hui-markdown-card")
|
||||
export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
@@ -72,32 +68,9 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
||||
this._tryConnect();
|
||||
}
|
||||
|
||||
private _computeCacheKey() {
|
||||
return hash(this._config);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._tryDisconnect();
|
||||
|
||||
if (this._config && this._templateResult) {
|
||||
const key = this._computeCacheKey();
|
||||
templateCache.set(key, this._templateResult);
|
||||
}
|
||||
}
|
||||
|
||||
protected willUpdate(_changedProperties: PropertyValues): void {
|
||||
super.willUpdate(_changedProperties);
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._templateResult) {
|
||||
const key = this._computeCacheKey();
|
||||
if (templateCache.has(key)) {
|
||||
this._templateResult = templateCache.get(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -114,7 +87,6 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
||||
: nothing}
|
||||
<ha-card .header=${this._config.title}>
|
||||
<ha-markdown
|
||||
cache
|
||||
breaks
|
||||
class=${classMap({
|
||||
"no-header": !this._config.title,
|
||||
@@ -135,7 +107,7 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
||||
this._tryConnect();
|
||||
}
|
||||
const shouldBeHidden =
|
||||
!!this._templateResult &&
|
||||
this._templateResult &&
|
||||
this._config.show_empty === false &&
|
||||
this._templateResult.result.length === 0;
|
||||
if (shouldBeHidden !== this.hidden) {
|
||||
|
@@ -327,7 +327,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
|
||||
);
|
||||
const endDate = this._energyEnd;
|
||||
try {
|
||||
let unitClass: string | undefined | null;
|
||||
let unitClass;
|
||||
if (this._config!.unit && this._metadata) {
|
||||
const metadata = Object.values(this._metadata).find(
|
||||
(metaData) =>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user