mirror of
https://github.com/home-assistant/frontend.git
synced 2026-05-18 23:27:09 +00:00
Compare commits
100 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 188e82fa02 | |||
| a438fc5e41 | |||
| 783132ae46 | |||
| 680d81001c | |||
| a917383d7a | |||
| 455a6761cd | |||
| acf42d7637 | |||
| 3857c7321a | |||
| 5eec814988 | |||
| edd37565a6 | |||
| fb3f779121 | |||
| 4d7634ac67 | |||
| ba5c1133c6 | |||
| 0a05dd8f71 | |||
| 400106ec09 | |||
| a7a4194e09 | |||
| 0bd7d27c57 | |||
| 8175e45921 | |||
| cae36b393b | |||
| f84ad92356 | |||
| fb1ee2ed1d | |||
| 9073282174 | |||
| 91bd5cba08 | |||
| a68bdbfe08 | |||
| f3d614b0d3 | |||
| f3c9e4a4a0 | |||
| d22a82c4a6 | |||
| 5cddc6e5c6 | |||
| c5c067ef19 | |||
| 694bb3088c | |||
| ad487470fd | |||
| 2fba41b8ca | |||
| 2801d071ba | |||
| 71b65f208f | |||
| ab4efb7412 | |||
| c7a46ec25b | |||
| 25a14c87a5 | |||
| 83d4a408f6 | |||
| 06932d1479 | |||
| 24211d5f25 | |||
| d387f19a31 | |||
| 347ee2a4c3 | |||
| 1363884773 | |||
| 0256da511d | |||
| c52217c1ce | |||
| cdd17eed2e | |||
| 4546c6f624 | |||
| 2c34760204 | |||
| 0b64861297 | |||
| 94a5e737cc | |||
| 18b2360e46 | |||
| 05163588fc | |||
| ee64536862 | |||
| 695a6a506e | |||
| c1a214d1af | |||
| 6123d932e1 | |||
| a3dcf77f2a | |||
| 7d7f8a9bc2 | |||
| 3ee3cfa6cb | |||
| 00d0cb7afa | |||
| 3ae34403bd | |||
| 1434966170 | |||
| 8dd70f7017 | |||
| 84a0289e1b | |||
| a25e1d3f7f | |||
| f53ac41eee | |||
| b9acd40b0f | |||
| 7524dc8709 | |||
| cbedf62c39 | |||
| 63a98155cd | |||
| 7369b7e0d5 | |||
| 922abafabf | |||
| f1bb4a5694 | |||
| e0b9cb8ccb | |||
| 06f27650da | |||
| a772eaffd7 | |||
| c39be4a9b8 | |||
| 0abccb88d6 | |||
| 5dc5879773 | |||
| 41df7a3f4a | |||
| 920ec035c5 | |||
| 043e8d6e2e | |||
| d8e36894a0 | |||
| 65b6a3c6a3 | |||
| b16f82cedb | |||
| 02deeb4ce7 | |||
| 0c6651c2c2 | |||
| abbf56db1d | |||
| bc0cc8b387 | |||
| b66f41db7d | |||
| 05fbe204c5 | |||
| ee199fbbc0 | |||
| 56ab29da81 | |||
| 10abaa538d | |||
| f25dac7f68 | |||
| 99065a689f | |||
| ac88d5993a | |||
| b09ce45d31 | |||
| 78e2809fe7 | |||
| a631bf9854 |
@@ -5,7 +5,7 @@
|
||||
"context": ".."
|
||||
},
|
||||
"appPort": "8124:8123",
|
||||
"postCreateCommand": "sudo apt update && sudo apt upgrade -y && sudo apt install -y libpcap-dev",
|
||||
"postCreateCommand": "./.devcontainer/post_create.sh",
|
||||
"postStartCommand": "script/bootstrap",
|
||||
"containerEnv": {
|
||||
"DEV_CONTAINER": "1",
|
||||
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/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."
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Build resources
|
||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||
- name: Setup lint cache
|
||||
uses: actions/cache@v4.2.0
|
||||
uses: actions/cache@v4.2.1
|
||||
with:
|
||||
path: |
|
||||
node_modules/.cache/prettier
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@v4.6.0
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: frontend-bundle-stats
|
||||
path: build/stats/*.json
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@v4.6.0
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: supervisor-bundle-stats
|
||||
path: build/stats/*.json
|
||||
|
||||
@@ -57,14 +57,14 @@ jobs:
|
||||
run: tar -czvf translations.tar.gz translations
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4.6.0
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: wheels
|
||||
path: dist/home_assistant_frontend*.whl
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@v4.6.0
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
|
||||
Vendored
+42
@@ -1,6 +1,42 @@
|
||||
{
|
||||
"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",
|
||||
@@ -241,6 +277,12 @@
|
||||
"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 [
|
||||
...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 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",
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
@@ -90,6 +90,14 @@ 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/")
|
||||
|
||||
+17
-15
@@ -1,11 +1,16 @@
|
||||
// @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);
|
||||
@@ -15,17 +20,14 @@ const compat = new FlatCompat({
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
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"
|
||||
),
|
||||
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"],
|
||||
{
|
||||
plugins: {
|
||||
"unused-imports": unusedImports,
|
||||
@@ -43,7 +45,7 @@ export default [
|
||||
Polymer: true,
|
||||
},
|
||||
|
||||
parser: tsParser,
|
||||
parser: tseslint.parser,
|
||||
ecmaVersion: 2020,
|
||||
sourceType: "module",
|
||||
|
||||
@@ -184,5 +186,5 @@ export default [
|
||||
],
|
||||
"no-use-before-define": "off",
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// @ts-check
|
||||
|
||||
import tseslint from "typescript-eslint";
|
||||
import rootConfig from "../eslint.config.mjs";
|
||||
|
||||
export default [
|
||||
...rootConfig,
|
||||
{
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
},
|
||||
export default tseslint.config(...rootConfig, {
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import "../../../src/components/ha-alert";
|
||||
import {
|
||||
ALTERNATIVE_DNS_SERVERS,
|
||||
getSupervisorNetworkInfo,
|
||||
pingSupervisor,
|
||||
setSupervisorNetworkDns,
|
||||
} from "../data/supervisor";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
@@ -85,7 +86,28 @@ class LandingPageNetwork extends LitElement {
|
||||
|
||||
protected firstUpdated(_changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(_changedProperties);
|
||||
this._fetchSupervisorInfo();
|
||||
this._pingSupervisor();
|
||||
}
|
||||
|
||||
private _schedulePingSupervisor() {
|
||||
setTimeout(
|
||||
() => this._pingSupervisor(),
|
||||
SCHEDULE_FETCH_NETWORK_INFO_SECONDS * 1000
|
||||
);
|
||||
}
|
||||
|
||||
private async _pingSupervisor() {
|
||||
try {
|
||||
const response = await pingSupervisor();
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to ping supervisor, assume update in progress");
|
||||
}
|
||||
this._fetchSupervisorInfo();
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
this._schedulePingSupervisor();
|
||||
}
|
||||
}
|
||||
|
||||
private _scheduleFetchSupervisorInfo() {
|
||||
|
||||
@@ -18,7 +18,7 @@ export const ALTERNATIVE_DNS_SERVERS: {
|
||||
];
|
||||
|
||||
export async function getSupervisorLogs(lines = 100) {
|
||||
return fetch(`/supervisor/supervisor/logs?lines=${lines}`, {
|
||||
return fetch(`/supervisor-api/supervisor/logs?lines=${lines}`, {
|
||||
headers: {
|
||||
Accept: "text/plain",
|
||||
},
|
||||
@@ -26,22 +26,26 @@ export async function getSupervisorLogs(lines = 100) {
|
||||
}
|
||||
|
||||
export async function getSupervisorLogsFollow(lines = 500) {
|
||||
return fetch(`/supervisor/supervisor/logs/follow?lines=${lines}`, {
|
||||
return fetch(`/supervisor-api/supervisor/logs/follow?lines=${lines}`, {
|
||||
headers: {
|
||||
Accept: "text/plain",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function pingSupervisor() {
|
||||
return fetch("/supervisor-api/supervisor/ping");
|
||||
}
|
||||
|
||||
export async function getSupervisorNetworkInfo() {
|
||||
return fetch("/supervisor/network/info");
|
||||
return fetch("/supervisor-api/network/info");
|
||||
}
|
||||
|
||||
export const setSupervisorNetworkDns = async (
|
||||
dnsServerIndex: number,
|
||||
primaryInterface: string
|
||||
) =>
|
||||
fetch(`/supervisor/network/interface/${primaryInterface}/update`, {
|
||||
fetch(`/supervisor-api/network/interface/${primaryInterface}/update`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
ipv4: {
|
||||
|
||||
+42
-39
@@ -26,25 +26,25 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.26.7",
|
||||
"@babel/runtime": "7.26.9",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@codemirror/autocomplete": "6.18.4",
|
||||
"@codemirror/autocomplete": "6.18.6",
|
||||
"@codemirror/commands": "6.8.0",
|
||||
"@codemirror/language": "6.10.8",
|
||||
"@codemirror/legacy-modes": "6.4.2",
|
||||
"@codemirror/search": "6.5.8",
|
||||
"@codemirror/legacy-modes": "6.4.3",
|
||||
"@codemirror/search": "6.5.9",
|
||||
"@codemirror/state": "6.5.2",
|
||||
"@codemirror/view": "6.36.2",
|
||||
"@codemirror/view": "6.36.3",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.17.2",
|
||||
"@formatjs/intl-displaynames": "6.8.9",
|
||||
"@formatjs/intl-durationformat": "0.7.2",
|
||||
"@formatjs/intl-datetimeformat": "6.17.3",
|
||||
"@formatjs/intl-displaynames": "6.8.10",
|
||||
"@formatjs/intl-durationformat": "0.7.3",
|
||||
"@formatjs/intl-getcanonicallocales": "2.5.4",
|
||||
"@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",
|
||||
"@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",
|
||||
"@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.7",
|
||||
"@lit-labs/observers": "2.0.4",
|
||||
"@lit-labs/virtualizer": "2.0.15",
|
||||
"@lit-labs/motion": "1.0.8",
|
||||
"@lit-labs/observers": "2.0.5",
|
||||
"@lit-labs/virtualizer": "2.1.0",
|
||||
"@lrnwebcomponents/simple-tooltip": "8.0.2",
|
||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||
@@ -90,9 +90,10 @@
|
||||
"@polymer/paper-tabs": "3.1.0",
|
||||
"@polymer/polymer": "3.5.2",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@shoelace-style/shoelace": "2.20.0",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.6.4",
|
||||
"@vaadin/vaadin-themable-mixin": "24.6.4",
|
||||
"@vaadin/combo-box": "24.6.5",
|
||||
"@vaadin/vaadin-themable-mixin": "24.6.5",
|
||||
"@vibrant/color": "4.0.0",
|
||||
"@vue/web-component-wrapper": "1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
||||
@@ -116,16 +117,18 @@
|
||||
"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.14",
|
||||
"intl-messageformat": "10.7.15",
|
||||
"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.6",
|
||||
"marked": "15.0.7",
|
||||
"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",
|
||||
@@ -137,7 +140,7 @@
|
||||
"tinykeys": "3.0.0",
|
||||
"tsparticles-engine": "2.12.0",
|
||||
"tsparticles-preset-links": "2.12.0",
|
||||
"ua-parser-js": "2.0.1",
|
||||
"ua-parser-js": "2.0.2",
|
||||
"vis-data": "7.1.9",
|
||||
"vis-network": "9.1.9",
|
||||
"vue": "2.7.16",
|
||||
@@ -152,20 +155,20 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.26.7",
|
||||
"@babel/core": "7.26.9",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.3",
|
||||
"@babel/plugin-proposal-decorators": "7.25.9",
|
||||
"@babel/plugin-transform-runtime": "7.25.9",
|
||||
"@babel/preset-env": "7.26.7",
|
||||
"@babel/plugin-transform-runtime": "7.26.9",
|
||||
"@babel/preset-env": "7.26.9",
|
||||
"@babel/preset-typescript": "7.26.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.18.2",
|
||||
"@lokalise/node-api": "13.0.0",
|
||||
"@octokit/auth-oauth-device": "7.1.2",
|
||||
"@octokit/plugin-retry": "7.1.3",
|
||||
"@octokit/rest": "21.1.0",
|
||||
"@lokalise/node-api": "13.2.0",
|
||||
"@octokit/auth-oauth-device": "7.1.3",
|
||||
"@octokit/plugin-retry": "7.1.4",
|
||||
"@octokit/rest": "21.1.1",
|
||||
"@rsdoctor/rspack-plugin": "0.4.13",
|
||||
"@rspack/cli": "1.2.2",
|
||||
"@rspack/core": "1.2.2",
|
||||
"@rspack/cli": "1.2.5",
|
||||
"@rspack/core": "1.2.5",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.21",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
@@ -175,6 +178,7 @@
|
||||
"@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",
|
||||
@@ -183,14 +187,12 @@
|
||||
"@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",
|
||||
"@vitest/coverage-v8": "3.0.6",
|
||||
"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.0",
|
||||
"eslint": "9.20.1",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.0.1",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
@@ -198,7 +200,7 @@
|
||||
"eslint-plugin-lit": "1.15.0",
|
||||
"eslint-plugin-lit-a11y": "4.1.4",
|
||||
"eslint-plugin-unused-imports": "4.1.4",
|
||||
"eslint-plugin-wc": "2.2.0",
|
||||
"eslint-plugin-wc": "2.2.1",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.0",
|
||||
"glob": "11.0.1",
|
||||
@@ -215,9 +217,8 @@
|
||||
"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.4.2",
|
||||
"prettier": "3.5.1",
|
||||
"rspack-manifest-plugin": "5.0.3",
|
||||
"serve": "14.2.4",
|
||||
"sinon": "19.0.2",
|
||||
@@ -225,7 +226,9 @@
|
||||
"terser-webpack-plugin": "5.3.11",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.7.3",
|
||||
"vitest": "3.0.5",
|
||||
"typescript-eslint": "8.24.1",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "3.0.6",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
||||
@@ -239,7 +242,7 @@
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "1.6.3",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"globals": "15.14.0",
|
||||
"globals": "16.0.0",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"packageManager": "yarn@4.6.0"
|
||||
|
||||
@@ -136,11 +136,18 @@ export function theme2hex(themeColor: string): string {
|
||||
}
|
||||
|
||||
const rgbFromColorName = colors[themeColor];
|
||||
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;
|
||||
if (rgbFromColorName) {
|
||||
return rgb2hex(rgbFromColorName);
|
||||
}
|
||||
return rgb2hex(rgbFromColorName);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -16,11 +16,30 @@ 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,2 +1,2 @@
|
||||
export const computeDomain = (entityId: string): string =>
|
||||
entityId.substr(0, entityId.indexOf("."));
|
||||
entityId.substring(0, entityId.indexOf("."));
|
||||
|
||||
@@ -120,11 +120,6 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
return value;
|
||||
}
|
||||
|
||||
if (domain === "datetime") {
|
||||
const time = new Date(state);
|
||||
return formatDateTime(time, locale, config);
|
||||
}
|
||||
|
||||
if (["date", "input_datetime", "time"].includes(domain)) {
|
||||
// If trying to display an explicit state, need to parse the explicit state to `Date` then format.
|
||||
// Attributes aren't available, we have to use `state`.
|
||||
@@ -181,6 +176,7 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
"tag",
|
||||
"tts",
|
||||
"wake_word",
|
||||
"datetime",
|
||||
].includes(domain) ||
|
||||
(domain === "sensor" && attributes.device_class === "timestamp")
|
||||
) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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: [
|
||||
@@ -237,6 +240,7 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
|
||||
};
|
||||
|
||||
export const getStates = (
|
||||
hass: HomeAssistant,
|
||||
state: HassEntity,
|
||||
attribute: string | undefined = undefined
|
||||
): string[] => {
|
||||
@@ -269,7 +273,19 @@ export const getStates = (
|
||||
case "device_tracker":
|
||||
case "person":
|
||||
if (!attribute) {
|
||||
result.push("home", "not_home");
|
||||
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)
|
||||
)
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "event":
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ 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;
|
||||
|
||||
@@ -67,12 +68,16 @@ 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() {
|
||||
@@ -83,19 +88,19 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
this._listeners.push(
|
||||
listenMediaQuery("(prefers-reduced-motion)", (matches) => {
|
||||
this._reducedMotion = matches;
|
||||
this.chart?.setOption({ animation: !this._reducedMotion });
|
||||
if (this._reducedMotion !== matches) {
|
||||
this._reducedMotion = matches;
|
||||
this._setChartOptions({ animation: !this._reducedMotion });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Add keyboard event listeners
|
||||
const handleKeyDown = (ev: KeyboardEvent) => {
|
||||
if ((isMac && ev.metaKey) || (!isMac && ev.ctrlKey)) {
|
||||
if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
|
||||
this._modifierPressed = true;
|
||||
if (!this.options?.dataZoom) {
|
||||
this.chart?.setOption({
|
||||
dataZoom: this._getDataZoomConfig(),
|
||||
});
|
||||
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -104,9 +109,7 @@ export class HaChartBase extends LitElement {
|
||||
if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
|
||||
this._modifierPressed = false;
|
||||
if (!this.options?.dataZoom) {
|
||||
this.chart?.setOption({
|
||||
dataZoom: this._getDataZoomConfig(),
|
||||
});
|
||||
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -124,27 +127,24 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this.hasUpdated || !this.chart) {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
if (changedProps.has("_themes")) {
|
||||
this._setupChart();
|
||||
return;
|
||||
}
|
||||
let chartOptions: ECOption = {};
|
||||
if (changedProps.has("data")) {
|
||||
this.chart.setOption(
|
||||
{ series: this.data },
|
||||
{ lazyUpdate: true, replaceMerge: ["series"] }
|
||||
);
|
||||
chartOptions.series = this.data;
|
||||
}
|
||||
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"],
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,6 @@ export class HaChartBase extends LitElement {
|
||||
style=${styleMap({
|
||||
height: this.height ?? `${this._getDefaultHeight()}px`,
|
||||
})}
|
||||
@wheel=${this._handleWheel}
|
||||
>
|
||||
<div class="chart"></div>
|
||||
${this._isZoomed
|
||||
@@ -240,8 +239,8 @@ export class HaChartBase extends LitElement {
|
||||
type: "inside",
|
||||
orient: "horizontal",
|
||||
filterMode: "none",
|
||||
moveOnMouseMove: this._isZoomed,
|
||||
preventDefaultMouseMove: this._isZoomed,
|
||||
moveOnMouseMove: !this._isTouchDevice || this._isZoomed,
|
||||
preventDefaultMouseMove: !this._isTouchDevice || this._isZoomed,
|
||||
zoomLock: !this._isTouchDevice && !this._modifierPressed,
|
||||
};
|
||||
}
|
||||
@@ -512,25 +511,33 @@ export class HaChartBase extends LitElement {
|
||||
return Math.max(this.clientWidth / 2, 200);
|
||||
}
|
||||
|
||||
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(),
|
||||
});
|
||||
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) {
|
||||
// delay the last bit of the render to avoid blocking the main thread
|
||||
// this is not that impactful with sampling enabled but it doesn't hurt to have it
|
||||
const zr = this.chart.getZr();
|
||||
this._originalZrFlush = zr.flush;
|
||||
zr.flush = () => {
|
||||
setTimeout(() => {
|
||||
this._originalZrFlush?.call(zr);
|
||||
}, 5);
|
||||
};
|
||||
}
|
||||
}
|
||||
const replaceMerge = options.series ? ["series"] : [];
|
||||
this.chart.setOption(options, { replaceMerge });
|
||||
}
|
||||
|
||||
private _handleZoomReset() {
|
||||
this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
|
||||
@@ -75,6 +75,8 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
@state() private _yWidth = 25;
|
||||
|
||||
@state() private _visualMap?: VisualMapComponentOption[];
|
||||
|
||||
private _chartTime: Date = new Date();
|
||||
|
||||
protected render() {
|
||||
@@ -92,7 +94,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTooltip(params: any) {
|
||||
private _renderTooltip = (params: any) => {
|
||||
const time = params[0].axisValue;
|
||||
const title =
|
||||
formatDateTimeWithSeconds(
|
||||
@@ -115,7 +117,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;
|
||||
let lastData: any;
|
||||
const data = dataset.data || [];
|
||||
for (let i = data.length - 1; i >= 0; i--) {
|
||||
const point = data[i];
|
||||
@@ -175,7 +177,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
})
|
||||
.join("<br>")
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private _datasetHidden(ev: CustomEvent) {
|
||||
this._hiddenStats.add(ev.detail.name);
|
||||
@@ -208,8 +210,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);
|
||||
@@ -280,37 +282,11 @@ export class StateHistoryChartLine extends LitElement {
|
||||
right: rtl ? Math.max(this.paddingYAxis, this._yWidth) : 1,
|
||||
bottom: 30,
|
||||
},
|
||||
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[],
|
||||
visualMap: this._visualMap,
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
appendTo: document.body,
|
||||
formatter: this._renderTooltip.bind(this),
|
||||
formatter: this._renderTooltip,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -378,9 +354,10 @@ export class StateHistoryChartLine extends LitElement {
|
||||
name: nameY,
|
||||
color,
|
||||
symbol: "circle",
|
||||
step: "end",
|
||||
animationDurationUpdate: 0,
|
||||
symbolSize: 1,
|
||||
step: "end",
|
||||
sampling: "minmax",
|
||||
animationDurationUpdate: 0,
|
||||
lineStyle: {
|
||||
width: fill ? 0 : 1.5,
|
||||
},
|
||||
@@ -725,6 +702,33 @@ 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,11 +273,13 @@ export class StatisticsChart extends LitElement {
|
||||
this._chartOptions = {
|
||||
xAxis: [
|
||||
{
|
||||
id: "xAxis",
|
||||
type: "time",
|
||||
min: startTime,
|
||||
max: endTime,
|
||||
max: this.endTime,
|
||||
},
|
||||
{
|
||||
id: "hiddenAxis",
|
||||
type: "time",
|
||||
show: false,
|
||||
},
|
||||
@@ -368,7 +370,6 @@ export class StatisticsChart extends LitElement {
|
||||
if (endTime > new Date()) {
|
||||
endTime = new Date();
|
||||
}
|
||||
this.endTime = endTime;
|
||||
|
||||
let unit: string | undefined | null;
|
||||
|
||||
@@ -491,8 +492,8 @@ export class StatisticsChart extends LitElement {
|
||||
: this.hass.localize(
|
||||
`ui.components.statistics_charts.statistic_types.${type}`
|
||||
),
|
||||
symbol: "circle",
|
||||
symbolSize: 0,
|
||||
symbol: "none",
|
||||
sampling: "minmax",
|
||||
animationDurationUpdate: 0,
|
||||
lineStyle: {
|
||||
width: 1.5,
|
||||
@@ -510,7 +511,6 @@ export class StatisticsChart extends LitElement {
|
||||
if (band && this.chartType === "line") {
|
||||
series.stack = `band-${statistic_id}`;
|
||||
series.stackStrategy = "all";
|
||||
(series as LineSeriesOption).symbol = "none";
|
||||
if (drawBands && type === "max") {
|
||||
(series as LineSeriesOption).areaStyle = {
|
||||
color: color + "3F",
|
||||
|
||||
@@ -448,6 +448,7 @@ 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(stateObj, this.attribute).map((key) => ({
|
||||
? getStates(this.hass, stateObj, this.attribute).map((key) => ({
|
||||
value: key,
|
||||
label: !this.attribute
|
||||
? this.hass.formatEntityState(stateObj, key)
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { mdiContentCopy, mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
|
||||
import "./ha-button";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { copyToClipboard } from "../common/util/copy-clipboard";
|
||||
import { showToast } from "../util/toast";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
@customElement("ha-copy-textfield")
|
||||
export class HaCopyTextfield extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: "value" }) public value!: string;
|
||||
|
||||
@property({ attribute: "masked-value" }) public maskedValue?: string;
|
||||
|
||||
@property({ attribute: "label" }) public label?: string;
|
||||
|
||||
@state() private _showMasked = true;
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="textfield-container">
|
||||
<ha-textfield
|
||||
.value=${this._showMasked && this.maskedValue
|
||||
? this.maskedValue
|
||||
: this.value}
|
||||
readonly
|
||||
.suffix=${this.maskedValue
|
||||
? html`<div style="width: 24px"></div>`
|
||||
: nothing}
|
||||
@click=${this._focusInput}
|
||||
></ha-textfield>
|
||||
${this.maskedValue
|
||||
? html`<ha-icon-button
|
||||
class="toggle-unmasked"
|
||||
.label=${this.hass.localize(
|
||||
`ui.common.${this._showMasked ? "show" : "hide"}`
|
||||
)}
|
||||
@click=${this._toggleMasked}
|
||||
.path=${this._showMasked ? mdiEye : mdiEyeOff}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-button @click=${this._copy} unelevated>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
${this.label || this.hass.localize("ui.common.copy")}
|
||||
</ha-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _focusInput(ev) {
|
||||
const inputElement = ev.currentTarget as HaTextField;
|
||||
inputElement.select();
|
||||
}
|
||||
|
||||
private _toggleMasked(): void {
|
||||
this._showMasked = !this._showMasked;
|
||||
}
|
||||
|
||||
private async _copy(): Promise<void> {
|
||||
await copyToClipboard(this.value);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.textfield-container {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.textfield-container ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.toggle-unmasked {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-copy-textfield": HaCopyTextfield;
|
||||
}
|
||||
}
|
||||
@@ -5,15 +5,16 @@ 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";
|
||||
@@ -178,6 +179,96 @@ 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
|
||||
),
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
@@ -395,44 +486,55 @@ export class HaDateRangePicker extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-icon-button {
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
ha-icon-button {
|
||||
direction: var(--direction);
|
||||
.date-range-inputs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
|
||||
.date-range-inputs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
}
|
||||
@media only screen and (max-width: 800px) {
|
||||
.date-range-ranges {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
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;
|
||||
}
|
||||
|
||||
.date-range-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 8px;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
:host([header-position]) .date-range-ranges {
|
||||
max-height: calc(90vh - 430px);
|
||||
}
|
||||
|
||||
ha-textarea {
|
||||
display: inline-block;
|
||||
width: 340px;
|
||||
}
|
||||
@media only screen and (max-width: 460px) {
|
||||
ha-textarea {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
.date-range-ranges {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -80,7 +80,6 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
if (!this.isPassword) return nothing;
|
||||
return html`
|
||||
<ha-icon-button
|
||||
toggles
|
||||
.label=${this.localize?.(
|
||||
`${this.localizeBaseKey}.${
|
||||
this.unmaskedPassword ? "hide_password" : "show_password"
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
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:
|
||||
@@ -26,6 +31,16 @@ 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;
|
||||
}
|
||||
@@ -37,6 +52,24 @@ 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,6 +13,8 @@ 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;
|
||||
@@ -23,6 +25,7 @@ export class HaMarkdown extends LitElement {
|
||||
.allowSvg=${this.allowSvg}
|
||||
.breaks=${this.breaks}
|
||||
.lazyImages=${this.lazyImages}
|
||||
.cache=${this.cache}
|
||||
></ha-markdown-element>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -132,7 +132,6 @@ export class HaPasswordField extends LitElement {
|
||||
@change=${this._handleChangeEvent}
|
||||
></ha-textfield>
|
||||
<ha-icon-button
|
||||
toggles
|
||||
.label=${this.hass?.localize(
|
||||
this._unmaskedPassword
|
||||
? "ui.components.selectors.text.hide_password"
|
||||
|
||||
@@ -95,7 +95,6 @@ export class HaTextSelector extends LitElement {
|
||||
></ha-textfield>
|
||||
${this.selector.text?.type === "password"
|
||||
? html`<ha-icon-button
|
||||
toggles
|
||||
.label=${this.hass?.localize(
|
||||
this._unmaskedPassword
|
||||
? "ui.components.selectors.text.hide_password"
|
||||
|
||||
+198
-51
@@ -1,4 +1,5 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@shoelace-style/shoelace/dist/components/skeleton/skeleton";
|
||||
import {
|
||||
mdiBell,
|
||||
mdiCalendar,
|
||||
@@ -49,6 +50,10 @@ import "./ha-sortable";
|
||||
import "./ha-svg-icon";
|
||||
import "./user/ha-user-badge";
|
||||
import { preventDefault } from "../common/dom/prevent_default";
|
||||
import {
|
||||
saveSidebarPreferences,
|
||||
subscribeSidebarPreferences,
|
||||
} from "../data/sidebar";
|
||||
|
||||
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
|
||||
|
||||
@@ -207,30 +212,40 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _unsubPersistentNotifications: UnsubscribeFunc | undefined;
|
||||
|
||||
@storage({
|
||||
key: "sidebarPanelOrder",
|
||||
state: true,
|
||||
subscribe: true,
|
||||
})
|
||||
private _panelOrder: string[] = [];
|
||||
@storage({ key: "sidebarPanelOrder", state: true, subscribe: true })
|
||||
private _devicePanelOrder?: string[];
|
||||
|
||||
@storage({
|
||||
key: "sidebarHiddenPanels",
|
||||
state: true,
|
||||
subscribe: true,
|
||||
})
|
||||
private _hiddenPanels: string[] = [];
|
||||
@storage({ key: "sidebarHiddenPanels", state: true, subscribe: true })
|
||||
private _deviceHiddenPanels?: string[];
|
||||
|
||||
@state()
|
||||
private _userPanelOrder: string[] = [];
|
||||
|
||||
@state()
|
||||
private _userHiddenPanels: string[] = [];
|
||||
|
||||
@state()
|
||||
private _loadingUserPreferences = true;
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return this.hass.user?.is_admin
|
||||
? [
|
||||
subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => {
|
||||
this._issuesCount = repairs.issues.filter(
|
||||
(issue) => !issue.ignored
|
||||
).length;
|
||||
}),
|
||||
]
|
||||
: [];
|
||||
const subscribeFunctions = [
|
||||
subscribeSidebarPreferences(this.hass, (sidebar) => {
|
||||
this._userPanelOrder = sidebar?.panelOrder || [];
|
||||
this._userHiddenPanels = sidebar?.hiddenPanels || [];
|
||||
this._loadingUserPreferences = false;
|
||||
}),
|
||||
];
|
||||
if (this.hass.user?.is_admin) {
|
||||
subscribeFunctions.push(
|
||||
subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => {
|
||||
this._issuesCount = repairs.issues.filter(
|
||||
(issue) => !issue.ignored
|
||||
).length;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return subscribeFunctions;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -260,8 +275,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
changedProps.has("_updatesCount") ||
|
||||
changedProps.has("_issuesCount") ||
|
||||
changedProps.has("_notifications") ||
|
||||
changedProps.has("_hiddenPanels") ||
|
||||
changedProps.has("_panelOrder")
|
||||
changedProps.has("_devicePanelOrder") ||
|
||||
changedProps.has("_deviceHiddenPanels") ||
|
||||
changedProps.has("_userPanelOrder") ||
|
||||
changedProps.has("_userHiddenPanels")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -381,12 +398,51 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _getPanelPreferencesMemoized = memoizeOne(
|
||||
(
|
||||
userPanelOrder: string[],
|
||||
userHiddenPanels: string[],
|
||||
userPreferencesLoading: boolean,
|
||||
devicePanelOrder?: string[],
|
||||
deviceHiddenPanels?: string[]
|
||||
): { panelOrder: string[]; hiddenPanels: string[]; loading: boolean } => {
|
||||
let panelOrder = userPanelOrder ?? [];
|
||||
let hiddenPanels = userHiddenPanels ?? [];
|
||||
|
||||
let loading = userPreferencesLoading;
|
||||
|
||||
if (devicePanelOrder || deviceHiddenPanels) {
|
||||
panelOrder = devicePanelOrder ?? [];
|
||||
hiddenPanels = deviceHiddenPanels ?? [];
|
||||
loading = false;
|
||||
}
|
||||
|
||||
return {
|
||||
panelOrder,
|
||||
hiddenPanels,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
private _getPanelPreferences() {
|
||||
return this._getPanelPreferencesMemoized(
|
||||
this._userPanelOrder,
|
||||
this._userHiddenPanels,
|
||||
this._loadingUserPreferences,
|
||||
this._devicePanelOrder,
|
||||
this._deviceHiddenPanels
|
||||
);
|
||||
}
|
||||
|
||||
private _renderAllPanels() {
|
||||
const { panelOrder, hiddenPanels, loading } = this._getPanelPreferences();
|
||||
|
||||
const [beforeSpacer, afterSpacer] = computePanels(
|
||||
this.hass.panels,
|
||||
this.hass.defaultPanel,
|
||||
this._panelOrder,
|
||||
this._hiddenPanels,
|
||||
panelOrder,
|
||||
hiddenPanels,
|
||||
this.hass.locale
|
||||
);
|
||||
|
||||
@@ -407,12 +463,21 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
@keydown=${this._listboxKeydown}
|
||||
@iron-activate=${preventDefault}
|
||||
>
|
||||
${this.editMode
|
||||
? this._renderPanelsEdit(beforeSpacer)
|
||||
: this._renderPanels(beforeSpacer)}
|
||||
${this._renderSpacer()}
|
||||
${this._renderPanels(afterSpacer)}
|
||||
${this._renderExternalConfiguration()}
|
||||
${loading ? html`
|
||||
<div class="loading">
|
||||
<sl-skeleton effect="sheen"></sl-skeleton>
|
||||
<sl-skeleton effect="sheen"></sl-skeleton>
|
||||
<sl-skeleton effect="sheen"></sl-skeleton>
|
||||
<sl-skeleton effect="sheen"></sl-skeleton>
|
||||
</div>
|
||||
` : html`
|
||||
${this.editMode
|
||||
? this._renderPanelsEdit(beforeSpacer)
|
||||
: this._renderPanels(beforeSpacer)}
|
||||
${this._renderSpacer()}
|
||||
${this._renderPanels(afterSpacer)}
|
||||
${this._renderExternalConfiguration()}
|
||||
`}
|
||||
</paper-listbox>
|
||||
`;
|
||||
}
|
||||
@@ -474,23 +539,49 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _setPanelOrder(panelOrder: string[]) {
|
||||
if (this._devicePanelOrder || this._deviceHiddenPanels) {
|
||||
this._devicePanelOrder = [...panelOrder];
|
||||
} else {
|
||||
this._userPanelOrder = [...panelOrder];
|
||||
await saveSidebarPreferences(this.hass, {
|
||||
panelOrder: panelOrder,
|
||||
hiddenPanels: this._userHiddenPanels,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _setHiddenPanels(hiddenPanels: string[]) {
|
||||
if (this._devicePanelOrder || this._deviceHiddenPanels) {
|
||||
this._deviceHiddenPanels = hiddenPanels;
|
||||
} else {
|
||||
this._userHiddenPanels = hiddenPanels;
|
||||
await saveSidebarPreferences(this.hass, {
|
||||
panelOrder: this._userPanelOrder,
|
||||
hiddenPanels: hiddenPanels,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _panelMoved(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
|
||||
const { panelOrder, hiddenPanels } = this._getPanelPreferences();
|
||||
|
||||
const [beforeSpacer] = computePanels(
|
||||
this.hass.panels,
|
||||
this.hass.defaultPanel,
|
||||
this._panelOrder,
|
||||
this._hiddenPanels,
|
||||
panelOrder,
|
||||
hiddenPanels!,
|
||||
this.hass.locale
|
||||
);
|
||||
|
||||
const panelOrder = beforeSpacer.map((panel) => panel.url_path);
|
||||
const panel = panelOrder.splice(oldIndex, 1)[0];
|
||||
panelOrder.splice(newIndex, 0, panel);
|
||||
const panelOrderNew = beforeSpacer.map((panel) => panel.url_path);
|
||||
const panel = panelOrderNew.splice(oldIndex, 1)[0];
|
||||
panelOrderNew.splice(newIndex, 0, panel);
|
||||
|
||||
this._panelOrder = panelOrder;
|
||||
this._setPanelOrder(panelOrderNew);
|
||||
}
|
||||
|
||||
private _renderPanelsEdit(beforeSpacer: PanelInfo[]) {
|
||||
@@ -507,8 +598,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _renderHiddenPanels() {
|
||||
return html`${this._hiddenPanels.length
|
||||
? html`${this._hiddenPanels.map((url) => {
|
||||
const { hiddenPanels } = this._getPanelPreferences();
|
||||
|
||||
return html`${hiddenPanels.length
|
||||
? html`${hiddenPanels.map((url) => {
|
||||
const panel = this.hass.panels[url];
|
||||
if (!panel) {
|
||||
return "";
|
||||
@@ -690,9 +783,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _handleExternalAppConfiguration(ev: Event) {
|
||||
ev.preventDefault();
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "config_screen/show",
|
||||
});
|
||||
this.hass.auth.external!.fireMessage({ type: "config_screen/show" });
|
||||
}
|
||||
|
||||
private get _tooltip() {
|
||||
@@ -730,21 +821,25 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
private async _hidePanel(ev: Event) {
|
||||
ev.preventDefault();
|
||||
const panel = (ev.currentTarget as any).panel;
|
||||
if (this._hiddenPanels.includes(panel)) {
|
||||
if ((this._deviceHiddenPanels || this._userHiddenPanels).includes(panel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { panelOrder, hiddenPanels } = this._getPanelPreferences();
|
||||
|
||||
// Make a copy for Memoize
|
||||
this._hiddenPanels = [...this._hiddenPanels, panel];
|
||||
this._setHiddenPanels([...hiddenPanels, panel]);
|
||||
// Remove it from the panel order
|
||||
this._panelOrder = this._panelOrder.filter((order) => order !== panel);
|
||||
this._setPanelOrder(panelOrder.filter((order) => order !== panel));
|
||||
}
|
||||
|
||||
private async _unhidePanel(ev: Event) {
|
||||
ev.preventDefault();
|
||||
const panel = (ev.currentTarget as any).panel;
|
||||
this._hiddenPanels = this._hiddenPanels.filter(
|
||||
(hidden) => hidden !== panel
|
||||
);
|
||||
|
||||
const { hiddenPanels } = this._getPanelPreferences();
|
||||
|
||||
this._setHiddenPanels(hiddenPanels.filter((hidden) => hidden !== panel));
|
||||
}
|
||||
|
||||
private _itemMouseEnter(ev: MouseEvent) {
|
||||
@@ -784,9 +879,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
this._hideTooltip();
|
||||
}
|
||||
|
||||
@eventOptions({
|
||||
passive: true,
|
||||
})
|
||||
@eventOptions({ passive: true })
|
||||
private _listboxScroll() {
|
||||
// On keypresses on the listbox, we're going to ignore scroll events
|
||||
// for 100ms so that if pressing down arrow scrolls the sidebar, the tooltip
|
||||
@@ -1117,6 +1210,60 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
-webkit-transform: scaleX(var(--scale-direction));
|
||||
transform: scaleX(var(--scale-direction));
|
||||
}
|
||||
|
||||
@keyframes skeletonAnimate {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes contentAnimate {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
opacity: 0;
|
||||
animation-name: skeletonAnimate;
|
||||
animation-duration: 2000ms;
|
||||
animation-delay: 0;
|
||||
animation-iteration-count: 1;
|
||||
animation-fill-mode: forwards;
|
||||
animation-timing-function: ease-out;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
sl-skeleton {
|
||||
--border-radius: 8px;
|
||||
height: 24px;
|
||||
--color: var(--outline-color);
|
||||
--sheen-color: var(--outline-hover-color);
|
||||
}
|
||||
|
||||
sl-skeleton:nth-child(2) {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
sl-skeleton:nth-child(3) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
sl-skeleton:nth-child(4) {
|
||||
width: 90%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -8,9 +8,10 @@ import type {
|
||||
Map,
|
||||
Marker,
|
||||
Polyline,
|
||||
MarkerClusterGroup,
|
||||
} from "leaflet";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { ReactiveElement, css } from "lit";
|
||||
import { css, ReactiveElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { formatDateTime } from "../../common/datetime/format_date_time";
|
||||
@@ -26,6 +27,7 @@ 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
|
||||
@@ -84,6 +86,9 @@ 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;
|
||||
@@ -96,10 +101,12 @@ export class HaMap extends ReactiveElement {
|
||||
|
||||
private _mapFocusItems: (Marker | Circle)[] = [];
|
||||
|
||||
private _mapZones: (Marker | Circle)[] = [];
|
||||
private _mapZones: DecoratedMarker[] = [];
|
||||
|
||||
private _mapFocusZones: (Marker | Circle)[] = [];
|
||||
|
||||
private _mapCluster: MarkerClusterGroup | undefined;
|
||||
|
||||
private _mapPaths: (Polyline | CircleMarker)[] = [];
|
||||
|
||||
private _clickCount = 0;
|
||||
@@ -151,6 +158,10 @@ export class HaMap extends ReactiveElement {
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has("clusterMarkers")) {
|
||||
this._drawEntities();
|
||||
}
|
||||
|
||||
if (changedProps.has("_loaded") || changedProps.has("paths")) {
|
||||
this._drawPaths();
|
||||
}
|
||||
@@ -175,6 +186,7 @@ export class HaMap extends ReactiveElement {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateMapStyle();
|
||||
}
|
||||
|
||||
@@ -426,6 +438,11 @@ export class HaMap extends ReactiveElement {
|
||||
this._mapFocusZones = [];
|
||||
}
|
||||
|
||||
if (this._mapCluster) {
|
||||
this._mapCluster.remove();
|
||||
this._mapCluster = undefined;
|
||||
}
|
||||
|
||||
if (!this.entities) {
|
||||
return;
|
||||
}
|
||||
@@ -481,26 +498,24 @@ 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,
|
||||
});
|
||||
this._mapZones.push(circle);
|
||||
|
||||
const marker = new DecoratedMarker([latitude, longitude], circle, {
|
||||
icon: Leaflet.divIcon({
|
||||
html: iconHTML,
|
||||
iconSize: [24, 24],
|
||||
className,
|
||||
}),
|
||||
interactive: this.interactiveZones,
|
||||
title,
|
||||
});
|
||||
|
||||
this._mapZones.push(marker);
|
||||
if (
|
||||
this.fitZones &&
|
||||
(typeof entity === "string" || entity.focus !== false)
|
||||
@@ -538,7 +553,7 @@ export class HaMap extends ReactiveElement {
|
||||
}
|
||||
|
||||
// create marker with the icon
|
||||
const marker = Leaflet.marker([latitude, longitude], {
|
||||
const marker = new DecoratedMarker([latitude, longitude], undefined, {
|
||||
icon: Leaflet.divIcon({
|
||||
html: entityMarker,
|
||||
iconSize: [48, 48],
|
||||
@@ -546,24 +561,34 @@ 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) {
|
||||
this._mapItems.push(
|
||||
Leaflet.circle([latitude, longitude], {
|
||||
interactive: false,
|
||||
color: darkPrimaryColor,
|
||||
radius: gpsAccuracy,
|
||||
})
|
||||
);
|
||||
marker.decorationLayer = 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,
|
||||
maxClusterRadius: 40,
|
||||
});
|
||||
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,25 +1,81 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { customElement, property } 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 {
|
||||
return html`
|
||||
<div class="shape">
|
||||
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>
|
||||
<slot></slot>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
--tile-icon-color: var(--disabled-color);
|
||||
--mdc-icon-size: 22px;
|
||||
--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;
|
||||
}
|
||||
.shape::before {
|
||||
: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 {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -27,24 +83,21 @@ export class HaTileIcon extends LitElement {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--tile-icon-color);
|
||||
transition: background-color 180ms ease-in-out;
|
||||
opacity: 0.2;
|
||||
transition:
|
||||
background-color 180ms ease-in-out,
|
||||
opacity 180ms ease-in-out;
|
||||
opacity: var(--tile-icon-opacity);
|
||||
}
|
||||
.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(*) {
|
||||
.container ::slotted([slot="icon"]) {
|
||||
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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -12,6 +12,7 @@ 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";
|
||||
|
||||
@@ -130,7 +131,13 @@ export interface BackupContentExtended extends BackupContent, BackupData {}
|
||||
|
||||
export interface BackupInfo {
|
||||
backups: BackupContent[];
|
||||
backing_up: boolean;
|
||||
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;
|
||||
}
|
||||
|
||||
export interface BackupDetails {
|
||||
|
||||
@@ -73,6 +73,7 @@ export interface CloudWebhook {
|
||||
interface CloudLoginBase {
|
||||
hass: HomeAssistant;
|
||||
email: string;
|
||||
check_connection?: boolean;
|
||||
}
|
||||
|
||||
export interface CloudLoginPassword extends CloudLoginBase {
|
||||
|
||||
@@ -233,11 +233,11 @@ export const restoreBackup = async (
|
||||
type: HassioBackupDetail["type"],
|
||||
backupSlug: string,
|
||||
backupDetails: HassioPartialBackupCreateParams | HassioFullBackupCreateParams,
|
||||
useSnapshotUrl: boolean
|
||||
useBackupUrl: boolean
|
||||
): Promise<void> => {
|
||||
await hass.callApi<HassioResponse<{ job_id: string }>>(
|
||||
"POST",
|
||||
`hassio/${useSnapshotUrl ? "snapshots" : "backups"}/${backupSlug}/restore/${type}`,
|
||||
`hassio/${useBackupUrl ? "backups" : "snapshots"}/${backupSlug}/restore/${type}`,
|
||||
backupDetails
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
import {
|
||||
fetchFrontendUserData,
|
||||
saveFrontendUserData,
|
||||
subscribeFrontendUserData,
|
||||
} from "./frontend";
|
||||
|
||||
export const SIDEBAR_PREFERENCES_KEY = "sidebar";
|
||||
|
||||
export interface SidebarPreferences {
|
||||
panelOrder?: string[];
|
||||
hiddenPanels?: string[];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface FrontendUserData {
|
||||
sidebar?: SidebarPreferences;
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchSidebarPreferences = (hass: HomeAssistant) =>
|
||||
fetchFrontendUserData(hass.connection, SIDEBAR_PREFERENCES_KEY);
|
||||
|
||||
export const saveSidebarPreferences = (
|
||||
hass: HomeAssistant,
|
||||
data: SidebarPreferences
|
||||
) => saveFrontendUserData(hass.connection, SIDEBAR_PREFERENCES_KEY, data);
|
||||
|
||||
export const subscribeSidebarPreferences = (
|
||||
hass: HomeAssistant,
|
||||
callback: (sidebar?: SidebarPreferences | null) => void
|
||||
) =>
|
||||
subscribeFrontendUserData(hass.connection, SIDEBAR_PREFERENCES_KEY, callback);
|
||||
@@ -85,6 +85,7 @@ class StepFlowCreateEntry extends LitElement {
|
||||
assistSatelliteSupportsSetupFlow(this.hass.states[satellite.entity_id])
|
||||
)
|
||||
) {
|
||||
this.navigateToResult = false;
|
||||
this._flowDone();
|
||||
showVoiceAssistantSetupDialog(this, {
|
||||
deviceId: devices[0].id,
|
||||
|
||||
@@ -40,8 +40,13 @@ export class DialogEnterCode
|
||||
|
||||
@state() private _showClearButton = false;
|
||||
|
||||
@state() private _narrow = false;
|
||||
|
||||
public async showDialog(dialogParams: EnterCodeDialogParams): Promise<void> {
|
||||
this._dialogParams = dialogParams;
|
||||
this._narrow = matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
@@ -96,7 +101,7 @@ export class DialogEnterCode
|
||||
>
|
||||
<ha-textfield
|
||||
class="input"
|
||||
dialogInitialFocus
|
||||
?dialogInitialFocus=${!this._narrow}
|
||||
id="code"
|
||||
.label=${this.hass.localize("ui.dialogs.enter_code.input_label")}
|
||||
type="password"
|
||||
@@ -134,6 +139,7 @@ export class DialogEnterCode
|
||||
.label=${this.hass.localize("ui.dialogs.enter_code.input_label")}
|
||||
type="password"
|
||||
inputmode="numeric"
|
||||
?dialogInitialFocus=${!this._narrow}
|
||||
></ha-textfield>
|
||||
<div class="keypad">
|
||||
${BUTTONS.map((value) =>
|
||||
|
||||
@@ -49,6 +49,8 @@ class LightRgbColorPicker extends LitElement {
|
||||
|
||||
@state() private _hsPickerValue?: [number, number];
|
||||
|
||||
@state() private _isInteracting?: boolean;
|
||||
|
||||
protected render() {
|
||||
if (!this.stateObj) {
|
||||
return nothing;
|
||||
@@ -211,7 +213,10 @@ class LightRgbColorPicker extends LitElement {
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!changedProps.has("entityId") && !changedProps.has("hass")) {
|
||||
if (
|
||||
this._isInteracting ||
|
||||
(!changedProps.has("entityId") && !changedProps.has("hass"))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -219,10 +224,13 @@ class LightRgbColorPicker extends LitElement {
|
||||
}
|
||||
|
||||
private _hsColorCursorMoved(ev: CustomEvent) {
|
||||
if (!ev.detail.value) {
|
||||
const color = ev.detail.value;
|
||||
this._isInteracting = color !== undefined;
|
||||
|
||||
if (color === undefined) {
|
||||
return;
|
||||
}
|
||||
this._hsPickerValue = ev.detail.value;
|
||||
this._hsPickerValue = color;
|
||||
|
||||
this._throttleUpdateColor();
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import { DOMAIN_ATTRIBUTES_UNITS } from "../../../../data/entity_attributes";
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"color-changed": LightColor;
|
||||
"color-hovered": LightColor | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +53,8 @@ class LightColorTempPicker extends LitElement {
|
||||
|
||||
@state() private _ctPickerValue?: number;
|
||||
|
||||
@state() private _isInteracting?: boolean;
|
||||
|
||||
protected render() {
|
||||
if (!this.stateObj) {
|
||||
return nothing;
|
||||
@@ -113,7 +114,7 @@ class LightColorTempPicker extends LitElement {
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!changedProps.has("stateObj")) {
|
||||
if (this._isInteracting || !changedProps.has("stateObj")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,16 +124,14 @@ 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();
|
||||
}
|
||||
|
||||
@@ -143,8 +142,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -99,7 +99,12 @@ class MoreInfoSirenAdvancedControls extends LitElement {
|
||||
this._stateObj.attributes.available_tones
|
||||
).map(
|
||||
([toneId, toneName]) => html`
|
||||
<ha-list-item .value=${toneId}
|
||||
<ha-list-item
|
||||
.value=${Array.isArray(
|
||||
this._stateObj!.attributes.available_tones
|
||||
)
|
||||
? toneName
|
||||
: toneId}
|
||||
>${toneName}</ha-list-item
|
||||
>
|
||||
`
|
||||
@@ -179,7 +184,7 @@ class MoreInfoSirenAdvancedControls extends LitElement {
|
||||
await this.hass.callService("siren", "turn_on", {
|
||||
entity_id: this._stateObj!.entity_id,
|
||||
tone: this._tone,
|
||||
volume: this._volume,
|
||||
volume_level: this._volume,
|
||||
duration: this._duration,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -251,6 +251,7 @@ export class QuickBar extends LitElement {
|
||||
<mwc-list>
|
||||
${this._opened
|
||||
? html`<lit-virtualizer
|
||||
tabindex="-1"
|
||||
scroller
|
||||
@keydown=${this._handleListItemKeyDown}
|
||||
@rangechange=${this._handleRangeChanged}
|
||||
@@ -326,6 +327,7 @@ export class QuickBar extends LitElement {
|
||||
.twoline=${Boolean(item.area)}
|
||||
.item=${item}
|
||||
index=${ifDefined(index)}
|
||||
tabindex="0"
|
||||
>
|
||||
<span>${item.primaryText}</span>
|
||||
${item.area
|
||||
@@ -346,6 +348,7 @@ export class QuickBar extends LitElement {
|
||||
.item=${item}
|
||||
index=${ifDefined(index)}
|
||||
graphic="icon"
|
||||
tabindex="0"
|
||||
>
|
||||
${item.iconPath
|
||||
? html`
|
||||
@@ -375,6 +378,7 @@ export class QuickBar extends LitElement {
|
||||
index=${ifDefined(index)}
|
||||
class="command-item"
|
||||
hasMeta
|
||||
tabindex="0"
|
||||
>
|
||||
<span>
|
||||
<ha-label
|
||||
|
||||
@@ -10,6 +10,7 @@ import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../components/ha-textfield";
|
||||
import { cloudLogin } from "../../../data/cloud";
|
||||
import { showCloudAlreadyConnectedDialog } from "../../../panels/config/cloud/dialog-cloud-already-connected/show-dialog-cloud-already-connected";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import {
|
||||
showAlertDialog,
|
||||
@@ -25,6 +26,8 @@ export class CloudStepSignin extends LitElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _checkConnection = true;
|
||||
|
||||
@query("#email", true) private _emailField!: HaTextField;
|
||||
|
||||
@query("#password", true) private _passwordField!: HaPasswordField;
|
||||
@@ -115,6 +118,7 @@ export class CloudStepSignin extends LitElement {
|
||||
hass: this.hass,
|
||||
email: username,
|
||||
...(code ? { code } : { password }),
|
||||
check_connection: this._checkConnection,
|
||||
});
|
||||
} catch (err: any) {
|
||||
const errCode = err && err.body && err.body.code;
|
||||
@@ -139,6 +143,20 @@ export class CloudStepSignin extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
if (errCode === "alreadyconnectederror") {
|
||||
showCloudAlreadyConnectedDialog(this, {
|
||||
details: JSON.parse(err.body.message),
|
||||
logInHereAction: () => {
|
||||
this._checkConnection = false;
|
||||
doLogin(username);
|
||||
},
|
||||
closeDialog: () => {
|
||||
this._requestInProgress = false;
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (errCode === "usernotfound" && username !== username.toLowerCase()) {
|
||||
await doLogin(username.toLowerCase());
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { consume } from "@lit-labs/context";
|
||||
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import {
|
||||
mdiAlertCircleCheck,
|
||||
mdiArrowDown,
|
||||
@@ -27,7 +26,9 @@ 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-button-menu";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-menu-item";
|
||||
import "../../../../components/ha-md-divider";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
@@ -240,89 +241,104 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
</div> `
|
||||
: nothing}
|
||||
|
||||
<ha-button-menu
|
||||
<ha-md-button-menu
|
||||
slot="icons"
|
||||
@action=${this._handleAction}
|
||||
@click=${preventDefault}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
fixed
|
||||
positioning="fixed"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-md-menu-item .clickAction=${this._runAction}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.run"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPlay}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlay}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._renameAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<li divider role="separator"></li>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._duplicateAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.duplicate"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._copyAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.copy"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._cutAction}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.cut"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveUp}
|
||||
.disabled=${this.disabled || this.first}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveDown}
|
||||
.disabled=${this.disabled || this.last}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${!this._uiModeAvailable}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${!this._uiModeAvailable}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlaylistEdit}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<li divider role="separator"></li>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDisable}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.action.enabled === false
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.enable"
|
||||
@@ -331,15 +347,15 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
"ui.panel.config.automation.editor.actions.disable"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.path=${this.action.enabled === false
|
||||
? mdiPlayCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
class="warning"
|
||||
graphic="icon"
|
||||
.clickAction=${this._onDelete}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
@@ -347,11 +363,11 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
|
||||
<div
|
||||
class=${classMap({
|
||||
@@ -424,47 +440,6 @@ 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,
|
||||
@@ -472,16 +447,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 async _runAction() {
|
||||
private _runAction = async () => {
|
||||
const validated = await validateConfig(this.hass, {
|
||||
actions: this.action,
|
||||
});
|
||||
@@ -513,9 +488,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"
|
||||
@@ -530,7 +505,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private _onYamlChange(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
@@ -561,7 +536,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
this._yamlMode = true;
|
||||
}
|
||||
|
||||
private async _renameAction(): Promise<void> {
|
||||
private _renameAction = async (): Promise<void> => {
|
||||
const alias = await showPromptDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.change_alias"
|
||||
@@ -598,7 +573,37 @@ 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(() => {
|
||||
@@ -610,7 +615,6 @@ 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);
|
||||
}
|
||||
@@ -649,18 +653,11 @@ 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;
|
||||
}
|
||||
li[role="separator"] {
|
||||
border-bottom-color: var(--divider-color);
|
||||
ha-md-menu-item > ha-svg-icon {
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { consume } from "@lit-labs/context";
|
||||
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import {
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
@@ -24,11 +23,12 @@ 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-button-menu";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-menu-item";
|
||||
import "../../../../components/ha-md-divider";
|
||||
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-button-menu
|
||||
<ha-md-button-menu
|
||||
slot="icons"
|
||||
@action=${this._handleAction}
|
||||
@click=${preventDefault}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
fixed
|
||||
positioning="fixed"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
@@ -155,76 +155,91 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
>
|
||||
</ha-icon-button>
|
||||
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-md-menu-item .clickAction=${this._testCondition}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.test"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiFlask}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-svg-icon slot="start" .path=${mdiFlask}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._renameCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<li divider role="separator"></li>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._duplicateCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.duplicate"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._copyCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.copy"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._cutCondition}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.cut"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveUp}
|
||||
.disabled=${this.disabled || this.first}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveDown}
|
||||
.disabled=${this.disabled || this.last}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this._warnings}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${this._warnings}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlaylistEdit}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<li divider role="separator"></li>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDisable}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.condition.enabled === false
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.enable"
|
||||
@@ -233,15 +248,15 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
"ui.panel.config.automation.editor.actions.disable"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.path=${this.condition.enabled === false
|
||||
? mdiPlayCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
class="warning"
|
||||
graphic="icon"
|
||||
.clickAction=${this._onDelete}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
@@ -249,11 +264,11 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
|
||||
<div
|
||||
class=${classMap({
|
||||
@@ -325,47 +340,6 @@ 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,
|
||||
@@ -373,13 +347,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"
|
||||
@@ -394,7 +368,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private _switchUiMode() {
|
||||
this._warnings = undefined;
|
||||
@@ -406,7 +380,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
this._yamlMode = true;
|
||||
}
|
||||
|
||||
private async _testCondition() {
|
||||
private _testCondition = async () => {
|
||||
if (this._testing) {
|
||||
return;
|
||||
}
|
||||
@@ -461,9 +435,9 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
this._testing = false;
|
||||
}, 2500);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private async _renameCondition(): Promise<void> {
|
||||
private _renameCondition = async (): Promise<void> => {
|
||||
const alias = await showPromptDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.change_alias"
|
||||
@@ -489,7 +463,37 @@ 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(() => {
|
||||
@@ -501,9 +505,6 @@ 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;
|
||||
@@ -539,12 +540,6 @@ 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;
|
||||
@@ -571,8 +566,8 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
.testing.pass {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
li[role="separator"] {
|
||||
border-bottom-color: var(--divider-color);
|
||||
ha-md-menu-item > ha-svg-icon {
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -339,9 +339,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
hidden: narrow,
|
||||
title: "",
|
||||
type: "overflow",
|
||||
label: this.hass.localize("ui.panel.config.automation.picker.state"),
|
||||
title: this.hass.localize("ui.panel.config.automation.picker.state"),
|
||||
template: (automation) => html`
|
||||
<ha-entity-toggle
|
||||
.stateObj=${automation}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { consume } from "@lit-labs/context";
|
||||
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import {
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
@@ -28,7 +27,9 @@ 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-button-menu";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-menu-item";
|
||||
import "../../../../components/ha-md-divider";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
@@ -169,12 +170,12 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
<ha-button-menu
|
||||
<ha-md-button-menu
|
||||
slot="icons"
|
||||
@action=${this._handleAction}
|
||||
@click=${preventDefault}
|
||||
@keydown=${stopPropagation}
|
||||
@closed=${stopPropagation}
|
||||
fixed
|
||||
positioning="fixed"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
@@ -182,84 +183,93 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._renameTrigger}
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._showTriggerId}
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.edit_id"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiIdentifier}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiIdentifier}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<li divider role="separator"></li>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._duplicateTrigger}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.duplicate"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._copyTrigger}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.copy"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${this.disabled}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._cutTrigger}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.cut"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveUp}
|
||||
.disabled=${this.disabled || this.first}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._moveDown}
|
||||
.disabled=${this.disabled || this.last}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
|
||||
></ha-md-menu-item>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${!supported}>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._toggleYamlMode}
|
||||
.disabled=${!supported}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlaylistEdit}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-md-menu-item>
|
||||
|
||||
<li divider role="separator"></li>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDisable}
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
${"enabled" in this.trigger && this.trigger.enabled === false
|
||||
@@ -270,16 +280,16 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
"ui.panel.config.automation.editor.actions.disable"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.path=${"enabled" in this.trigger &&
|
||||
this.trigger.enabled === false
|
||||
? mdiPlayCircleOutline
|
||||
: mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._onDelete}
|
||||
class="warning"
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
@@ -287,11 +297,11 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
|
||||
<div
|
||||
class=${classMap({
|
||||
@@ -464,48 +474,6 @@ 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,
|
||||
@@ -513,7 +481,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"
|
||||
@@ -528,9 +496,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 };
|
||||
@@ -538,7 +506,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
if (this._yamlMode) {
|
||||
this._yamlEditor?.setValue(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private _idChanged(ev: CustomEvent) {
|
||||
if (isTriggerList(this.trigger)) return;
|
||||
@@ -605,7 +573,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private async _renameTrigger(): Promise<void> {
|
||||
private _renameTrigger = async (): Promise<void> => {
|
||||
if (isTriggerList(this.trigger)) return;
|
||||
const alias = await showPromptDialog(this, {
|
||||
title: this.hass.localize(
|
||||
@@ -636,7 +604,42 @@ 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(() => {
|
||||
@@ -648,9 +651,6 @@ 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,18 +714,12 @@ 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;
|
||||
}
|
||||
li[role="separator"] {
|
||||
border-bottom-color: var(--divider-color);
|
||||
ha-md-menu-item > ha-svg-icon {
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -4,6 +4,7 @@ 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";
|
||||
@@ -22,7 +23,6 @@ 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";
|
||||
type: "copies" | "days" | "forever";
|
||||
value: number;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ const RETENTION_PRESETS: Record<
|
||||
RetentionData
|
||||
> = {
|
||||
copies_3: { type: "copies", value: 3 },
|
||||
forever: { type: "days", value: 0 },
|
||||
forever: { type: "forever", value: 0 },
|
||||
};
|
||||
|
||||
const SCHEDULE_OPTIONS = [
|
||||
@@ -79,7 +79,10 @@ const computeRetentionPreset = (
|
||||
data: RetentionData
|
||||
): RetentionPreset | undefined => {
|
||||
for (const [key, value] of Object.entries(RETENTION_PRESETS)) {
|
||||
if (value.type === data.type && value.value === data.value) {
|
||||
if (
|
||||
value.type === data.type &&
|
||||
(value.type === RetentionPreset.FOREVER || value.value === data.value)
|
||||
) {
|
||||
return key as RetentionPreset;
|
||||
}
|
||||
}
|
||||
@@ -92,7 +95,7 @@ interface FormData {
|
||||
time?: string | null;
|
||||
days: BackupDay[];
|
||||
retention: {
|
||||
type: "copies" | "days";
|
||||
type: "copies" | "days" | "forever";
|
||||
value: number;
|
||||
};
|
||||
}
|
||||
@@ -142,7 +145,12 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
? config.schedule.days
|
||||
: [],
|
||||
retention: {
|
||||
type: config.retention.days != null ? "days" : "copies",
|
||||
type:
|
||||
config.retention.days === null && config.retention.copies === null
|
||||
? "forever"
|
||||
: config.retention.days != null
|
||||
? "days"
|
||||
: "copies",
|
||||
value: config.retention.days ?? config.retention.copies ?? 3,
|
||||
},
|
||||
};
|
||||
@@ -160,9 +168,11 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
: [],
|
||||
},
|
||||
retention:
|
||||
data.retention.type === "days"
|
||||
? { days: data.retention.value, copies: null }
|
||||
: { copies: data.retention.value, days: null },
|
||||
data.retention.type === "forever"
|
||||
? { days: null, copies: null }
|
||||
: data.retention.type === "days"
|
||||
? { days: data.retention.value, copies: null }
|
||||
: { copies: data.retention.value, days: null },
|
||||
};
|
||||
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
@@ -481,9 +491,19 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
private _retentionPresetChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
const target = ev.currentTarget as HaMdSelect;
|
||||
const value = target.value as RetentionPreset;
|
||||
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;
|
||||
}
|
||||
|
||||
this._retentionPreset = value;
|
||||
if (value !== RetentionPreset.CUSTOM) {
|
||||
const data = this._getData(this.value);
|
||||
const retention = RETENTION_PRESETS[value];
|
||||
@@ -493,7 +513,7 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
}
|
||||
this._setData({
|
||||
...data,
|
||||
retention: RETENTION_PRESETS[value],
|
||||
retention,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -504,6 +524,7 @@ 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: {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
mdiUpload,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { css, 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,6 +27,7 @@ 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";
|
||||
@@ -460,7 +461,17 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
extended
|
||||
@click=${this._newBackup}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
${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-fab>
|
||||
`
|
||||
: nothing}
|
||||
@@ -605,7 +616,14 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return haStyle;
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
ha-circular-progress {
|
||||
--md-sys-color-primary: var(--mdc-theme-on-secondary);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ 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";
|
||||
@@ -17,8 +18,10 @@ import type {
|
||||
BackupAgent,
|
||||
BackupConfig,
|
||||
BackupContent,
|
||||
BackupInfo,
|
||||
} from "../../../data/backup";
|
||||
import {
|
||||
computeBackupAgentName,
|
||||
generateBackup,
|
||||
generateBackupWithAutomaticSettings,
|
||||
} from "../../../data/backup";
|
||||
@@ -50,6 +53,8 @@ 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;
|
||||
@@ -151,6 +156,26 @@ 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
|
||||
@@ -204,7 +229,14 @@ class HaConfigBackupOverview extends LitElement {
|
||||
extended
|
||||
@click=${this._newBackup}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
${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-fab>
|
||||
</hass-subpage>
|
||||
`;
|
||||
@@ -231,6 +263,9 @@ 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 } from "@mdi/js";
|
||||
import { mdiDotsVertical, mdiHarddisk, mdiOpenInNew } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -28,6 +28,7 @@ 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 {
|
||||
@@ -98,6 +99,8 @@ class HaConfigBackupSettings extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const supervisor = isComponentLoaded(this.hass, "hassio");
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/backup"
|
||||
@@ -105,7 +108,7 @@ class HaConfigBackupSettings extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.backup.settings.header")}
|
||||
>
|
||||
${isComponentLoaded(this.hass, "hassio")
|
||||
${supervisor
|
||||
? html`
|
||||
<ha-button-menu slot="toolbar-icon">
|
||||
<ha-icon-button
|
||||
@@ -203,6 +206,29 @@ 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">
|
||||
@@ -342,6 +368,9 @@ 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,
|
||||
BackupContent,
|
||||
BackupInfo,
|
||||
} from "../../../data/backup";
|
||||
import {
|
||||
compareAgents,
|
||||
@@ -44,7 +44,7 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
|
||||
|
||||
@state() private _manager: ManagerStateEvent = DEFAULT_MANAGER_STATE;
|
||||
|
||||
@state() private _backups: BackupContent[] = [];
|
||||
@state() private _info?: BackupInfo;
|
||||
|
||||
@state() private _agents: BackupAgent[] = [];
|
||||
|
||||
@@ -87,8 +87,7 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
|
||||
}
|
||||
|
||||
private async _fetchBackupInfo() {
|
||||
const info = await fetchBackupInfo(this.hass);
|
||||
this._backups = info.backups;
|
||||
this._info = await fetchBackupInfo(this.hass);
|
||||
}
|
||||
|
||||
private async _fetchBackupConfig() {
|
||||
@@ -134,7 +133,8 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
|
||||
pageEl.narrow = this.narrow;
|
||||
pageEl.cloudStatus = this.cloudStatus;
|
||||
pageEl.manager = this._manager;
|
||||
pageEl.backups = this._backups;
|
||||
pageEl.info = this._info;
|
||||
pageEl.backups = this._info?.backups || [];
|
||||
pageEl.config = this._config;
|
||||
pageEl.agents = this._agents;
|
||||
pageEl.fetching = this._fetching;
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import { mdiContentCopy, mdiEye, mdiEyeOff, mdiHelpCircle } from "@mdi/js";
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-radio";
|
||||
import "../../../../components/ha-settings-row";
|
||||
import "../../../../components/ha-switch";
|
||||
import "../../../../components/ha-textfield";
|
||||
|
||||
import { formatDate } from "../../../../common/datetime/format_date";
|
||||
import type { HaSwitch } from "../../../../components/ha-switch";
|
||||
@@ -25,6 +21,7 @@ import type { HomeAssistant } from "../../../../types";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import { showCloudCertificateDialog } from "../dialog-cloud-certificate/show-dialog-cloud-certificate";
|
||||
import { obfuscateUrl } from "../../../../util/url";
|
||||
import "../../../../components/ha-copy-textfield";
|
||||
|
||||
@customElement("cloud-remote-pref")
|
||||
export class CloudRemotePref extends LitElement {
|
||||
@@ -34,8 +31,6 @@ export class CloudRemotePref extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _unmaskedUrl = false;
|
||||
|
||||
protected render() {
|
||||
if (!this.cloudStatus) {
|
||||
return nothing;
|
||||
@@ -139,37 +134,13 @@ export class CloudRemotePref extends LitElement {
|
||||
)}
|
||||
</p>
|
||||
`}
|
||||
<div class="url-container">
|
||||
<div class="textfield-container">
|
||||
<ha-textfield
|
||||
.value=${this._unmaskedUrl
|
||||
? `https://${remote_domain}`
|
||||
: obfuscateUrl(`https://${remote_domain}`)}
|
||||
readonly
|
||||
.suffix=${
|
||||
// reserve some space for the icon.
|
||||
html`<div style="width: 24px"></div>`
|
||||
}
|
||||
></ha-textfield>
|
||||
<ha-icon-button
|
||||
class="toggle-unmasked-url"
|
||||
toggles
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.common.${this._unmaskedUrl ? "hide" : "show"}_url`
|
||||
)}
|
||||
@click=${this._toggleUnmaskedUrl}
|
||||
.path=${this._unmaskedUrl ? mdiEyeOff : mdiEye}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<ha-button
|
||||
.url=${`https://${remote_domain}`}
|
||||
@click=${this._copyURL}
|
||||
unelevated
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.common.copy_link")}
|
||||
</ha-button>
|
||||
</div>
|
||||
|
||||
<ha-copy-textfield
|
||||
.hass=${this.hass}
|
||||
.value=${`https://${remote_domain}`}
|
||||
.maskedValue=${obfuscateUrl(`https://${remote_domain}`)}
|
||||
.label=${this.hass!.localize("ui.panel.config.common.copy_link")}
|
||||
></ha-copy-textfield>
|
||||
|
||||
<ha-expansion-panel
|
||||
outlined
|
||||
@@ -234,10 +205,6 @@ export class CloudRemotePref extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _toggleUnmaskedUrl(): void {
|
||||
this._unmaskedUrl = !this._unmaskedUrl;
|
||||
}
|
||||
|
||||
private async _toggleChanged(ev) {
|
||||
const toggle = ev.target as HaSwitch;
|
||||
|
||||
@@ -268,14 +235,6 @@ export class CloudRemotePref extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _copyURL(ev): Promise<void> {
|
||||
const url = ev.currentTarget.url;
|
||||
await copyToClipboard(url);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.preparing {
|
||||
padding: 0 16px 16px;
|
||||
@@ -335,30 +294,6 @@ export class CloudRemotePref extends LitElement {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.url-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.textfield-container {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
.textfield-container ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
.toggle-unmasked-url {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
hr {
|
||||
border: none;
|
||||
height: 1px;
|
||||
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
import { formatDateTime } from "../../../../common/datetime/format_date_time";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import { createCloseHeading } from "../../../../components/ha-dialog";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { CloudAlreadyConnectedParams as CloudAlreadyConnectedDialogParams } from "./show-dialog-cloud-already-connected";
|
||||
import { obfuscateUrl } from "../../../../util/url";
|
||||
|
||||
@customElement("dialog-cloud-already-connected")
|
||||
class DialogCloudAlreadyConnected extends LitElement {
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: CloudAlreadyConnectedDialogParams;
|
||||
|
||||
@state() private _obfuscateIp = true;
|
||||
|
||||
public showDialog(params: CloudAlreadyConnectedDialogParams) {
|
||||
this._params = params;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._params?.closeDialog();
|
||||
this._params = undefined;
|
||||
this._obfuscateIp = true;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
const { details } = this._params;
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize(
|
||||
"ui.panel.config.cloud.dialog_already_connected.heading"
|
||||
)
|
||||
)}
|
||||
>
|
||||
<div class="intro">
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.dialog_already_connected.description"
|
||||
)}
|
||||
</span>
|
||||
<b>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.dialog_already_connected.other_home_assistant"
|
||||
)}
|
||||
</b>
|
||||
</div>
|
||||
<div class="instance-details">
|
||||
<div class="instance-detail">
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.dialog_already_connected.ip_address"
|
||||
)}:
|
||||
</span>
|
||||
<div class="obfuscated">
|
||||
<span>
|
||||
${this._obfuscateIp
|
||||
? obfuscateUrl(details.remote_ip_address)
|
||||
: details.remote_ip_address}
|
||||
</span>
|
||||
|
||||
<ha-icon-button
|
||||
class="toggle-unmasked-url"
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.cloud.dialog_already_connected.obfuscated_ip.${this._obfuscateIp ? "hide" : "show"}`
|
||||
)}
|
||||
@click=${this._toggleObfuscateIp}
|
||||
.path=${this._obfuscateIp ? mdiEye : mdiEyeOff}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="instance-detail">
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.dialog_already_connected.connected_at"
|
||||
)}:
|
||||
</span>
|
||||
<span>
|
||||
${formatDateTime(
|
||||
new Date(details.connected_at),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ha-alert
|
||||
alert-type="info"
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.cloud.dialog_already_connected.info_backups.title"
|
||||
)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.dialog_already_connected.info_backups.description"
|
||||
)}
|
||||
</ha-alert>
|
||||
|
||||
<ha-button @click=${this.closeDialog} slot="secondaryAction">
|
||||
${this.hass!.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button @click=${this._logInHere} slot="primaryAction">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.cloud.dialog_already_connected.login_here"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _toggleObfuscateIp() {
|
||||
this._obfuscateIp = !this._obfuscateIp;
|
||||
}
|
||||
|
||||
private _logInHere() {
|
||||
this._params?.logInHereAction();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 535px;
|
||||
}
|
||||
.intro b {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.instance-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.instance-detail {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.obfuscated {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-cloud-already-connected": DialogCloudAlreadyConnected;
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
|
||||
export interface CloudAlreadyConnectedParams {
|
||||
details: {
|
||||
remote_ip_address: string;
|
||||
connected_at: string;
|
||||
};
|
||||
logInHereAction: () => void;
|
||||
closeDialog: () => void;
|
||||
}
|
||||
|
||||
export const showCloudAlreadyConnectedDialog = (
|
||||
element: HTMLElement,
|
||||
webhookDialogParams: CloudAlreadyConnectedParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-cloud-already-connected",
|
||||
dialogImport: () => import("./dialog-cloud-already-connected"),
|
||||
dialogParams: webhookDialogParams,
|
||||
});
|
||||
};
|
||||
@@ -1,27 +1,23 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiContentCopy, mdiOpenInNew } from "@mdi/js";
|
||||
import { mdiOpenInNew } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { query, state } from "lit/decorators";
|
||||
import { state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||
import { createCloseHeading } from "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../../components/ha-textfield";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { documentationUrl } from "../../../../util/documentation-url";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import type { WebhookDialogParams } from "./show-dialog-manage-cloudhook";
|
||||
|
||||
import "../../../../components/ha-copy-textfield";
|
||||
|
||||
export class DialogManageCloudhook extends LitElement {
|
||||
protected hass?: HomeAssistant;
|
||||
|
||||
@state() private _params?: WebhookDialogParams;
|
||||
|
||||
@query("ha-textfield") _input!: HaTextField;
|
||||
|
||||
public showDialog(params: WebhookDialogParams) {
|
||||
this._params = params;
|
||||
}
|
||||
@@ -82,21 +78,12 @@ export class DialogManageCloudhook extends LitElement {
|
||||
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</a>
|
||||
</p>
|
||||
<ha-textfield
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.cloud.dialog_cloudhook.public_url"
|
||||
)}
|
||||
|
||||
<ha-copy-textfield
|
||||
.hass=${this.hass}
|
||||
.value=${cloudhook.cloudhook_url}
|
||||
iconTrailing
|
||||
readOnly
|
||||
@click=${this._focusInput}
|
||||
>
|
||||
<ha-icon-button
|
||||
@click=${this._copyUrl}
|
||||
slot="trailingIcon"
|
||||
.path=${mdiContentCopy}
|
||||
></ha-icon-button>
|
||||
</ha-textfield>
|
||||
.label=${this.hass!.localize("ui.panel.config.common.copy_link")}
|
||||
></ha-copy-textfield>
|
||||
</div>
|
||||
|
||||
<a
|
||||
@@ -137,24 +124,6 @@ export class DialogManageCloudhook extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _focusInput(ev) {
|
||||
const inputElement = ev.currentTarget as HaTextField;
|
||||
inputElement.select();
|
||||
}
|
||||
|
||||
private async _copyUrl(ev): Promise<void> {
|
||||
if (!this.hass) return;
|
||||
ev.stopPropagation();
|
||||
const inputElement = ev.target.parentElement as HaTextField;
|
||||
inputElement.select();
|
||||
const url = this.hass.hassUrl(inputElement.value);
|
||||
|
||||
await copyToClipboard(url);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -163,13 +132,6 @@ export class DialogManageCloudhook extends LitElement {
|
||||
ha-dialog {
|
||||
width: 650px;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
ha-textfield > ha-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
--mdc-icon-size: 18px;
|
||||
}
|
||||
button.link {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
|
||||
@@ -28,6 +28,7 @@ import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../../ha-config-section";
|
||||
import { showSupportPackageDialog } from "../account/show-dialog-cloud-support-package";
|
||||
import { showCloudAlreadyConnectedDialog } from "../dialog-cloud-already-connected/show-dialog-cloud-already-connected";
|
||||
|
||||
@customElement("cloud-login")
|
||||
export class CloudLogin extends LitElement {
|
||||
@@ -47,6 +48,8 @@ export class CloudLogin extends LitElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _checkConnection = true;
|
||||
|
||||
@query("#email", true) private _emailField!: HaTextField;
|
||||
|
||||
@query("#password", true) private _passwordField!: HaPasswordField;
|
||||
@@ -244,6 +247,7 @@ export class CloudLogin extends LitElement {
|
||||
hass: this.hass,
|
||||
email: username,
|
||||
...(code ? { code } : { password }),
|
||||
check_connection: this._checkConnection,
|
||||
});
|
||||
this.email = "";
|
||||
this._password = "";
|
||||
@@ -283,6 +287,21 @@ export class CloudLogin extends LitElement {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (errCode === "alreadyconnectederror") {
|
||||
showCloudAlreadyConnectedDialog(this, {
|
||||
details: JSON.parse(err.body.message),
|
||||
logInHereAction: () => {
|
||||
this._checkConnection = false;
|
||||
doLogin(username);
|
||||
},
|
||||
closeDialog: () => {
|
||||
this._requestInProgress = false;
|
||||
this.email = "";
|
||||
this._password = "";
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (errCode === "PasswordChangeRequired") {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
|
||||
@@ -355,6 +355,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
showNarrow: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
minWidth: "80px",
|
||||
maxWidth: "80px",
|
||||
template: (entry) =>
|
||||
entry.unavailable ||
|
||||
entry.disabled_by ||
|
||||
|
||||
@@ -346,9 +346,11 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
groupable: true,
|
||||
},
|
||||
editable: {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.helpers.picker.headers.editable"),
|
||||
title: localize("ui.panel.config.helpers.picker.headers.editable"),
|
||||
type: "icon",
|
||||
sortable: true,
|
||||
minWidth: "88px",
|
||||
maxWidth: "88px",
|
||||
showNarrow: true,
|
||||
template: (helper) => html`
|
||||
${!helper.editable
|
||||
|
||||
@@ -185,6 +185,14 @@ class AddIntegrationDialog extends LitElement {
|
||||
const yamlIntegrations: IntegrationListItem[] = [];
|
||||
|
||||
Object.entries(i).forEach(([domain, integration]) => {
|
||||
if (
|
||||
"integration_type" in integration &&
|
||||
integration.integration_type === "hardware"
|
||||
) {
|
||||
// Ignore hardware integrations, they cannot be added via UI
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
"integration_type" in integration &&
|
||||
(integration.config_flow ||
|
||||
|
||||
@@ -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(
|
||||
|
||||
+24
-10
@@ -76,6 +76,8 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
@state()
|
||||
private _statistics?: ZWaveJSControllerStatisticsUpdatedMessage;
|
||||
|
||||
private _dialogOpen = false;
|
||||
|
||||
protected async firstUpdated() {
|
||||
if (this.hass) {
|
||||
await this._fetchData();
|
||||
@@ -104,11 +106,17 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
),
|
||||
subscribeS2Inclusion(this.hass, this.configEntryId, (message) => {
|
||||
showZWaveJSAddNodeDialog(this, {
|
||||
entry_id: this.configEntryId,
|
||||
dsk: message.dsk,
|
||||
onStop: () => setTimeout(() => this._fetchData(), 100),
|
||||
});
|
||||
if (!this._dialogOpen) {
|
||||
showZWaveJSAddNodeDialog(this, {
|
||||
entry_id: this.configEntryId,
|
||||
dsk: message.dsk,
|
||||
onStop: () => {
|
||||
setTimeout(() => this._fetchData(), 100);
|
||||
this._dialogOpen = false;
|
||||
},
|
||||
});
|
||||
this._dialogOpen = true;
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
@@ -570,11 +578,17 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private async _addNodeClicked() {
|
||||
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),
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private async _removeNodeClicked() {
|
||||
|
||||
@@ -153,7 +153,6 @@ class ConfigUrlForm extends LitElement {
|
||||
? html`
|
||||
<ha-icon-button
|
||||
class="toggle-unmasked-url"
|
||||
toggles
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.common.${this._unmaskedExternalUrl ? "hide" : "show"}_url`
|
||||
)}
|
||||
@@ -254,7 +253,6 @@ class ConfigUrlForm extends LitElement {
|
||||
? html`
|
||||
<ha-icon-button
|
||||
class="toggle-unmasked-url"
|
||||
toggles
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.common.${this._unmaskedInternalUrl ? "hide" : "show"}_url`
|
||||
)}
|
||||
|
||||
@@ -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));
|
||||
height: calc(100vh - 1px - var(--header-height) - 48px);
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,40 +20,31 @@ export class HuiCardFeatures extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<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>
|
||||
${this.features.map(
|
||||
(feature) => html`
|
||||
<hui-card-feature
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
.color=${this.color}
|
||||
.feature=${feature}
|
||||
></hui-card-feature>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
padding: var(--feature-padding);
|
||||
padding-top: 0px;
|
||||
gap: var(--feature-padding);
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { mdiRestore, mdiPlus, mdiMinus } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import "../../../components/ha-control-select";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
||||
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||
import { COUNTER_ACTIONS, type CounterActionsCardFeatureConfig } from "./types";
|
||||
import "../../../components/ha-control-button-group";
|
||||
import "../../../components/ha-control-button";
|
||||
|
||||
export const supportsCounterActionsCardFeature = (stateObj: HassEntity) => {
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return domain === "counter";
|
||||
};
|
||||
|
||||
interface CounterButton {
|
||||
translationKey: string;
|
||||
icon: string;
|
||||
serviceName: string;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export const COUNTER_ACTIONS_BUTTON: Record<
|
||||
string,
|
||||
(stateObj: HassEntity) => CounterButton
|
||||
> = {
|
||||
increment: (stateObj) => ({
|
||||
translationKey: "increment",
|
||||
icon: mdiPlus,
|
||||
serviceName: "increment",
|
||||
disabled: parseInt(stateObj.state) === stateObj.attributes.maximum,
|
||||
}),
|
||||
reset: () => ({
|
||||
translationKey: "reset",
|
||||
icon: mdiRestore,
|
||||
serviceName: "reset",
|
||||
disabled: false,
|
||||
}),
|
||||
decrement: (stateObj) => ({
|
||||
translationKey: "decrement",
|
||||
icon: mdiMinus,
|
||||
serviceName: "decrement",
|
||||
disabled: parseInt(stateObj.state) === stateObj.attributes.minimum,
|
||||
}),
|
||||
};
|
||||
|
||||
@customElement("hui-counter-actions-card-feature")
|
||||
class HuiCounterActionsCardFeature
|
||||
extends LitElement
|
||||
implements LovelaceCardFeature
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
@state() private _config?: CounterActionsCardFeatureConfig;
|
||||
|
||||
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
|
||||
await import(
|
||||
"../editor/config-elements/hui-counter-actions-card-feature-editor"
|
||||
);
|
||||
return document.createElement("hui-counter-actions-card-feature-editor");
|
||||
}
|
||||
|
||||
static getStubConfig(): CounterActionsCardFeatureConfig {
|
||||
return {
|
||||
type: "counter-actions",
|
||||
actions: COUNTER_ACTIONS.map((action) => action),
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: CounterActionsCardFeatureConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | null {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.stateObj ||
|
||||
!supportsCounterActionsCardFeature(this.stateObj)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-control-button-group>
|
||||
${this._config?.actions
|
||||
?.filter((action) => COUNTER_ACTIONS.includes(action))
|
||||
.map((action) => {
|
||||
const button = COUNTER_ACTIONS_BUTTON[action](this.stateObj!);
|
||||
return html`
|
||||
<ha-control-button
|
||||
.entry=${button}
|
||||
.label=${this.hass!.localize(
|
||||
// @ts-ignore
|
||||
`ui.card.counter.actions.${button.translationKey}`
|
||||
)}
|
||||
@click=${this._onActionTap}
|
||||
.disabled=${button.disabled ||
|
||||
this.stateObj?.state === UNAVAILABLE}
|
||||
>
|
||||
<ha-svg-icon .path=${button.icon}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
`;
|
||||
})}
|
||||
</ha-control-button-group>
|
||||
`;
|
||||
}
|
||||
|
||||
private _onActionTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
const entry = (ev.target! as any).entry as CounterButton;
|
||||
this.hass!.callService("counter", entry.serviceName, {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = cardFeatureStyles;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-counter-actions-card-feature": HuiCounterActionsCardFeature;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { mdiPowerOff, mdiPower } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import "../../../components/ha-control-select";
|
||||
import type { ControlSelectOption } from "../../../components/ha-control-select";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceCardFeature } from "../types";
|
||||
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||
import type { ToggleCardFeatureConfig } from "./types";
|
||||
import { showToast } from "../../../util/toast";
|
||||
|
||||
export const supportsToggleCardFeature = (stateObj: HassEntity) => {
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return ["switch", "input_boolean"].includes(domain);
|
||||
};
|
||||
|
||||
@customElement("hui-toggle-card-feature")
|
||||
class HuiToggleCardFeature extends LitElement implements LovelaceCardFeature {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
@state() private _config?: ToggleCardFeatureConfig;
|
||||
|
||||
static getStubConfig(): ToggleCardFeatureConfig {
|
||||
return {
|
||||
type: "toggle",
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: ToggleCardFeatureConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | null {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.stateObj ||
|
||||
!supportsToggleCardFeature(this.stateObj)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const color = stateColorCss(this.stateObj);
|
||||
|
||||
const options = ["on", "off"].map<ControlSelectOption>((entityState) => ({
|
||||
value: entityState,
|
||||
label: this.hass!.formatEntityState(this.stateObj!, entityState),
|
||||
path: entityState === "on" ? mdiPower : mdiPowerOff,
|
||||
}));
|
||||
|
||||
return html`
|
||||
<ha-control-select
|
||||
.options=${options}
|
||||
.value=${this.stateObj.state}
|
||||
@value-changed=${this._valueChanged}
|
||||
hide-label
|
||||
.ariaLabel=${this.hass.localize("ui.card.humidifier.state")}
|
||||
style=${styleMap({
|
||||
"--control-select-color": color,
|
||||
})}
|
||||
.disabled=${this.stateObj!.state === UNAVAILABLE}
|
||||
>
|
||||
</ha-control-select>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _valueChanged(ev: CustomEvent) {
|
||||
const newState = (ev.detail as any).value;
|
||||
|
||||
if (
|
||||
newState === this.stateObj!.state &&
|
||||
!this.stateObj!.attributes.assumed_state
|
||||
)
|
||||
return;
|
||||
const service = newState === "on" ? "turn_on" : "turn_off";
|
||||
const domain = computeDomain(this.stateObj!.entity_id);
|
||||
|
||||
try {
|
||||
await this.hass!.callService(domain, service, {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
});
|
||||
} catch (_err) {
|
||||
showToast(this, {
|
||||
message: this.hass!.localize("ui.notification_toast.action_failed", {
|
||||
service: domain + "." + service,
|
||||
}),
|
||||
duration: 5000,
|
||||
dismissable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static styles = cardFeatureStyles;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-toggle-card-feature": HuiToggleCardFeature;
|
||||
}
|
||||
}
|
||||
@@ -83,6 +83,15 @@ export interface ClimatePresetModesCardFeatureConfig {
|
||||
preset_modes?: string[];
|
||||
}
|
||||
|
||||
export const COUNTER_ACTIONS = ["increment", "reset", "decrement"] as const;
|
||||
|
||||
export type CounterActions = (typeof COUNTER_ACTIONS)[number];
|
||||
|
||||
export interface CounterActionsCardFeatureConfig {
|
||||
type: "counter-actions";
|
||||
actions?: CounterActions[];
|
||||
}
|
||||
|
||||
export interface SelectOptionsCardFeatureConfig {
|
||||
type: "select-options";
|
||||
options?: string[];
|
||||
@@ -101,6 +110,10 @@ export interface TargetTemperatureCardFeatureConfig {
|
||||
type: "target-temperature";
|
||||
}
|
||||
|
||||
export interface ToggleCardFeatureConfig {
|
||||
type: "toggle";
|
||||
}
|
||||
|
||||
export interface WaterHeaterOperationModesCardFeatureConfig {
|
||||
type: "water-heater-operation-modes";
|
||||
operation_modes?: OperationMode[];
|
||||
@@ -152,6 +165,7 @@ export type LovelaceCardFeatureConfig =
|
||||
| ClimateSwingHorizontalModesCardFeatureConfig
|
||||
| ClimateHvacModesCardFeatureConfig
|
||||
| ClimatePresetModesCardFeatureConfig
|
||||
| CounterActionsCardFeatureConfig
|
||||
| CoverOpenCloseCardFeatureConfig
|
||||
| CoverPositionCardFeatureConfig
|
||||
| CoverTiltPositionCardFeatureConfig
|
||||
@@ -170,6 +184,7 @@ export type LovelaceCardFeatureConfig =
|
||||
| SelectOptionsCardFeatureConfig
|
||||
| TargetHumidityCardFeatureConfig
|
||||
| TargetTemperatureCardFeatureConfig
|
||||
| ToggleCardFeatureConfig
|
||||
| UpdateActionsCardFeatureConfig
|
||||
| VacuumCommandsCardFeatureConfig
|
||||
| WaterHeaterOperationModesCardFeatureConfig;
|
||||
|
||||
@@ -327,17 +327,19 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
);
|
||||
|
||||
const untrackedConsumption: BarSeriesOption["data"] = [];
|
||||
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);
|
||||
});
|
||||
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);
|
||||
});
|
||||
// random id to always add untracked at the end
|
||||
const order = Date.now();
|
||||
const dataset: BarSeriesOption = {
|
||||
@@ -448,7 +450,15 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
});
|
||||
});
|
||||
return sorted_devices
|
||||
.map((device) => data.find((d) => (d.id as string).includes(device))!)
|
||||
.map(
|
||||
(device) =>
|
||||
data.find((d) => {
|
||||
const id = (d.id as string)
|
||||
.replace(/^compare-/, "") // Remove compare- prefix
|
||||
.replace(/-\d+$/, ""); // Remove numeric suffix
|
||||
return id === device;
|
||||
})!
|
||||
)
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
|
||||
@@ -291,20 +291,19 @@ export class HuiEnergyUsageGraphCard
|
||||
true
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// add empty dataset so compare bars are first
|
||||
// `stack: usage` so it doesn't take up space yet
|
||||
const firstId = statIds.from_grid?.[0] ?? "placeholder";
|
||||
datasets.push({
|
||||
id: "compare-" + firstId,
|
||||
type: "bar",
|
||||
stack: "usage",
|
||||
data: [],
|
||||
// @ts-expect-error
|
||||
order: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// add empty dataset so compare bars are first
|
||||
// `stack: usage` so it doesn't take up space yet
|
||||
datasets.push({
|
||||
id: "compare-placeholder",
|
||||
type: "bar",
|
||||
stack: energyData.statsCompare ? "compare" : "usage",
|
||||
data: [],
|
||||
// @ts-expect-error
|
||||
order: 0,
|
||||
});
|
||||
|
||||
datasets.push(
|
||||
...this._processDataSet(
|
||||
energyData.stats,
|
||||
|
||||
@@ -256,6 +256,7 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
hui-card-features {
|
||||
width: 100%;
|
||||
flex: none;
|
||||
padding: 0 12px 12px 12px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { mdiImageFilterCenterFocus } from "@mdi/js";
|
||||
import {
|
||||
mdiDotsHexagon,
|
||||
mdiGoogleCirclesCommunities,
|
||||
mdiImageFilterCenterFocus,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import type { LatLngTuple } from "leaflet";
|
||||
import type { PropertyValues } from "lit";
|
||||
@@ -72,6 +76,8 @@ 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 {
|
||||
@@ -170,18 +176,32 @@ 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>
|
||||
<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 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>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
@@ -320,6 +340,10 @@ 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) {
|
||||
@@ -464,11 +488,12 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
ha-icon-button {
|
||||
#buttons {
|
||||
position: absolute;
|
||||
top: 75px;
|
||||
left: 3px;
|
||||
outline: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#root {
|
||||
|
||||
@@ -3,17 +3,21 @@ 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 { fireEvent } from "../../../common/dom/fire_event";
|
||||
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 "../../../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> {
|
||||
@@ -68,9 +72,32 @@ 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() {
|
||||
@@ -80,17 +107,26 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
||||
|
||||
return html`
|
||||
${this._error
|
||||
? html`<ha-alert
|
||||
alert-type=${this._errorLevel?.toLowerCase() || "error"}
|
||||
>${this._error}</ha-alert
|
||||
>`
|
||||
? html`
|
||||
<ha-alert
|
||||
.alertType=${(this._errorLevel?.toLowerCase() as
|
||||
| "error"
|
||||
| "warning") || "error"}
|
||||
>
|
||||
${this._error}
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
<ha-card .header=${this._config.title}>
|
||||
<ha-card
|
||||
.header=${!this._config.text_only ? this._config.title : undefined}
|
||||
class=${classMap({
|
||||
"with-header": !!this._config.title,
|
||||
"text-only": this._config.text_only ?? false,
|
||||
})}
|
||||
>
|
||||
<ha-markdown
|
||||
cache
|
||||
breaks
|
||||
class=${classMap({
|
||||
"no-header": !this._config.title,
|
||||
})}
|
||||
.content=${this._templateResult?.result}
|
||||
></ha-markdown>
|
||||
</ha-card>
|
||||
@@ -107,7 +143,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) {
|
||||
@@ -200,11 +236,19 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
ha-markdown {
|
||||
padding: 0 16px 16px;
|
||||
padding: 16px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
ha-markdown.no-header {
|
||||
padding-top: 16px;
|
||||
.with-header ha-markdown {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
.text-only {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
}
|
||||
.text-only ha-markdown {
|
||||
padding: 2px 4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -327,7 +327,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
|
||||
);
|
||||
const endDate = this._energyEnd;
|
||||
try {
|
||||
let unitClass;
|
||||
let unitClass: string | undefined | null;
|
||||
if (this._config!.unit && this._metadata) {
|
||||
const metadata = Object.values(this._metadata).find(
|
||||
(metaData) =>
|
||||
|
||||
@@ -248,6 +248,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
hui-card-features {
|
||||
width: 100%;
|
||||
flex: none;
|
||||
padding: 0 12px 12px 12px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -18,8 +18,7 @@ import "../../../components/ha-state-icon";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/tile/ha-tile-badge";
|
||||
import "../../../components/tile/ha-tile-icon";
|
||||
import "../../../components/tile/ha-tile-image";
|
||||
import type { TileImageStyle } from "../../../components/tile/ha-tile-image";
|
||||
import type { TileIconImageStyle } from "../../../components/tile/ha-tile-icon";
|
||||
import "../../../components/tile/ha-tile-info";
|
||||
import { cameraUrlWithWidthHeight } from "../../../data/camera";
|
||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
@@ -36,7 +35,7 @@ import type {
|
||||
LovelaceGridOptions,
|
||||
} from "../types";
|
||||
import { renderTileBadge } from "./tile/badges/tile-badge";
|
||||
import type { ThermostatCardConfig, TileCardConfig } from "./types";
|
||||
import type { TileCardConfig } from "./types";
|
||||
|
||||
export const getEntityDefaultTileIconAction = (entityId: string) => {
|
||||
const domain = computeDomain(entityId);
|
||||
@@ -44,10 +43,10 @@ export const getEntityDefaultTileIconAction = (entityId: string) => {
|
||||
DOMAINS_TOGGLE.has(domain) ||
|
||||
["button", "input_button", "scene"].includes(domain);
|
||||
|
||||
return supportsIconAction ? "toggle" : "more-info";
|
||||
return supportsIconAction ? "toggle" : "none";
|
||||
};
|
||||
|
||||
const DOMAIN_IMAGE_STYLE: Record<string, TileImageStyle> = {
|
||||
const DOMAIN_IMAGE_SHAPE: Record<string, TileIconImageStyle> = {
|
||||
update: "square",
|
||||
media_player: "rounded-square",
|
||||
};
|
||||
@@ -84,7 +83,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@state() private _config?: TileCardConfig;
|
||||
|
||||
public setConfig(config: ThermostatCardConfig): void {
|
||||
public setConfig(config: TileCardConfig): void {
|
||||
if (!config.entity) {
|
||||
throw new Error("Specify an entity");
|
||||
}
|
||||
@@ -101,10 +100,13 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
const featuresPosition =
|
||||
this._config && this._featurePosition(this._config);
|
||||
const featuresCount = this._config?.features?.length || 0;
|
||||
return (
|
||||
1 +
|
||||
(this._config?.vertical ? 1 : 0) +
|
||||
(this._config?.features?.length || 0)
|
||||
(featuresPosition === "inline" ? 0 : featuresCount)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -112,9 +114,16 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
const columns = 6;
|
||||
let min_columns = 6;
|
||||
let rows = 1;
|
||||
if (this._config?.features?.length) {
|
||||
rows += this._config.features.length;
|
||||
const featurePosition = this._config && this._featurePosition(this._config);
|
||||
const featuresCount = this._config?.features?.length || 0;
|
||||
if (featuresCount) {
|
||||
if (featurePosition === "inline") {
|
||||
min_columns = 12;
|
||||
} else {
|
||||
rows += featuresCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._config?.vertical) {
|
||||
rows++;
|
||||
min_columns = 3;
|
||||
@@ -196,7 +205,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
);
|
||||
|
||||
get hasCardAction() {
|
||||
private get _hasCardAction() {
|
||||
return (
|
||||
!this._config?.tap_action ||
|
||||
hasAction(this._config?.tap_action) ||
|
||||
@@ -205,12 +214,29 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
);
|
||||
}
|
||||
|
||||
get hasIconAction() {
|
||||
private get _hasIconAction() {
|
||||
return (
|
||||
!this._config?.icon_tap_action || hasAction(this._config?.icon_tap_action)
|
||||
);
|
||||
}
|
||||
|
||||
private _featurePosition = memoizeOne((config: TileCardConfig) => {
|
||||
if (config.vertical) {
|
||||
return "bottom";
|
||||
}
|
||||
return config.features_position || "bottom";
|
||||
});
|
||||
|
||||
private _displayedFeatures = memoizeOne((config: TileCardConfig) => {
|
||||
const features = config.features || [];
|
||||
const featurePosition = this._featurePosition(config);
|
||||
|
||||
if (featurePosition === "inline") {
|
||||
return features.slice(0, 1);
|
||||
}
|
||||
return features;
|
||||
});
|
||||
|
||||
protected render() {
|
||||
if (!this._config || !this.hass) {
|
||||
return nothing;
|
||||
@@ -224,14 +250,12 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="content ${classMap(contentClasses)}">
|
||||
<div class="icon-container">
|
||||
<ha-tile-icon>
|
||||
<ha-svg-icon .path=${mdiHelp}></ha-svg-icon>
|
||||
</ha-tile-icon>
|
||||
<ha-tile-icon>
|
||||
<ha-svg-icon slot="icon" .path=${mdiHelp}></ha-svg-icon>
|
||||
<ha-tile-badge class="not-found">
|
||||
<ha-svg-icon .path=${mdiExclamationThick}></ha-svg-icon>
|
||||
</ha-tile-badge>
|
||||
</div>
|
||||
</ha-tile-icon>
|
||||
<ha-tile-info
|
||||
.primary=${entityId}
|
||||
secondary=${this.hass.localize("ui.card.tile.not_found")}
|
||||
@@ -266,6 +290,12 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
? this._getImageUrl(stateObj)
|
||||
: undefined;
|
||||
|
||||
const featurePosition = this._featurePosition(this._config);
|
||||
const features = this._displayedFeatures(this._config);
|
||||
|
||||
const containerOrientationClass =
|
||||
featurePosition === "inline" ? "horizontal" : "";
|
||||
|
||||
return html`
|
||||
<ha-card style=${styleMap(style)} class=${classMap({ active })}>
|
||||
<div
|
||||
@@ -275,58 +305,49 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
hasHold: hasAction(this._config!.hold_action),
|
||||
hasDoubleClick: hasAction(this._config!.double_tap_action),
|
||||
})}
|
||||
role=${ifDefined(this.hasCardAction ? "button" : undefined)}
|
||||
tabindex=${ifDefined(this.hasCardAction ? "0" : undefined)}
|
||||
role=${ifDefined(this._hasCardAction ? "button" : undefined)}
|
||||
tabindex=${ifDefined(this._hasCardAction ? "0" : undefined)}
|
||||
aria-labelledby="info"
|
||||
>
|
||||
<ha-ripple .disabled=${!this.hasCardAction}></ha-ripple>
|
||||
<ha-ripple .disabled=${!this._hasCardAction}></ha-ripple>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="container ${containerOrientationClass}">
|
||||
<div class="content ${classMap(contentClasses)}">
|
||||
<div
|
||||
class="icon-container"
|
||||
role=${ifDefined(this.hasIconAction ? "button" : undefined)}
|
||||
tabindex=${ifDefined(this.hasIconAction ? "0" : undefined)}
|
||||
<ha-tile-icon
|
||||
role=${ifDefined(this._hasIconAction ? "button" : undefined)}
|
||||
tabindex=${ifDefined(this._hasIconAction ? "0" : undefined)}
|
||||
@action=${this._handleIconAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(this._config!.icon_hold_action),
|
||||
hasDoubleClick: hasAction(this._config!.icon_double_tap_action),
|
||||
})}
|
||||
.interactive=${this._hasIconAction}
|
||||
.imageStyle=${DOMAIN_IMAGE_SHAPE[domain]}
|
||||
.imageUrl=${imageUrl}
|
||||
data-domain=${ifDefined(domain)}
|
||||
data-state=${ifDefined(stateObj?.state)}
|
||||
>
|
||||
${imageUrl
|
||||
? html`
|
||||
<ha-tile-image
|
||||
.imageStyle=${DOMAIN_IMAGE_STYLE[domain] || "circle"}
|
||||
.imageUrl=${imageUrl}
|
||||
></ha-tile-image>
|
||||
`
|
||||
: html`
|
||||
<ha-tile-icon
|
||||
data-domain=${ifDefined(domain)}
|
||||
data-state=${ifDefined(stateObj?.state)}
|
||||
>
|
||||
<ha-state-icon
|
||||
.icon=${this._config.icon}
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
></ha-state-icon>
|
||||
</ha-tile-icon>
|
||||
`}
|
||||
<ha-state-icon
|
||||
slot="icon"
|
||||
.icon=${this._config.icon}
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
></ha-state-icon>
|
||||
${renderTileBadge(stateObj, this.hass)}
|
||||
</div>
|
||||
</ha-tile-icon>
|
||||
<ha-tile-info
|
||||
id="info"
|
||||
.primary=${name}
|
||||
.secondary=${stateDisplay}
|
||||
></ha-tile-info>
|
||||
</div>
|
||||
${this._config.features
|
||||
${features.length > 0
|
||||
? html`
|
||||
<hui-card-features
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.color=${this._config.color}
|
||||
.features=${this._config.features}
|
||||
.features=${features}
|
||||
></hui-card-features>
|
||||
`
|
||||
: nothing}
|
||||
@@ -363,6 +384,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
[role="button"]:focus {
|
||||
outline: none;
|
||||
@@ -383,6 +405,10 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
.container.horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -390,55 +416,34 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.vertical {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.vertical .icon-container {
|
||||
margin-bottom: 10px;
|
||||
margin-right: 0;
|
||||
margin-inline-start: initial;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
.vertical ha-tile-info {
|
||||
width: 100%;
|
||||
flex: none;
|
||||
}
|
||||
.icon-container {
|
||||
position: relative;
|
||||
flex: none;
|
||||
margin-right: 10px;
|
||||
margin-inline-start: initial;
|
||||
margin-inline-end: 10px;
|
||||
direction: var(--direction);
|
||||
transition: transform 180ms ease-in-out;
|
||||
}
|
||||
.icon-container ha-tile-icon,
|
||||
.icon-container ha-tile-image {
|
||||
ha-tile-icon {
|
||||
--tile-icon-color: var(--tile-color);
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
position: relative;
|
||||
padding: 6px;
|
||||
margin: -6px;
|
||||
}
|
||||
.icon-container ha-tile-badge {
|
||||
ha-tile-badge {
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
right: -3px;
|
||||
inset-inline-end: -3px;
|
||||
top: 3px;
|
||||
right: 3px;
|
||||
inset-inline-end: 3px;
|
||||
inset-inline-start: initial;
|
||||
}
|
||||
.icon-container[role="button"] {
|
||||
pointer-events: auto;
|
||||
}
|
||||
.icon-container[role="button"]:focus-visible,
|
||||
.icon-container[role="button"]:active {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
ha-tile-info {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
@@ -447,6 +452,14 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
hui-card-features {
|
||||
--feature-color: var(--tile-color);
|
||||
padding: 0 12px 12px 12px;
|
||||
}
|
||||
.container.horizontal hui-card-features {
|
||||
width: calc(50% - var(--column-gap, 0px) / 2 - 12px);
|
||||
flex: none;
|
||||
--feature-height: 36px;
|
||||
padding: 0 12px;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
ha-tile-icon[data-domain="alarm_control_panel"][data-state="pending"],
|
||||
|
||||
@@ -37,6 +37,7 @@ import type {
|
||||
LovelaceGridOptions,
|
||||
} from "../types";
|
||||
import type { WeatherForecastCardConfig } from "./types";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
|
||||
@customElement("hui-weather-forecast-card")
|
||||
class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
@@ -106,7 +107,9 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
!this.isConnected ||
|
||||
!this.hass ||
|
||||
!this._config ||
|
||||
!this._needForecastSubscription()
|
||||
!this._needForecastSubscription() ||
|
||||
!isComponentLoaded(this.hass, "weather") ||
|
||||
!this.hass.states[this._config!.entity]
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -118,7 +121,14 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
(event) => {
|
||||
this._forecastEvent = event;
|
||||
}
|
||||
);
|
||||
).catch((e) => {
|
||||
if (e.code === "invalid_entity_id") {
|
||||
setTimeout(() => {
|
||||
this._subscribed = undefined;
|
||||
}, 2000);
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
|
||||
@@ -336,6 +336,7 @@ export interface MapCardConfig extends LovelaceCardConfig {
|
||||
export interface MarkdownCardConfig extends LovelaceCardConfig {
|
||||
type: "markdown";
|
||||
content: string;
|
||||
text_only?: boolean;
|
||||
title?: string;
|
||||
card_size?: number;
|
||||
entity_ids?: string | string[];
|
||||
@@ -533,6 +534,7 @@ export interface TileCardConfig extends LovelaceCardConfig {
|
||||
icon_hold_action?: ActionConfig;
|
||||
icon_double_tap_action?: ActionConfig;
|
||||
features?: LovelaceCardFeatureConfig[];
|
||||
features_position?: "bottom" | "inline";
|
||||
}
|
||||
|
||||
export interface HeadingCardConfig extends LovelaceCardConfig {
|
||||
|
||||
@@ -23,8 +23,10 @@ import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||
import { addCard } from "../editor/config-util";
|
||||
import type { LovelaceCardPath } from "../editor/lovelace-path";
|
||||
import {
|
||||
findLovelaceContainer,
|
||||
findLovelaceItems,
|
||||
getLovelaceContainerPath,
|
||||
parseLovelaceCardPath,
|
||||
@@ -253,14 +255,24 @@ export class HuiCardEditMode extends LitElement {
|
||||
}
|
||||
|
||||
private _duplicateCard(): void {
|
||||
const { cardIndex } = parseLovelaceCardPath(this.path!);
|
||||
const { cardIndex, sectionIndex } = parseLovelaceCardPath(this.path!);
|
||||
const containerPath = getLovelaceContainerPath(this.path!);
|
||||
const sectionConfig =
|
||||
sectionIndex !== undefined
|
||||
? findLovelaceContainer(this.lovelace!.config, containerPath)
|
||||
: undefined;
|
||||
|
||||
const cardConfig = this._cards![cardIndex];
|
||||
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace!.config,
|
||||
saveConfig: this.lovelace!.saveConfig,
|
||||
path: containerPath,
|
||||
saveCardConfig: async (config) => {
|
||||
const newConfig = addCard(this.lovelace!.config, containerPath, config);
|
||||
await this.lovelace!.saveConfig(newConfig);
|
||||
},
|
||||
cardConfig,
|
||||
sectionConfig,
|
||||
isNew: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -278,9 +278,12 @@ export class HuiCardOptions extends LitElement {
|
||||
const cardConfig = this._cards![cardIndex];
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace!.config,
|
||||
saveConfig: this.lovelace!.saveConfig,
|
||||
path: containerPath,
|
||||
saveCardConfig: async (config) => {
|
||||
const newConfig = addCard(this.lovelace!.config, containerPath, config);
|
||||
await this.lovelace!.saveConfig(newConfig);
|
||||
},
|
||||
cardConfig,
|
||||
isNew: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
startOfWeek,
|
||||
startOfYear,
|
||||
subDays,
|
||||
subMonths,
|
||||
} from "date-fns";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
@@ -179,6 +180,30 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||
calcDate(today, startOfYear, this.hass.locale, this.hass.config),
|
||||
calcDate(today, endOfYear, this.hass.locale, this.hass.config),
|
||||
],
|
||||
[this.hass.localize("ui.components.date-range-picker.ranges.now-7d")]: [
|
||||
calcDate(today, subDays, this.hass.locale, this.hass.config, 7),
|
||||
calcDate(today, subDays, this.hass.locale, this.hass.config, 1),
|
||||
],
|
||||
[this.hass.localize("ui.components.date-range-picker.ranges.now-30d")]:
|
||||
[
|
||||
calcDate(today, subDays, this.hass.locale, this.hass.config, 30),
|
||||
calcDate(today, subDays, this.hass.locale, this.hass.config, 1),
|
||||
],
|
||||
[this.hass.localize("ui.components.date-range-picker.ranges.now-12m")]:
|
||||
[
|
||||
calcDate(
|
||||
subMonths(today, 12),
|
||||
startOfMonth,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
),
|
||||
calcDate(
|
||||
subMonths(today, 1),
|
||||
endOfMonth,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -248,6 +273,7 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||
.ranges=${this._ranges}
|
||||
@value-changed=${this._dateRangeChanged}
|
||||
minimal
|
||||
header-position
|
||||
></ha-date-range-picker>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -200,8 +200,6 @@ export class HuiGenericEntityRow extends LitElement {
|
||||
padding-inline-start: 16px;
|
||||
padding-inline-end: 8px;
|
||||
flex: 1 1 30%;
|
||||
min-height: 40px;
|
||||
align-content: center;
|
||||
}
|
||||
.info,
|
||||
.info > * {
|
||||
@@ -235,8 +233,6 @@ export class HuiGenericEntityRow extends LitElement {
|
||||
}
|
||||
.value {
|
||||
direction: ltr;
|
||||
min-height: 40px;
|
||||
align-content: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import "../card-features/hui-climate-swing-modes-card-feature";
|
||||
import "../card-features/hui-climate-swing-horizontal-modes-card-feature";
|
||||
import "../card-features/hui-climate-hvac-modes-card-feature";
|
||||
import "../card-features/hui-climate-preset-modes-card-feature";
|
||||
import "../card-features/hui-counter-actions-card-feature";
|
||||
import "../card-features/hui-cover-open-close-card-feature";
|
||||
import "../card-features/hui-cover-position-card-feature";
|
||||
import "../card-features/hui-cover-tilt-card-feature";
|
||||
@@ -22,6 +23,7 @@ import "../card-features/hui-numeric-input-card-feature";
|
||||
import "../card-features/hui-select-options-card-feature";
|
||||
import "../card-features/hui-target-temperature-card-feature";
|
||||
import "../card-features/hui-target-humidity-card-feature";
|
||||
import "../card-features/hui-toggle-card-feature";
|
||||
import "../card-features/hui-update-actions-card-feature";
|
||||
import "../card-features/hui-vacuum-commands-card-feature";
|
||||
import "../card-features/hui-water-heater-operation-modes-card-feature";
|
||||
@@ -39,6 +41,7 @@ const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([
|
||||
"climate-swing-horizontal-modes",
|
||||
"climate-hvac-modes",
|
||||
"climate-preset-modes",
|
||||
"counter-actions",
|
||||
"cover-open-close",
|
||||
"cover-position",
|
||||
"cover-tilt-position",
|
||||
@@ -57,6 +60,7 @@ const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([
|
||||
"select-options",
|
||||
"target-humidity",
|
||||
"target-temperature",
|
||||
"toggle",
|
||||
"update-actions",
|
||||
"vacuum-commands",
|
||||
"water-heater-operation-modes",
|
||||
|
||||
@@ -3,10 +3,10 @@ import "@material/mwc-tab/mwc-tab";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoize from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
computeCards,
|
||||
computeSection,
|
||||
} from "../../common/generate-lovelace-config";
|
||||
import { addCard } from "../config-util";
|
||||
import {
|
||||
findLovelaceContainer,
|
||||
parseLovelaceContainerPath,
|
||||
@@ -241,11 +242,24 @@ export class HuiCreateDialogCard
|
||||
}
|
||||
}
|
||||
|
||||
const lovelaceConfig = this._params!.lovelaceConfig;
|
||||
const containerPath = this._params!.path;
|
||||
const saveConfig = this._params!.saveConfig;
|
||||
|
||||
const sectionConfig =
|
||||
containerPath.length === 2
|
||||
? findLovelaceContainer(lovelaceConfig, containerPath)
|
||||
: undefined;
|
||||
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this._params!.lovelaceConfig,
|
||||
saveConfig: this._params!.saveConfig,
|
||||
path: this._params!.path,
|
||||
lovelaceConfig,
|
||||
saveCardConfig: async (newCardConfig) => {
|
||||
const newConfig = addCard(lovelaceConfig, containerPath, newCardConfig);
|
||||
await saveConfig(newConfig);
|
||||
},
|
||||
cardConfig: config,
|
||||
sectionConfig,
|
||||
isNew: true,
|
||||
});
|
||||
|
||||
this.closeDialog();
|
||||
|
||||
@@ -13,7 +13,6 @@ import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import {
|
||||
getCustomCardEntry,
|
||||
isCustomType,
|
||||
@@ -23,13 +22,12 @@ import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||
import "../../cards/hui-card";
|
||||
import "../../sections/hui-section";
|
||||
import { addCard, replaceCard } from "../config-util";
|
||||
import { getCardDocumentationURL } from "../get-dashboard-documentation-url";
|
||||
import type { ConfigChangedEvent } from "../hui-element-editor";
|
||||
import { findLovelaceContainer } from "../lovelace-path";
|
||||
import type { GUIModeChangedEvent } from "../types";
|
||||
import "./hui-card-element-editor";
|
||||
import type { HuiCardElementEditor } from "./hui-card-element-editor";
|
||||
@@ -59,9 +57,7 @@ export class HuiDialogEditCard
|
||||
|
||||
@state() private _cardConfig?: LovelaceCardConfig;
|
||||
|
||||
@state() private _containerConfig!:
|
||||
| LovelaceViewConfig
|
||||
| LovelaceSectionConfig;
|
||||
@state() private _sectionConfig?: LovelaceSectionConfig;
|
||||
|
||||
@state() private _saving = false;
|
||||
|
||||
@@ -85,23 +81,10 @@ export class HuiDialogEditCard
|
||||
this._GUImode = true;
|
||||
this._guiModeAvailable = true;
|
||||
|
||||
const containerConfig = findLovelaceContainer(
|
||||
params.lovelaceConfig,
|
||||
params.path
|
||||
);
|
||||
this._sectionConfig = this._params.sectionConfig;
|
||||
|
||||
if ("strategy" in containerConfig) {
|
||||
throw new Error("Can't edit strategy");
|
||||
}
|
||||
|
||||
this._containerConfig = containerConfig;
|
||||
|
||||
if ("cardConfig" in params) {
|
||||
this._cardConfig = params.cardConfig;
|
||||
this._dirty = true;
|
||||
} else {
|
||||
this._cardConfig = this._containerConfig.cards?.[params.cardIndex];
|
||||
}
|
||||
this._cardConfig = params.cardConfig;
|
||||
this._dirty = Boolean(this._params.isNew);
|
||||
|
||||
this.large = false;
|
||||
if (this._cardConfig && !Object.isFrozen(this._cardConfig)) {
|
||||
@@ -156,12 +139,12 @@ export class HuiDialogEditCard
|
||||
};
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
if (!this._params || !this._cardConfig) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
let heading: string;
|
||||
if (this._cardConfig && this._cardConfig.type) {
|
||||
if (this._cardConfig.type) {
|
||||
let cardName: string | undefined;
|
||||
if (isCustomType(this._cardConfig.type)) {
|
||||
// prettier-ignore
|
||||
@@ -181,13 +164,6 @@ export class HuiDialogEditCard
|
||||
"ui.panel.lovelace.editor.edit_card.typed_header",
|
||||
{ type: cardName }
|
||||
);
|
||||
} else if (!this._cardConfig) {
|
||||
heading = this._containerConfig.title
|
||||
? this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.pick_card_view_title",
|
||||
{ name: this._containerConfig.title }
|
||||
)
|
||||
: this.hass!.localize("ui.panel.lovelace.editor.edit_card.pick_card");
|
||||
} else {
|
||||
heading = this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.header"
|
||||
@@ -230,10 +206,8 @@ export class HuiDialogEditCard
|
||||
<div class="content">
|
||||
<div class="element-editor">
|
||||
<hui-card-element-editor
|
||||
.showVisibilityTab=${this._cardConfig?.type !== "conditional"}
|
||||
.sectionConfig=${this._isInSection
|
||||
? this._containerConfig
|
||||
: undefined}
|
||||
.showVisibilityTab=${this._cardConfig.type !== "conditional"}
|
||||
.sectionConfig=${this._sectionConfig}
|
||||
.hass=${this.hass}
|
||||
.lovelace=${this._params.lovelaceConfig}
|
||||
.value=${this._cardConfig}
|
||||
@@ -244,7 +218,7 @@ export class HuiDialogEditCard
|
||||
></hui-card-element-editor>
|
||||
</div>
|
||||
<div class="element-preview">
|
||||
${this._isInSection
|
||||
${this._sectionConfig
|
||||
? html`
|
||||
<hui-section
|
||||
.hass=${this.hass}
|
||||
@@ -345,14 +319,10 @@ export class HuiDialogEditCard
|
||||
this._cardEditorEl?.focusYamlEditor();
|
||||
}
|
||||
|
||||
private get _isInSection() {
|
||||
return this._params!.path.length === 2;
|
||||
}
|
||||
|
||||
private _cardConfigInSection = memoizeOne(
|
||||
(cardConfig?: LovelaceCardConfig) => {
|
||||
(cardConfig: LovelaceCardConfig) => {
|
||||
const { cards, title, ...containerConfig } = this
|
||||
._containerConfig as LovelaceSectionConfig;
|
||||
._sectionConfig as LovelaceSectionConfig;
|
||||
|
||||
return {
|
||||
...containerConfig,
|
||||
@@ -411,20 +381,18 @@ export class HuiDialogEditCard
|
||||
return;
|
||||
}
|
||||
this._saving = true;
|
||||
const path = this._params!.path;
|
||||
await this._params!.saveConfig(
|
||||
"cardConfig" in this._params!
|
||||
? addCard(this._params!.lovelaceConfig, path, this._cardConfig!)
|
||||
: replaceCard(
|
||||
this._params!.lovelaceConfig,
|
||||
[...path, this._params!.cardIndex],
|
||||
this._cardConfig!
|
||||
)
|
||||
);
|
||||
this._saving = false;
|
||||
this._dirty = false;
|
||||
showSaveSuccessToast(this, this.hass);
|
||||
this.closeDialog();
|
||||
try {
|
||||
await this._params!.saveCardConfig(this._cardConfig!);
|
||||
this._saving = false;
|
||||
this._dirty = false;
|
||||
showSaveSuccessToast(this, this.hass);
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
showToast(this, {
|
||||
message: err.message,
|
||||
});
|
||||
this._saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { LovelaceContainerPath } from "../lovelace-path";
|
||||
|
||||
export type EditCardDialogParams = {
|
||||
export interface EditCardDialogParams {
|
||||
lovelaceConfig: LovelaceConfig;
|
||||
saveConfig: (config: LovelaceConfig) => void;
|
||||
path: LovelaceContainerPath;
|
||||
} & (
|
||||
| {
|
||||
cardIndex: number;
|
||||
}
|
||||
| {
|
||||
cardConfig: LovelaceCardConfig;
|
||||
}
|
||||
);
|
||||
saveCardConfig: (config: LovelaceCardConfig) => void;
|
||||
cardConfig: LovelaceCardConfig;
|
||||
sectionConfig?: LovelaceSectionConfig;
|
||||
isNew?: boolean;
|
||||
}
|
||||
|
||||
export const importEditCardDialog = () => import("./hui-dialog-edit-card");
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import { supportsClimateHvacModesCardFeature } from "../../card-features/hui-cli
|
||||
import { supportsClimatePresetModesCardFeature } from "../../card-features/hui-climate-preset-modes-card-feature";
|
||||
import { supportsClimateSwingModesCardFeature } from "../../card-features/hui-climate-swing-modes-card-feature";
|
||||
import { supportsClimateSwingHorizontalModesCardFeature } from "../../card-features/hui-climate-swing-horizontal-modes-card-feature";
|
||||
import { supportsCounterActionsCardFeature } from "../../card-features/hui-counter-actions-card-feature";
|
||||
import { supportsCoverOpenCloseCardFeature } from "../../card-features/hui-cover-open-close-card-feature";
|
||||
import { supportsCoverPositionCardFeature } from "../../card-features/hui-cover-position-card-feature";
|
||||
import { supportsCoverTiltCardFeature } from "../../card-features/hui-cover-tilt-card-feature";
|
||||
@@ -42,6 +43,7 @@ import { supportsNumericInputCardFeature } from "../../card-features/hui-numeric
|
||||
import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature";
|
||||
import { supportsTargetHumidityCardFeature } from "../../card-features/hui-target-humidity-card-feature";
|
||||
import { supportsTargetTemperatureCardFeature } from "../../card-features/hui-target-temperature-card-feature";
|
||||
import { supportsToggleCardFeature } from "../../card-features/hui-toggle-card-feature";
|
||||
import { supportsUpdateActionsCardFeature } from "../../card-features/hui-update-actions-card-feature";
|
||||
import { supportsVacuumCommandsCardFeature } from "../../card-features/hui-vacuum-commands-card-feature";
|
||||
import { supportsWaterHeaterOperationModesCardFeature } from "../../card-features/hui-water-heater-operation-modes-card-feature";
|
||||
@@ -58,6 +60,7 @@ const UI_FEATURE_TYPES = [
|
||||
"climate-preset-modes",
|
||||
"climate-swing-modes",
|
||||
"climate-swing-horizontal-modes",
|
||||
"counter-actions",
|
||||
"cover-open-close",
|
||||
"cover-position",
|
||||
"cover-tilt-position",
|
||||
@@ -76,6 +79,7 @@ const UI_FEATURE_TYPES = [
|
||||
"select-options",
|
||||
"target-humidity",
|
||||
"target-temperature",
|
||||
"toggle",
|
||||
"update-actions",
|
||||
"vacuum-commands",
|
||||
"water-heater-operation-modes",
|
||||
@@ -90,6 +94,7 @@ const EDITABLES_FEATURE_TYPES = new Set<UiFeatureTypes>([
|
||||
"climate-preset-modes",
|
||||
"climate-swing-modes",
|
||||
"climate-swing-horizontal-modes",
|
||||
"counter-actions",
|
||||
"fan-preset-modes",
|
||||
"humidifier-modes",
|
||||
"lawn-mower-commands",
|
||||
@@ -111,6 +116,7 @@ const SUPPORTS_FEATURE_TYPES: Record<
|
||||
supportsClimateSwingHorizontalModesCardFeature,
|
||||
"climate-hvac-modes": supportsClimateHvacModesCardFeature,
|
||||
"climate-preset-modes": supportsClimatePresetModesCardFeature,
|
||||
"counter-actions": supportsCounterActionsCardFeature,
|
||||
"cover-open-close": supportsCoverOpenCloseCardFeature,
|
||||
"cover-position": supportsCoverPositionCardFeature,
|
||||
"cover-tilt-position": supportsCoverTiltPositionCardFeature,
|
||||
@@ -129,6 +135,7 @@ const SUPPORTS_FEATURE_TYPES: Record<
|
||||
"select-options": supportsSelectOptionsCardFeature,
|
||||
"target-humidity": supportsTargetHumidityCardFeature,
|
||||
"target-temperature": supportsTargetTemperatureCardFeature,
|
||||
toggle: supportsToggleCardFeature,
|
||||
"update-actions": supportsUpdateActionsCardFeature,
|
||||
"vacuum-commands": supportsVacuumCommandsCardFeature,
|
||||
"water-heater-operation-modes": supportsWaterHeaterOperationModesCardFeature,
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
COUNTER_ACTIONS,
|
||||
type LovelaceCardFeatureContext,
|
||||
type CounterActionsCardFeatureConfig,
|
||||
} from "../../card-features/types";
|
||||
import type { LovelaceCardFeatureEditor } from "../../types";
|
||||
|
||||
@customElement("hui-counter-actions-card-feature-editor")
|
||||
export class HuiCounterActionsCardFeatureEditor
|
||||
extends LitElement
|
||||
implements LovelaceCardFeatureEditor
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||
|
||||
@state() private _config?: CounterActionsCardFeatureConfig;
|
||||
|
||||
public setConfig(config: CounterActionsCardFeatureConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc) =>
|
||||
[
|
||||
{
|
||||
name: "actions",
|
||||
selector: {
|
||||
select: {
|
||||
multiple: true,
|
||||
mode: "list",
|
||||
reorder: true,
|
||||
options: COUNTER_ACTIONS.map((action) => ({
|
||||
value: action,
|
||||
label: `${localize(
|
||||
`ui.panel.lovelace.editor.features.types.counter-actions.actions.${action}`
|
||||
)}`,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const schema = this._schema(this.hass.localize);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
default:
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-counter-actions-card-feature-editor": HuiCounterActionsCardFeatureEditor;
|
||||
}
|
||||
}
|
||||
@@ -93,6 +93,7 @@ export class HuiEntityBadgeEditor
|
||||
name: "color",
|
||||
selector: {
|
||||
ui_color: {
|
||||
default_color: "state",
|
||||
include_state: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { assert, assign, object, optional, string } from "superstruct";
|
||||
import { assert, assign, boolean, object, optional, string } from "superstruct";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { MarkdownCardConfig } from "../../cards/types";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
object({
|
||||
text_only: optional(boolean()),
|
||||
title: optional(string()),
|
||||
content: string(),
|
||||
theme: optional(string()),
|
||||
})
|
||||
);
|
||||
|
||||
const SCHEMA = [
|
||||
{ name: "title", selector: { text: {} } },
|
||||
{ name: "content", required: true, selector: { template: {} } },
|
||||
{ name: "theme", selector: { theme: {} } },
|
||||
] as const;
|
||||
|
||||
@customElement("hui-markdown-card-editor")
|
||||
export class HuiMarkdownCardEditor
|
||||
extends LitElement
|
||||
@@ -38,16 +37,51 @@ export class HuiMarkdownCardEditor
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc, text_only: boolean) =>
|
||||
[
|
||||
{
|
||||
name: "style",
|
||||
required: true,
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: ["card", "text-only"].map((style) => ({
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.markdown.style_options.${style}`
|
||||
),
|
||||
value: style,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
...(!text_only
|
||||
? ([{ name: "title", selector: { text: {} } }] as const)
|
||||
: []),
|
||||
{ name: "content", required: true, selector: { template: {} } },
|
||||
] as const satisfies HaFormSchema[]
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const data = {
|
||||
...this._config,
|
||||
style: this._config.text_only ? "text-only" : "card",
|
||||
};
|
||||
|
||||
const schema = this._schema(
|
||||
this.hass.localize,
|
||||
this._config.text_only || false
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.schema=${SCHEMA}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
@@ -55,17 +89,23 @@ export class HuiMarkdownCardEditor
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
const config = { ...ev.detail.value };
|
||||
|
||||
if (config.style === "text-only") {
|
||||
config.text_only = true;
|
||||
} else {
|
||||
delete config.text_only;
|
||||
}
|
||||
delete config.style;
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "theme":
|
||||
return `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
case "style":
|
||||
case "content":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.markdown.${schema.name}`
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
optional,
|
||||
string,
|
||||
} from "superstruct";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
@@ -84,6 +85,8 @@ export class HuiStackCardEditor
|
||||
|
||||
@state() protected _guiModeAvailable? = true;
|
||||
|
||||
protected _keys = new WeakMap<LovelaceCardConfig, string>();
|
||||
|
||||
protected _schema: readonly HaFormSchema[] = SCHEMA;
|
||||
|
||||
@query("hui-card-element-editor")
|
||||
@@ -199,14 +202,16 @@ export class HuiStackCardEditor
|
||||
@click=${this._handleDeleteCard}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
|
||||
<hui-card-element-editor
|
||||
.hass=${this.hass}
|
||||
.value=${this._config.cards[selected]}
|
||||
.lovelace=${this.lovelace}
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
></hui-card-element-editor>
|
||||
${keyed(
|
||||
this._getKey(this._config.cards[selected]),
|
||||
html`<hui-card-element-editor
|
||||
.hass=${this.hass}
|
||||
.value=${this._config.cards[selected]}
|
||||
.lovelace=${this.lovelace}
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
></hui-card-element-editor>`
|
||||
)}
|
||||
`
|
||||
: html`
|
||||
<hui-card-picker
|
||||
@@ -220,6 +225,14 @@ export class HuiStackCardEditor
|
||||
`;
|
||||
}
|
||||
|
||||
private _getKey(card: LovelaceCardConfig) {
|
||||
if (!this._keys.has(card)) {
|
||||
this._keys.set(card, Math.random().toString());
|
||||
}
|
||||
|
||||
return this._keys.get(card)!;
|
||||
}
|
||||
|
||||
protected _handleSelectedCard(ev) {
|
||||
if (ev.target.id === "add-card") {
|
||||
this._selectedCard = this._config!.cards.length;
|
||||
@@ -236,7 +249,10 @@ export class HuiStackCardEditor
|
||||
return;
|
||||
}
|
||||
const cards = [...this._config.cards];
|
||||
cards[this._selectedCard] = ev.detail.config as LovelaceCardConfig;
|
||||
const key = this._getKey(cards[this._selectedCard]);
|
||||
const newCard = ev.detail.config as LovelaceCardConfig;
|
||||
cards[this._selectedCard] = newCard;
|
||||
this._keys.set(newCard, key);
|
||||
this._config = { ...this._config, cards };
|
||||
this._guiModeAvailable = ev.detail.guiModeAvailable;
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
assert,
|
||||
assign,
|
||||
boolean,
|
||||
enums,
|
||||
object,
|
||||
optional,
|
||||
string,
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
} from "superstruct";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
@@ -48,12 +50,25 @@ const cardConfigStruct = assign(
|
||||
show_entity_picture: optional(boolean()),
|
||||
vertical: optional(boolean()),
|
||||
tap_action: optional(actionConfigStruct),
|
||||
icon_tap_action: optional(actionConfigStruct),
|
||||
hold_action: optional(actionConfigStruct),
|
||||
double_tap_action: optional(actionConfigStruct),
|
||||
icon_tap_action: optional(actionConfigStruct),
|
||||
icon_hold_action: optional(actionConfigStruct),
|
||||
icon_double_tap_action: optional(actionConfigStruct),
|
||||
features: optional(array(any())),
|
||||
features_position: optional(enums(["bottom", "inline"])),
|
||||
})
|
||||
);
|
||||
|
||||
const ADVANCED_ACTIONS = [
|
||||
"hold_action",
|
||||
"icon_hold_action",
|
||||
"double_tap_action",
|
||||
"icon_double_tap_action",
|
||||
] as const;
|
||||
|
||||
type AdvancedActions = (typeof ADVANCED_ACTIONS)[number];
|
||||
|
||||
@customElement("hui-tile-card-editor")
|
||||
export class HuiTileCardEditor
|
||||
extends LitElement
|
||||
@@ -63,13 +78,46 @@ export class HuiTileCardEditor
|
||||
|
||||
@state() private _config?: TileCardConfig;
|
||||
|
||||
@state() private _displayActions?: AdvancedActions[];
|
||||
|
||||
public setConfig(config: TileCardConfig): void {
|
||||
assert(config, cardConfigStruct);
|
||||
this._config = config;
|
||||
|
||||
if (this._displayActions) return;
|
||||
this._setDisplayActions(config);
|
||||
}
|
||||
|
||||
private _setDisplayActions(config: TileCardConfig) {
|
||||
this._displayActions = ADVANCED_ACTIONS.filter(
|
||||
(action) => action in config
|
||||
);
|
||||
}
|
||||
|
||||
private _resetConfiguredActions() {
|
||||
this._displayActions = undefined;
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this._config) {
|
||||
this._setDisplayActions(this._config);
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._resetConfiguredActions();
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(entityId: string | undefined, hideState: boolean) =>
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
entityId: string | undefined,
|
||||
hideState: boolean,
|
||||
vertical: boolean,
|
||||
displayActions: AdvancedActions[] = []
|
||||
) =>
|
||||
[
|
||||
{ name: "entity", selector: { entity: {} } },
|
||||
{
|
||||
@@ -105,12 +153,6 @@ export class HuiTileCardEditor
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "vertical",
|
||||
selector: {
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hide_state",
|
||||
selector: {
|
||||
@@ -132,6 +174,43 @@ export class HuiTileCardEditor
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "content_layout",
|
||||
required: true,
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: ["horizontal", "vertical"].map((value) => ({
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.tile.content_layout_options.${value}`
|
||||
),
|
||||
value,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "features_position",
|
||||
required: true,
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: ["bottom", "inline"].map((value) => ({
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.tile.features_position_options.${value}`
|
||||
),
|
||||
value,
|
||||
disabled: vertical && value === "inline",
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -158,14 +237,14 @@ export class HuiTileCardEditor
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hold_action",
|
||||
...displayActions.map((action) => ({
|
||||
name: action,
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "none",
|
||||
default_action: "none" as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
})),
|
||||
],
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
@@ -179,9 +258,23 @@ export class HuiTileCardEditor
|
||||
const entityId = this._config!.entity;
|
||||
const stateObj = entityId ? this.hass!.states[entityId] : undefined;
|
||||
|
||||
const schema = this._schema(entityId, this._config!.hide_state ?? false);
|
||||
const schema = this._schema(
|
||||
this.hass.localize,
|
||||
entityId,
|
||||
this._config.hide_state ?? false,
|
||||
this._config.vertical ?? false,
|
||||
this._displayActions
|
||||
);
|
||||
|
||||
const data = this._config;
|
||||
const data = {
|
||||
...this._config,
|
||||
content_layout: this._config.vertical ? "vertical" : "horizontal",
|
||||
};
|
||||
|
||||
// Default features position to bottom and force it to bottom in vertical mode
|
||||
if (!data.features_position || data.vertical) {
|
||||
data.features_position = "bottom";
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
@@ -233,6 +326,12 @@ export class HuiTileCardEditor
|
||||
delete config.state_content;
|
||||
}
|
||||
|
||||
// Convert content_layout to vertical
|
||||
if (config.content_layout) {
|
||||
config.vertical = config.content_layout === "vertical";
|
||||
delete config.content_layout;
|
||||
}
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
@@ -287,12 +386,14 @@ export class HuiTileCardEditor
|
||||
switch (schema.name) {
|
||||
case "color":
|
||||
case "icon_tap_action":
|
||||
case "icon_hold_action":
|
||||
case "icon_double_tap_action":
|
||||
case "show_entity_picture":
|
||||
case "vertical":
|
||||
case "hide_state":
|
||||
case "state_content":
|
||||
case "content_layout":
|
||||
case "appearance":
|
||||
case "interactions":
|
||||
case "features_position":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.tile.${schema.name}`
|
||||
);
|
||||
@@ -328,6 +429,14 @@ export class HuiTileCardEditor
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.info {
|
||||
color: var(--secondary-text-color);
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.features-form {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { createSectionElement } from "../create-element/create-section-element";
|
||||
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||
import { replaceCard } from "../editor/config-util";
|
||||
import { performDeleteCard } from "../editor/delete-card";
|
||||
import { parseLovelaceCardPath } from "../editor/lovelace-path";
|
||||
import { generateLovelaceSectionStrategy } from "../strategies/get-strategy";
|
||||
@@ -253,11 +254,23 @@ export class HuiSection extends ReactiveElement {
|
||||
ev.stopPropagation();
|
||||
if (!this.lovelace) return;
|
||||
const { cardIndex } = parseLovelaceCardPath(ev.detail.path);
|
||||
const sectionConfig = this.config;
|
||||
if (isStrategySection(sectionConfig)) {
|
||||
return;
|
||||
}
|
||||
const cardConfig = sectionConfig.cards![cardIndex];
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: [this.viewIndex, this.index],
|
||||
cardIndex,
|
||||
saveCardConfig: async (newCardConfig) => {
|
||||
const newConfig = replaceCard(
|
||||
this.lovelace!.config,
|
||||
[this.viewIndex, this.index, cardIndex],
|
||||
newCardConfig
|
||||
);
|
||||
await this.lovelace!.saveConfig(newConfig);
|
||||
},
|
||||
sectionConfig,
|
||||
cardConfig,
|
||||
});
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-delete-card", (ev) => {
|
||||
|
||||
@@ -21,6 +21,7 @@ import { showCreateBadgeDialog } from "../editor/badge-editor/show-create-badge-
|
||||
import { showEditBadgeDialog } from "../editor/badge-editor/show-edit-badge-dialog";
|
||||
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||
import { replaceCard } from "../editor/config-util";
|
||||
import {
|
||||
type DeleteBadgeParams,
|
||||
performDeleteBadge,
|
||||
@@ -270,11 +271,22 @@ export class HUIView extends ReactiveElement {
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-edit-card", (ev) => {
|
||||
const { cardIndex } = parseLovelaceCardPath(ev.detail.path);
|
||||
const viewConfig = this.lovelace!.config.views[this.index];
|
||||
if (isStrategyView(viewConfig)) {
|
||||
return;
|
||||
}
|
||||
const cardConfig = viewConfig.cards![cardIndex];
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: [this.index],
|
||||
cardIndex,
|
||||
saveCardConfig: async (newCardConfig) => {
|
||||
const newConfig = replaceCard(
|
||||
this.lovelace!.config,
|
||||
[this.index, cardIndex],
|
||||
newCardConfig
|
||||
);
|
||||
await this.lovelace.saveConfig(newConfig);
|
||||
},
|
||||
cardConfig,
|
||||
});
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-delete-card", (ev) => {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import "@material/mwc-button";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-card";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-expansion-panel";
|
||||
import "../../layouts/hass-tabs-subpage";
|
||||
import { profileSections } from "./ha-panel-profile";
|
||||
import { isExternal } from "../../data/external";
|
||||
@@ -27,6 +28,9 @@ import "./ha-pick-time-zone-row";
|
||||
import "./ha-push-notifications-row";
|
||||
import "./ha-set-suspend-row";
|
||||
import "./ha-set-vibrate-row";
|
||||
import { storage } from "../../common/decorators/storage";
|
||||
import type { HaSwitch } from "../../components/ha-switch";
|
||||
import { fetchSidebarPreferences } from "../../data/sidebar";
|
||||
|
||||
@customElement("ha-profile-section-general")
|
||||
class HaProfileSectionGeneral extends LitElement {
|
||||
@@ -38,6 +42,20 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@storage({
|
||||
key: "sidebarPanelOrder",
|
||||
state: true,
|
||||
subscribe: true,
|
||||
})
|
||||
private _devicePanelOrder?: string[];
|
||||
|
||||
@storage({
|
||||
key: "sidebarHiddenPanels",
|
||||
state: true,
|
||||
subscribe: true,
|
||||
})
|
||||
private _deviceHiddenPanels?: string[];
|
||||
|
||||
private _unsubCoreData?: UnsubscribeFunc;
|
||||
|
||||
private _getCoreData() {
|
||||
@@ -71,6 +89,9 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const deviceSidebarSettingsEnabled =
|
||||
!!this._devicePanelOrder || !!this._deviceHiddenPanels;
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
main-page
|
||||
@@ -91,9 +112,9 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
: ""}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button class="warning" @click=${this._handleLogOut}>
|
||||
<ha-button class="warning" @click=${this._handleLogOut}>
|
||||
${this.hass.localize("ui.panel.profile.logout")}
|
||||
</mwc-button>
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card
|
||||
@@ -128,6 +149,29 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-first-weekday-row>
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.customize_sidebar.header"
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
slot="description"
|
||||
class=${deviceSidebarSettingsEnabled ? "device-info" : ""}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.profile.customize_sidebar.${!deviceSidebarSettingsEnabled ? "description" : "overwritten_by_device"}`
|
||||
)}
|
||||
</span>
|
||||
<ha-button
|
||||
.disabled=${deviceSidebarSettingsEnabled}
|
||||
@click=${this._customizeSidebar}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.customize_sidebar.button"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-settings-row>
|
||||
${this.hass.user!.is_admin
|
||||
? html`
|
||||
<ha-advanced-mode-row
|
||||
@@ -159,20 +203,48 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.customize_sidebar.header"
|
||||
"ui.panel.profile.customize_sidebar.device_specific_header"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.customize_sidebar.description"
|
||||
"ui.panel.profile.customize_sidebar.device_description"
|
||||
)}
|
||||
</span>
|
||||
<mwc-button @click=${this._customizeSidebar}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.customize_sidebar.button"
|
||||
)}
|
||||
</mwc-button>
|
||||
<ha-switch
|
||||
.checked=${deviceSidebarSettingsEnabled}
|
||||
@change=${this._toggleDeviceSidebarPreferences}
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
${deviceSidebarSettingsEnabled
|
||||
? html`
|
||||
<ha-expansion-panel
|
||||
outlined
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.profile.customize_sidebar.device_specific_header"
|
||||
)}
|
||||
expanded
|
||||
>
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.customize_sidebar.header"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.customize_sidebar.description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button @click=${this._customizeSidebar}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.customize_sidebar.button"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-settings-row>
|
||||
</ha-expansion-panel>
|
||||
`
|
||||
: nothing}
|
||||
${this.hass.dockedSidebar !== "auto" || !this.narrow
|
||||
? html`
|
||||
<ha-force-narrow-row
|
||||
@@ -215,6 +287,38 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
fireEvent(this, "hass-edit-sidebar", { editMode: true });
|
||||
}
|
||||
|
||||
private async _toggleDeviceSidebarPreferences(ev: Event) {
|
||||
const switchElement = ev.target as HaSwitch;
|
||||
const enabled = switchElement.checked;
|
||||
|
||||
if (!enabled) {
|
||||
if (this._devicePanelOrder || this._deviceHiddenPanels) {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.profile.customize_sidebar.delete_device_preferences_header"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.profile.customize_sidebar.delete_device_preferences_description"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (confirm) {
|
||||
this._devicePanelOrder = undefined;
|
||||
this._deviceHiddenPanels = undefined;
|
||||
} else {
|
||||
// revert switch
|
||||
switchElement.click();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const sidebarPreferences = await fetchSidebarPreferences(this.hass);
|
||||
this._devicePanelOrder = sidebarPreferences?.panelOrder ?? [];
|
||||
this._deviceHiddenPanels = sidebarPreferences?.hiddenPanels ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
private _handleLogOut() {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize("ui.panel.profile.logout_title"),
|
||||
@@ -251,6 +355,14 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
text-align: center;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-expansion-panel {
|
||||
margin: 0 8px 8px;
|
||||
}
|
||||
|
||||
.device-info {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -47,7 +47,12 @@ export class HaStateControlAlarmControlPanelModes extends LitElement {
|
||||
}
|
||||
|
||||
private async _setMode(mode: AlarmMode) {
|
||||
setProtectedAlarmControlPanelMode(this, this.hass!, this.stateObj!, mode);
|
||||
await setProtectedAlarmControlPanelMode(
|
||||
this,
|
||||
this.hass!,
|
||||
this.stateObj!,
|
||||
mode
|
||||
);
|
||||
}
|
||||
|
||||
private async _valueChanged(ev: CustomEvent) {
|
||||
|
||||
@@ -286,7 +286,10 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
clearInterval(this.__backendPingInterval);
|
||||
this.__backendPingInterval = setInterval(() => {
|
||||
if (this.hass?.connected) {
|
||||
promiseTimeout(5000, this.hass?.connection.ping()).catch(() => {
|
||||
// If the backend is busy, or the connection is latent,
|
||||
// it can take more than 10 seconds for the ping to return.
|
||||
// We give it a 15 second timeout to be safe.
|
||||
promiseTimeout(15000, this.hass?.connection.ping()).catch(() => {
|
||||
if (!this.hass?.connected) {
|
||||
return;
|
||||
}
|
||||
@@ -296,7 +299,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
this.hass?.connection.reconnect(true);
|
||||
});
|
||||
}
|
||||
}, 10000);
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
protected hassReconnected() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user