mirror of
https://github.com/home-assistant/frontend.git
synced 2026-05-24 18:17:10 +00:00
Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bf5833262 | |||
| 8c9da19935 | |||
| 4b1eeb0eb1 | |||
| f7ffdabe5d | |||
| 678ee7e82a | |||
| ee72f4818d | |||
| ccbd9c1f24 | |||
| 84135a9424 | |||
| 4ebc334298 | |||
| 3c4c3e39e5 | |||
| 1267003b42 | |||
| 733359c869 | |||
| 50b6e07ae5 | |||
| 1b62a7cff8 | |||
| c293cf56f6 | |||
| 2ec6f3615d | |||
| d3b92059e5 | |||
| 72e69f6291 | |||
| 3bff97595f | |||
| 19b1d03cd1 | |||
| f04557688c | |||
| d6953ea1bc | |||
| 4501db18c7 | |||
| c68bcc5b32 | |||
| 3753c7d313 | |||
| 4a2ef18375 | |||
| 051da41eec | |||
| 9b1a679f21 | |||
| 905a0f957c | |||
| d9a687b79c | |||
| 3b56497134 | |||
| ed0ec871ce | |||
| 29f5362182 | |||
| 5096bab26c | |||
| ce2892cab9 | |||
| 57748c15de | |||
| ecb6a33c86 | |||
| d34921ff6d | |||
| e6c9e81082 | |||
| aa13c6fa53 | |||
| d47738aa24 | |||
| d473ee1084 | |||
| 0372ed932f | |||
| 2baa044db5 | |||
| 3d04046bcc | |||
| 8e860cb17d | |||
| c41d7ff923 | |||
| c22fc1021a | |||
| 6344233934 | |||
| 23441d593b | |||
| 8393ed5fd4 | |||
| 09afe9bb51 | |||
| fb8d6062c5 | |||
| f93ae58b83 | |||
| 7626b26b2d | |||
| a1bf30e501 | |||
| 37a45d1729 | |||
| 6962a915a3 | |||
| de3e2bcafa | |||
| e732280b70 | |||
| 4fb3453f73 | |||
| 0f9cb9c13e | |||
| 7aa235c6af | |||
| e8ddae8189 |
@@ -36,14 +36,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
uses: github/codeql-action/autobuild@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -57,4 +57,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
|
||||
@@ -31,4 +31,7 @@ module.exports = {
|
||||
isDevContainer() {
|
||||
return isTrue(process.env.DEV_CONTAINER);
|
||||
},
|
||||
jsMinifier() {
|
||||
return (process.env.JS_MINIFIER || "swc").toLowerCase();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -80,7 +80,13 @@ const doneHandler = (done) => (err, stats) => {
|
||||
console.log(stats.toString("minimal"));
|
||||
}
|
||||
|
||||
log(`Build done @ ${new Date().toLocaleTimeString()}`);
|
||||
const durationMs =
|
||||
stats?.startTime && stats?.endTime ? stats.endTime - stats.startTime : 0;
|
||||
const durationLabel = durationMs
|
||||
? ` (${(durationMs / 1000).toFixed(1)}s, minifier: ${env.jsMinifier()})`
|
||||
: ` (minifier: ${env.jsMinifier()})`;
|
||||
|
||||
log(`Build done @ ${new Date().toLocaleTimeString()}${durationLabel}`);
|
||||
|
||||
if (done) {
|
||||
done();
|
||||
|
||||
@@ -13,6 +13,7 @@ const { WebpackManifestPlugin } = require("rspack-manifest-plugin");
|
||||
const log = require("fancy-log");
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const WebpackBar = require("webpackbar/rspack");
|
||||
const env = require("./env.cjs");
|
||||
const paths = require("./paths.cjs");
|
||||
const bundle = require("./bundle.cjs");
|
||||
|
||||
@@ -100,11 +101,20 @@ const createRspackConfig = ({
|
||||
},
|
||||
optimization: {
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
parallel: true,
|
||||
extractComments: true,
|
||||
terserOptions: bundle.terserOptions({ latestBuild, isTestBuild }),
|
||||
}),
|
||||
env.jsMinifier() === "terser"
|
||||
? new TerserPlugin({
|
||||
parallel: true,
|
||||
extractComments: true,
|
||||
terserOptions: bundle.terserOptions({ latestBuild, isTestBuild }),
|
||||
})
|
||||
: new rspack.SwcJsMinimizerRspackPlugin({
|
||||
extractComments: true,
|
||||
minimizerOptions: {
|
||||
ecma: latestBuild ? 2015 : 5,
|
||||
module: latestBuild,
|
||||
format: { comments: false },
|
||||
},
|
||||
}),
|
||||
],
|
||||
moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
|
||||
chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
|
||||
|
||||
@@ -18,7 +18,7 @@ The Home Assistant interface is based on Material Design. It's a design system c
|
||||
|
||||
We want to make it as easy for designers to contribute as it is for developers. There’s a lot a designer can contribute to:
|
||||
|
||||
- Meet us at <a href="https://www.home-assistant.io/join-chat" rel="noopener noreferrer" target="_blank">devs_ux Discord</a>. Feel free to share your designs, user test or strategic ideas.
|
||||
- Meet us at <a href="https://www.home-assistant.io/join-chat-design" rel="noopener noreferrer" target="_blank">Discord #designers channel</a>. If you can't see the channel, make sure you set the correct role in Channels & Roles.
|
||||
- Start designing with our <a href="https://www.figma.com/community/file/967153512097289521/Home-Assistant-DesignKit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
|
||||
- Find the latest UX <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
|
||||
|
||||
|
||||
+7
-7
@@ -29,7 +29,7 @@
|
||||
"@babel/runtime": "7.28.6",
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@codemirror/autocomplete": "6.20.0",
|
||||
"@codemirror/commands": "6.10.1",
|
||||
"@codemirror/commands": "6.10.2",
|
||||
"@codemirror/language": "6.12.1",
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.6.0",
|
||||
@@ -52,7 +52,7 @@
|
||||
"@fullcalendar/list": "6.1.20",
|
||||
"@fullcalendar/luxon3": "6.1.20",
|
||||
"@fullcalendar/timegrid": "6.1.20",
|
||||
"@home-assistant/webawesome": "3.0.0-ha.2",
|
||||
"@home-assistant/webawesome": "3.2.1-ha.0",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lit-labs/motion": "1.1.0",
|
||||
"@lit-labs/observers": "2.1.0",
|
||||
@@ -132,7 +132,7 @@
|
||||
"stacktrace-js": "2.0.2",
|
||||
"superstruct": "2.0.2",
|
||||
"tinykeys": "3.0.0",
|
||||
"ua-parser-js": "2.0.8",
|
||||
"ua-parser-js": "2.0.9",
|
||||
"vue": "2.7.16",
|
||||
"vue2-daterange-picker": "0.6.8",
|
||||
"weekstart": "2.0.0",
|
||||
@@ -154,8 +154,8 @@
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.0.3",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.1",
|
||||
"@rspack/core": "1.7.4",
|
||||
"@rsdoctor/rspack-plugin": "1.5.2",
|
||||
"@rspack/core": "1.7.5",
|
||||
"@rspack/dev-server": "1.2.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.25",
|
||||
@@ -191,7 +191,7 @@
|
||||
"eslint-plugin-wc": "3.0.2",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.3",
|
||||
"glob": "13.0.0",
|
||||
"glob": "13.0.1",
|
||||
"gulp": "5.0.1",
|
||||
"gulp-brotli": "3.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
@@ -235,6 +235,6 @@
|
||||
},
|
||||
"packageManager": "yarn@4.12.0",
|
||||
"volta": {
|
||||
"node": "24.13.0"
|
||||
"node": "24.13.1"
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
OUT_ROOT="$ROOT_DIR/hass_frontend"
|
||||
OUT_LATEST="$OUT_ROOT/frontend_latest"
|
||||
OUT_ES5="$OUT_ROOT/frontend_es5"
|
||||
|
||||
bytes_dir() {
|
||||
if [ -d "$1" ]; then
|
||||
du -sb "$1" | cut -f1
|
||||
else
|
||||
echo 0
|
||||
fi
|
||||
}
|
||||
|
||||
run_build() {
|
||||
minifier="$1"
|
||||
printf "\n==> Building with %s\n" "$minifier"
|
||||
start_time=$(date +%s)
|
||||
JS_MINIFIER="$minifier" "$ROOT_DIR/script/build_frontend"
|
||||
end_time=$(date +%s)
|
||||
duration=$((end_time - start_time))
|
||||
|
||||
latest_size=$(bytes_dir "$OUT_LATEST")
|
||||
es5_size=$(bytes_dir "$OUT_ES5")
|
||||
total_size=$(bytes_dir "$OUT_ROOT")
|
||||
|
||||
printf "%s|%s|%s|%s\n" "$minifier" "$duration" "$latest_size" "$es5_size" >> "$ROOT_DIR/temp/minifier_benchmark.tsv"
|
||||
printf " duration: %ss\n" "$duration"
|
||||
printf " frontend_latest: %s bytes\n" "$latest_size"
|
||||
printf " frontend_es5: %s bytes\n" "$es5_size"
|
||||
printf " hass_frontend: %s bytes\n" "$total_size"
|
||||
}
|
||||
|
||||
mkdir -p "$ROOT_DIR/temp"
|
||||
rm -f "$ROOT_DIR/temp/minifier_benchmark.tsv"
|
||||
|
||||
run_build swc
|
||||
run_build terser
|
||||
|
||||
printf "\n==> Summary (minifier | seconds | latest bytes | es5 bytes)\n"
|
||||
cat "$ROOT_DIR/temp/minifier_benchmark.tsv"
|
||||
@@ -116,3 +116,6 @@ export const UNIT_F = "°F";
|
||||
|
||||
/** Entity ID of the default view. */
|
||||
export const DEFAULT_VIEW_ENTITY_ID = "group.default_view";
|
||||
|
||||
/** String to visually separate labels on UI */
|
||||
export const STRINGS_SEPARATOR_DOT = " · ";
|
||||
|
||||
@@ -3,13 +3,14 @@ import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import { TimeZone } from "../../data/translation";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, ValuePart } from "../../types";
|
||||
import { formatDate } from "../datetime/format_date";
|
||||
import { formatDateTime } from "../datetime/format_date_time";
|
||||
import { DURATION_UNITS, formatDuration } from "../datetime/format_duration";
|
||||
import { formatTime } from "../datetime/format_time";
|
||||
import {
|
||||
formatNumber,
|
||||
formatNumberToParts,
|
||||
getNumberFormatOptions,
|
||||
isNumericFromAttributes,
|
||||
} from "../number/format_number";
|
||||
@@ -51,8 +52,36 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
attributes: any,
|
||||
state: string
|
||||
): string => {
|
||||
const parts = computeStateToPartsFromEntityAttributes(
|
||||
localize,
|
||||
locale,
|
||||
sensorNumericDeviceClasses,
|
||||
config,
|
||||
entity,
|
||||
entityId,
|
||||
attributes,
|
||||
state
|
||||
);
|
||||
return parts.map((part) => part.value).join("");
|
||||
};
|
||||
|
||||
const computeStateToPartsFromEntityAttributes = (
|
||||
localize: LocalizeFunc,
|
||||
locale: FrontendLocaleData,
|
||||
sensorNumericDeviceClasses: string[],
|
||||
config: HassConfig,
|
||||
entity: EntityRegistryDisplayEntry | undefined,
|
||||
entityId: string,
|
||||
attributes: any,
|
||||
state: string
|
||||
): ValuePart[] => {
|
||||
if (state === UNKNOWN || state === UNAVAILABLE) {
|
||||
return localize(`state.default.${state}`);
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value: localize(`state.default.${state}`),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const domain = computeDomain(entityId);
|
||||
@@ -73,19 +102,27 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
DURATION_UNITS.includes(attributes.unit_of_measurement)
|
||||
) {
|
||||
try {
|
||||
return formatDuration(
|
||||
locale,
|
||||
state,
|
||||
attributes.unit_of_measurement,
|
||||
entity?.display_precision
|
||||
);
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value: formatDuration(
|
||||
locale,
|
||||
state,
|
||||
attributes.unit_of_measurement,
|
||||
entity?.display_precision
|
||||
),
|
||||
},
|
||||
];
|
||||
} catch (_err) {
|
||||
// fallback to default
|
||||
}
|
||||
}
|
||||
|
||||
// state is monetary
|
||||
if (attributes.device_class === "monetary") {
|
||||
let parts: Record<string, string>[] = [];
|
||||
try {
|
||||
return formatNumber(state, locale, {
|
||||
parts = formatNumberToParts(state, locale, {
|
||||
style: "currency",
|
||||
currency: attributes.unit_of_measurement,
|
||||
minimumFractionDigits: 2,
|
||||
@@ -98,8 +135,34 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
} catch (_err) {
|
||||
// fallback to default
|
||||
}
|
||||
|
||||
const TYPE_MAP: Record<string, ValuePart["type"]> = {
|
||||
integer: "value",
|
||||
group: "value",
|
||||
decimal: "value",
|
||||
fraction: "value",
|
||||
literal: "literal",
|
||||
currency: "unit",
|
||||
};
|
||||
|
||||
const valueParts: ValuePart[] = [];
|
||||
|
||||
for (const part of parts) {
|
||||
const type = TYPE_MAP[part.type];
|
||||
if (!type) continue;
|
||||
const last = valueParts[valueParts.length - 1];
|
||||
// Merge consecutive numeric parts (e.g. "1" + "," + "234" + "." + "56" → "1,234.56")
|
||||
if (type === "value" && last?.type === "value") {
|
||||
last.value += part.value;
|
||||
} else {
|
||||
valueParts.push({ type, value: part.value });
|
||||
}
|
||||
}
|
||||
|
||||
return valueParts;
|
||||
}
|
||||
|
||||
// default processing of numeric values
|
||||
const value = formatNumber(
|
||||
state,
|
||||
locale,
|
||||
@@ -114,10 +177,14 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
attributes.unit_of_measurement;
|
||||
|
||||
if (unit) {
|
||||
return `${value}${blankBeforeUnit(unit, locale)}${unit}`;
|
||||
return [
|
||||
{ type: "value", value: value },
|
||||
{ type: "literal", value: blankBeforeUnit(unit, locale) },
|
||||
{ type: "unit", value: unit },
|
||||
];
|
||||
}
|
||||
|
||||
return value;
|
||||
return [{ type: "value", value: value }];
|
||||
}
|
||||
|
||||
if (["date", "input_datetime", "time"].includes(domain)) {
|
||||
@@ -129,36 +196,51 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
const components = state.split(" ");
|
||||
if (components.length === 2) {
|
||||
// Date and time.
|
||||
return formatDateTime(
|
||||
new Date(components.join("T")),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
);
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value: formatDateTime(
|
||||
new Date(components.join("T")),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (components.length === 1) {
|
||||
if (state.includes("-")) {
|
||||
// Date only.
|
||||
return formatDate(
|
||||
new Date(`${state}T00:00`),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
);
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value: formatDate(
|
||||
new Date(`${state}T00:00`),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (state.includes(":")) {
|
||||
// Time only.
|
||||
const now = new Date();
|
||||
return formatTime(
|
||||
new Date(`${now.toISOString().split("T")[0]}T${state}`),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
);
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value: formatTime(
|
||||
new Date(`${now.toISOString().split("T")[0]}T${state}`),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
return state;
|
||||
return [{ type: "value", value: state }];
|
||||
} catch (_e) {
|
||||
// Formatting methods may throw error if date parsing doesn't go well,
|
||||
// just return the state string in that case.
|
||||
return state;
|
||||
return [{ type: "value", value: state }];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,25 +264,58 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
(domain === "sensor" && attributes.device_class === "timestamp")
|
||||
) {
|
||||
try {
|
||||
return formatDateTime(new Date(state), locale, config);
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value: formatDateTime(new Date(state), locale, config),
|
||||
},
|
||||
];
|
||||
} catch (_err) {
|
||||
return state;
|
||||
return [{ type: "value", value: state }];
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
(entity?.translation_key &&
|
||||
localize(
|
||||
`component.${entity.platform}.entity.${domain}.${entity.translation_key}.state.${state}`
|
||||
)) ||
|
||||
// Return device class translation
|
||||
(attributes.device_class &&
|
||||
localize(
|
||||
`component.${domain}.entity_component.${attributes.device_class}.state.${state}`
|
||||
)) ||
|
||||
// Return default translation
|
||||
localize(`component.${domain}.entity_component._.state.${state}`) ||
|
||||
// We don't know! Return the raw state.
|
||||
state
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value:
|
||||
(entity?.translation_key &&
|
||||
localize(
|
||||
`component.${entity.platform}.entity.${domain}.${entity.translation_key}.state.${state}`
|
||||
)) ||
|
||||
// Return device class translation
|
||||
(attributes.device_class &&
|
||||
localize(
|
||||
`component.${domain}.entity_component.${attributes.device_class}.state.${state}`
|
||||
)) ||
|
||||
// Return default translation
|
||||
localize(`component.${domain}.entity_component._.state.${state}`) ||
|
||||
// We don't know! Return the raw state.
|
||||
state,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const computeStateToParts = (
|
||||
localize: LocalizeFunc,
|
||||
stateObj: HassEntity,
|
||||
locale: FrontendLocaleData,
|
||||
sensorNumericDeviceClasses: string[],
|
||||
config: HassConfig,
|
||||
entities: HomeAssistant["entities"],
|
||||
state?: string
|
||||
): ValuePart[] => {
|
||||
const entity = entities?.[stateObj.entity_id] as
|
||||
| EntityRegistryDisplayEntry
|
||||
| undefined;
|
||||
return computeStateToPartsFromEntityAttributes(
|
||||
localize,
|
||||
locale,
|
||||
sensorNumericDeviceClasses,
|
||||
config,
|
||||
entity,
|
||||
stateObj.entity_id,
|
||||
stateObj.attributes,
|
||||
state !== undefined ? state : stateObj.state
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@ import type {
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import { NumberFormat } from "../../data/translation";
|
||||
import { round } from "./round";
|
||||
|
||||
/**
|
||||
* Returns true if the entity is considered numeric based on the attributes it has
|
||||
@@ -52,7 +51,22 @@ export const formatNumber = (
|
||||
num: string | number,
|
||||
localeOptions?: FrontendLocaleData,
|
||||
options?: Intl.NumberFormatOptions
|
||||
): string => {
|
||||
): string =>
|
||||
formatNumberToParts(num, localeOptions, options)
|
||||
.map((part) => part.value)
|
||||
.join("");
|
||||
|
||||
/**
|
||||
* Returns an array of objects containing the formatted number in parts
|
||||
* Similar to Intl.NumberFormat.prototype.formatToParts()
|
||||
*
|
||||
* Input params - same as for formatNumber()
|
||||
*/
|
||||
export const formatNumberToParts = (
|
||||
num: string | number,
|
||||
localeOptions?: FrontendLocaleData,
|
||||
options?: Intl.NumberFormatOptions
|
||||
): any[] => {
|
||||
const locale = localeOptions
|
||||
? numberFormatToLocale(localeOptions)
|
||||
: undefined;
|
||||
@@ -71,7 +85,7 @@ export const formatNumber = (
|
||||
return new Intl.NumberFormat(
|
||||
locale,
|
||||
getDefaultFormatOptions(num, options)
|
||||
).format(Number(num));
|
||||
).formatToParts(Number(num));
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -86,15 +100,10 @@ export const formatNumber = (
|
||||
...options,
|
||||
useGrouping: false,
|
||||
})
|
||||
).format(Number(num));
|
||||
).formatToParts(Number(num));
|
||||
}
|
||||
|
||||
if (typeof num === "string") {
|
||||
return num;
|
||||
}
|
||||
return `${round(num, options?.maximumFractionDigits).toString()}${
|
||||
options?.style === "currency" ? ` ${options.currency}` : ""
|
||||
}`;
|
||||
return [{ type: "literal", value: num }];
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
const SI_PREFIX_MULTIPLIERS: Record<string, number> = {
|
||||
T: 1e12,
|
||||
G: 1e9,
|
||||
M: 1e6,
|
||||
k: 1e3,
|
||||
m: 1e-3,
|
||||
"\u00B5": 1e-6, // µ (micro sign)
|
||||
"\u03BC": 1e-6, // μ (greek small letter mu)
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalize a numeric value by detecting SI unit prefixes (T, G, M, k, m, µ).
|
||||
* Only applies when the unit is longer than 1 character and starts with a
|
||||
* recognized prefix, avoiding false positives on standalone units like "m" (meters).
|
||||
*/
|
||||
export const normalizeValueBySIPrefix = (
|
||||
value: number,
|
||||
unit: string | undefined
|
||||
): number => {
|
||||
if (!unit || unit.length <= 1) {
|
||||
return value;
|
||||
}
|
||||
const prefix = unit[0];
|
||||
if (prefix in SI_PREFIX_MULTIPLIERS) {
|
||||
return value * SI_PREFIX_MULTIPLIERS[prefix];
|
||||
}
|
||||
return value;
|
||||
};
|
||||
@@ -12,6 +12,10 @@ export type FormatEntityStateFunc = (
|
||||
stateObj: HassEntity,
|
||||
state?: string
|
||||
) => string;
|
||||
export type FormatEntityStateToPartsFunc = (
|
||||
stateObj: HassEntity,
|
||||
state?: string
|
||||
) => ValuePart[];
|
||||
export type FormatEntityAttributeValueFunc = (
|
||||
stateObj: HassEntity,
|
||||
attribute: string,
|
||||
@@ -46,12 +50,13 @@ export const computeFormatFunctions = async (
|
||||
sensorNumericDeviceClasses: string[]
|
||||
): Promise<{
|
||||
formatEntityState: FormatEntityStateFunc;
|
||||
formatEntityStateToParts: FormatEntityStateToPartsFunc;
|
||||
formatEntityAttributeValue: FormatEntityAttributeValueFunc;
|
||||
formatEntityAttributeValueToParts: FormatEntityAttributeValueToPartsFunc;
|
||||
formatEntityAttributeName: FormatEntityAttributeNameFunc;
|
||||
formatEntityName: FormatEntityNameFunc;
|
||||
}> => {
|
||||
const { computeStateDisplay } =
|
||||
const { computeStateDisplay, computeStateToParts } =
|
||||
await import("../entity/compute_state_display");
|
||||
const {
|
||||
computeAttributeValueDisplay,
|
||||
@@ -70,6 +75,16 @@ export const computeFormatFunctions = async (
|
||||
entities,
|
||||
state
|
||||
),
|
||||
formatEntityStateToParts: (stateObj, state) =>
|
||||
computeStateToParts(
|
||||
localize,
|
||||
stateObj,
|
||||
locale,
|
||||
sensorNumericDeviceClasses,
|
||||
config,
|
||||
entities,
|
||||
state
|
||||
),
|
||||
formatEntityAttributeValue: (stateObj, attribute, value) =>
|
||||
computeAttributeValueDisplay(
|
||||
localize,
|
||||
|
||||
@@ -572,6 +572,7 @@ export class StatisticsChart extends LitElement {
|
||||
let firstSum: number | null | undefined = null;
|
||||
stats.forEach((stat) => {
|
||||
const startDate = new Date(stat.start);
|
||||
const endDate = new Date(stat.end);
|
||||
if (prevDate === startDate) {
|
||||
return;
|
||||
}
|
||||
@@ -601,10 +602,25 @@ export class StatisticsChart extends LitElement {
|
||||
dataValues.push(val);
|
||||
});
|
||||
if (!this._hiddenStats.has(statistic_id)) {
|
||||
pushData(startDate, new Date(stat.end), dataValues);
|
||||
pushData(
|
||||
startDate,
|
||||
endDate.getTime() < endTime.getTime() ? endDate : endTime,
|
||||
dataValues
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Close out the last stat segment at prevEndTime
|
||||
const lastEndTime = prevEndTime;
|
||||
const lastValues = prevValues;
|
||||
if (lastEndTime && lastValues) {
|
||||
statDataSets.forEach((d, i) => {
|
||||
d.data!.push(
|
||||
this._transformDataValue([lastEndTime, ...lastValues[i]!])
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Append current state if viewing recent data
|
||||
const now = new Date();
|
||||
// allow 10m of leeway for "now", because stats are 5 minute aggregated
|
||||
@@ -619,16 +635,6 @@ export class StatisticsChart extends LitElement {
|
||||
isFinite(currentValue) &&
|
||||
!this._hiddenStats.has(statistic_id)
|
||||
) {
|
||||
// First, close out the last stat segment at prevEndTime
|
||||
const lastEndTime = prevEndTime;
|
||||
const lastValues = prevValues;
|
||||
if (lastEndTime && lastValues) {
|
||||
statDataSets.forEach((d, i) => {
|
||||
d.data!.push(
|
||||
this._transformDataValue([lastEndTime, ...lastValues[i]!])
|
||||
);
|
||||
});
|
||||
}
|
||||
// Then push the current state at now
|
||||
statTypes.forEach((type, i) => {
|
||||
const val: (number | null)[] = [];
|
||||
|
||||
@@ -20,6 +20,7 @@ import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { groupBy } from "../../common/util/group-by";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
import { STRINGS_SEPARATOR_DOT } from "../../common/const";
|
||||
import { haStyleScrollbar } from "../../resources/styles";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -636,7 +637,7 @@ export class HaDataTable extends LitElement {
|
||||
.map(
|
||||
([key2, column2], i) =>
|
||||
html`${i !== 0
|
||||
? " · "
|
||||
? STRINGS_SEPARATOR_DOT
|
||||
: nothing}${column2.template
|
||||
? column2.template(row)
|
||||
: row[key2]}`
|
||||
@@ -1192,6 +1193,7 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
.mdc-data-table__cell--numeric {
|
||||
text-align: var(--float-end);
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell--icon {
|
||||
|
||||
@@ -9,16 +9,7 @@ import secondsToDuration from "../../common/datetime/seconds_to_duration";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
|
||||
import {
|
||||
formatNumber,
|
||||
getNumberFormatOptions,
|
||||
isNumericState,
|
||||
} from "../../common/number/format_number";
|
||||
import {
|
||||
isUnavailableState,
|
||||
UNAVAILABLE,
|
||||
UNKNOWN,
|
||||
} from "../../data/entity/entity";
|
||||
import { isUnavailableState, UNAVAILABLE } from "../../data/entity/entity";
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import { timerTimeRemaining } from "../../data/timer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -180,16 +171,11 @@ export class HaStateLabelBadge extends LitElement {
|
||||
}
|
||||
// eslint-disable-next-line: disable=no-fallthrough
|
||||
default:
|
||||
return entityState.state === UNKNOWN ||
|
||||
entityState.state === UNAVAILABLE
|
||||
return isUnavailableState(entityState.state)
|
||||
? "—"
|
||||
: isNumericState(entityState)
|
||||
? formatNumber(
|
||||
entityState.state,
|
||||
this.hass!.locale,
|
||||
getNumberFormatOptions(entityState, entry)
|
||||
)
|
||||
: this.hass!.formatEntityState(entityState);
|
||||
: this.hass!.formatEntityStateToParts(entityState).find(
|
||||
(part) => part.type === "value"
|
||||
)?.value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,7 +224,11 @@ export class HaStateLabelBadge extends LitElement {
|
||||
if (domain === "timer") {
|
||||
return secondsToDuration(_timerTimeRemaining);
|
||||
}
|
||||
return entityState.attributes.unit_of_measurement || null;
|
||||
return (
|
||||
this.hass!.formatEntityStateToParts(entityState).find(
|
||||
(part) => part.type === "unit"
|
||||
)?.value || null
|
||||
);
|
||||
}
|
||||
|
||||
private _clearInterval() {
|
||||
|
||||
@@ -163,7 +163,7 @@ export class HaAreaPicker extends LitElement {
|
||||
{
|
||||
id: ADD_NEW_ID + searchString,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.area-picker.add_new_sugestion",
|
||||
"ui.components.area-picker.add_new_suggestion",
|
||||
{
|
||||
name: searchString,
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import memoizeOne from "memoize-one";
|
||||
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeKeys } from "../common/translations/localize";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import "./ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
import type { PickerValueRenderer } from "./ha-picker-field";
|
||||
@@ -224,7 +224,7 @@ export class HaColorPicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent<{ value?: string }>) {
|
||||
private _valueChanged(ev: ValueChangedEvent<string | undefined>) {
|
||||
ev.stopPropagation();
|
||||
const selected = ev.detail.value;
|
||||
const normalized =
|
||||
|
||||
@@ -89,7 +89,7 @@ export class HaControlSelectMenu extends LitElement {
|
||||
private _renderOption = (option: SelectOption) =>
|
||||
html`<ha-dropdown-item
|
||||
.value=${option.value}
|
||||
class=${this.value === option.value ? "selected" : ""}
|
||||
.selected=${this.value === option.value}
|
||||
>${option.iconPath
|
||||
? html`<ha-svg-icon slot="icon" .path=${option.iconPath}></ha-svg-icon>`
|
||||
: option.icon
|
||||
@@ -263,15 +263,6 @@ export class HaControlSelectMenu extends LitElement {
|
||||
cursor: not-allowed;
|
||||
color: var(--disabled-color);
|
||||
}
|
||||
ha-dropdown-item.selected {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--primary-color);
|
||||
background-color: var(--ha-color-fill-primary-quiet-resting);
|
||||
--icon-primary-color: var(--primary-color);
|
||||
}
|
||||
ha-dropdown-item.selected:hover {
|
||||
background-color: var(--ha-color-fill-primary-quiet-hover);
|
||||
}
|
||||
|
||||
ha-dropdown::part(menu) {
|
||||
min-width: var(--control-select-menu-width);
|
||||
|
||||
@@ -7,8 +7,9 @@ import { nextRender } from "../common/util/render-status";
|
||||
import { haStyleDialog } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { DatePickerDialogParams } from "./ha-date-input";
|
||||
import "./ha-dialog";
|
||||
import "./ha-button";
|
||||
import "./ha-dialog-footer";
|
||||
import "./ha-wa-dialog";
|
||||
|
||||
@customElement("ha-dialog-date-picker")
|
||||
export class HaDialogDatePicker extends LitElement {
|
||||
@@ -22,6 +23,8 @@ export class HaDialogDatePicker extends LitElement {
|
||||
|
||||
@state() private _params?: DatePickerDialogParams;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _value?: string;
|
||||
|
||||
public async showDialog(params: DatePickerDialogParams): Promise<void> {
|
||||
@@ -30,9 +33,14 @@ export class HaDialogDatePicker extends LitElement {
|
||||
await nextRender();
|
||||
this._params = params;
|
||||
this._value = params.value;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
@@ -41,7 +49,13 @@ export class HaDialogDatePicker extends LitElement {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
return html`<ha-dialog open @closed=${this.closeDialog}>
|
||||
return html`<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
width="small"
|
||||
without-header
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<app-datepicker
|
||||
.value=${this._value}
|
||||
.min=${this._params.min}
|
||||
@@ -50,35 +64,40 @@ export class HaDialogDatePicker extends LitElement {
|
||||
@datepicker-value-updated=${this._valueChanged}
|
||||
.firstDayOfWeek=${this._params.firstWeekday}
|
||||
></app-datepicker>
|
||||
${this._params.canClear
|
||||
? html`<ha-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._clear}
|
||||
variant="danger"
|
||||
appearance="plain"
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.date-picker.clear")}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this._setToday}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.date-picker.today")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="primaryAction"
|
||||
dialogaction="cancel"
|
||||
class="cancel-btn"
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._setValue}>
|
||||
${this.hass.localize("ui.common.ok")}
|
||||
</ha-button>
|
||||
</ha-dialog>`;
|
||||
|
||||
<div class="bottom-actions">
|
||||
${this._params.canClear
|
||||
? html`<ha-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._clear}
|
||||
variant="danger"
|
||||
appearance="plain"
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.date-picker.clear")}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this._setToday}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.date-picker.today")}
|
||||
</ha-button>
|
||||
</div>
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._setValue}>
|
||||
${this.hass.localize("ui.common.ok")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
@@ -108,11 +127,20 @@ export class HaDialogDatePicker extends LitElement {
|
||||
static styles = [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
ha-wa-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
--justify-action-buttons: space-between;
|
||||
}
|
||||
.bottom-actions {
|
||||
display: flex;
|
||||
gap: var(--ha-space-4);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: var(--ha-space-1);
|
||||
}
|
||||
app-datepicker {
|
||||
display: block;
|
||||
margin-inline: auto;
|
||||
--app-datepicker-accent-color: var(--primary-color);
|
||||
--app-datepicker-bg-color: transparent;
|
||||
--app-datepicker-color: var(--primary-text-color);
|
||||
@@ -129,11 +157,6 @@ export class HaDialogDatePicker extends LitElement {
|
||||
app-datepicker::part(body) {
|
||||
direction: ltr;
|
||||
}
|
||||
@media all and (min-width: 450px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 300px;
|
||||
}
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
app-datepicker {
|
||||
width: 100%;
|
||||
|
||||
@@ -2,7 +2,7 @@ import DropdownItem from "@home-assistant/webawesome/dist/components/dropdown-it
|
||||
import "@home-assistant/webawesome/dist/components/icon/icon";
|
||||
import { mdiCheckboxBlankOutline, mdiCheckboxMarked } from "@mdi/js";
|
||||
import { css, type CSSResultGroup, html } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
/**
|
||||
@@ -17,6 +17,8 @@ import "./ha-svg-icon";
|
||||
*/
|
||||
@customElement("ha-dropdown-item")
|
||||
export class HaDropdownItem extends DropdownItem {
|
||||
@property({ type: Boolean, reflect: true }) selected = false;
|
||||
|
||||
protected renderCheckboxIcon() {
|
||||
return html`
|
||||
<ha-svg-icon
|
||||
@@ -47,6 +49,13 @@ export class HaDropdownItem extends DropdownItem {
|
||||
:host([variant="danger"]) #icon ::slotted(*) {
|
||||
color: var(--ha-color-on-danger-quiet);
|
||||
}
|
||||
|
||||
:host([selected]) {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--primary-color);
|
||||
background-color: var(--ha-color-fill-primary-quiet-resting);
|
||||
--icon-primary-color: var(--primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type WaButton from "@home-assistant/webawesome/dist/components/button/button";
|
||||
import Dropdown from "@home-assistant/webawesome/dist/components/dropdown/dropdown";
|
||||
import { css, type CSSResultGroup } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
@@ -22,11 +23,37 @@ export type HaDropdownSelectEvent<T = string> = CustomEvent<{
|
||||
*
|
||||
*/
|
||||
@customElement("ha-dropdown")
|
||||
// @ts-ignore Allow to set an alternative anchor element
|
||||
export class HaDropdown extends Dropdown {
|
||||
@property({ attribute: false }) dropdownTag = "ha-dropdown";
|
||||
|
||||
@property({ attribute: false }) dropdownItemTag = "ha-dropdown-item";
|
||||
|
||||
public get anchorElement(): HTMLButtonElement | WaButton | undefined {
|
||||
// @ts-ignore Allow to set an anchor element on popup
|
||||
return this.popup?.anchor as HTMLButtonElement | WaButton | undefined;
|
||||
}
|
||||
|
||||
public set anchorElement(element: HTMLButtonElement | WaButton | undefined) {
|
||||
// @ts-ignore Allow to get the current anchor element from popup
|
||||
if (!this.popup) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore Allow to get the current anchor element from popup
|
||||
this.popup.anchor = element;
|
||||
}
|
||||
|
||||
/** Get the slotted trigger button, a <wa-button> or <button> element */
|
||||
// @ts-ignore Override parent method to be able to use alternative anchor
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
private override getTrigger(): HTMLButtonElement | WaButton | null {
|
||||
if (this.anchorElement) {
|
||||
return this.anchorElement;
|
||||
}
|
||||
// @ts-ignore fallback to default trigger slot if no anchorElement is set
|
||||
return super.getTrigger();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
Dropdown.styles,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-base-time-input";
|
||||
import type { TimeChangedEvent } from "./ha-base-time-input";
|
||||
import "./ha-button-toggle-group";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
|
||||
export interface HaDurationData {
|
||||
days?: number;
|
||||
@@ -152,7 +153,9 @@ class HaDurationInput extends LitElement {
|
||||
: NaN;
|
||||
}
|
||||
|
||||
private _durationChanged(ev: CustomEvent<{ value?: TimeChangedEvent }>) {
|
||||
private _durationChanged(
|
||||
ev: ValueChangedEvent<TimeChangedEvent | undefined>
|
||||
) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value ? { ...ev.detail.value } : undefined;
|
||||
|
||||
|
||||
@@ -315,9 +315,13 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
ha-list {
|
||||
--mdc-list-item-meta-size: auto;
|
||||
--mdc-list-side-padding-right: 4px;
|
||||
--mdc-list-side-padding-right: var(--ha-space-1);
|
||||
--mdc-list-side-padding-left: var(--ha-space-4);
|
||||
--mdc-icon-button-size: 36px;
|
||||
}
|
||||
ha-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
}
|
||||
ha-dropdown-item {
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
|
||||
@@ -179,6 +179,9 @@ export class HaFilterDomains extends LitElement {
|
||||
margin-inline-start: initial;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
|
||||
@@ -199,6 +199,9 @@ export class HaFilterIntegrations extends LitElement {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
|
||||
@@ -164,6 +164,9 @@ export class HaFilterVoiceAssistants extends LitElement {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
|
||||
@@ -359,7 +359,7 @@ export class HaFloorPicker extends LitElement {
|
||||
{
|
||||
id: ADD_NEW_ID + searchString,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.floor-picker.add_new_sugestion",
|
||||
"ui.components.floor-picker.add_new_suggestion",
|
||||
{
|
||||
name: searchString,
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { HomeAssistant } from "../types";
|
||||
import "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-md-divider";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-tooltip";
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
{
|
||||
id: ADD_NEW_ID + searchString,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.label-picker.add_new_sugestion",
|
||||
"ui.components.label-picker.add_new_suggestion",
|
||||
{
|
||||
name: searchString,
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Divider } from "@material/web/divider/internal/divider";
|
||||
import { styles } from "@material/web/divider/internal/divider-styles";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-md-divider")
|
||||
export class HaMdDivider extends Divider {
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--md-divider-color: var(--divider-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-divider": HaMdDivider;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { MenuItemEl } from "@material/web/menu/internal/menuitem/menu-item";
|
||||
import { styles } from "@material/web/menu/internal/menuitem/menu-item-styles";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-md-menu-item")
|
||||
export class HaMdMenuItem extends MenuItemEl {
|
||||
@property({ attribute: false }) clickAction?: (item?: HTMLElement) => void;
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-on-primary: var(--primary-text-color);
|
||||
--md-sys-color-secondary: var(--secondary-text-color);
|
||||
--md-sys-color-surface: var(--card-background-color);
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||
--md-sys-color-secondary-container: rgba(
|
||||
var(--rgb-primary-color),
|
||||
0.15
|
||||
);
|
||||
--md-sys-color-on-secondary-container: var(--text-primary-color);
|
||||
--mdc-icon-size: 16px;
|
||||
|
||||
--md-sys-color-on-primary-container: var(--primary-text-color);
|
||||
--md-sys-color-on-secondary-container: var(--primary-text-color);
|
||||
--md-menu-item-label-text-font: Roboto, sans-serif;
|
||||
}
|
||||
:host(.warning) {
|
||||
--md-menu-item-label-text-color: var(--error-color);
|
||||
--md-menu-item-leading-icon-color: var(--error-color);
|
||||
}
|
||||
::slotted([slot="headline"]) {
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
:host([disabled]) {
|
||||
opacity: 1;
|
||||
--md-menu-item-label-text-color: var(--disabled-text-color);
|
||||
--md-menu-item-leading-icon-color: var(--disabled-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-menu-item": HaMdMenuItem;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Menu } from "@material/web/menu/internal/menu";
|
||||
import { styles } from "@material/web/menu/internal/menu-styles";
|
||||
import type { CloseMenuEvent } from "@material/web/menu/menu";
|
||||
import {
|
||||
CloseReason,
|
||||
KeydownCloseKey,
|
||||
} from "@material/web/menu/internal/controllers/shared";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import type { HaMdMenuItem } from "./ha-md-menu-item";
|
||||
|
||||
@customElement("ha-md-menu")
|
||||
export class HaMdMenu extends Menu {
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("close-menu", this._handleCloseMenu);
|
||||
}
|
||||
|
||||
private _handleCloseMenu(ev: CloseMenuEvent) {
|
||||
if (
|
||||
ev.detail.reason.kind === CloseReason.KEYDOWN &&
|
||||
ev.detail.reason.key === KeydownCloseKey.ESCAPE
|
||||
) {
|
||||
return;
|
||||
}
|
||||
(ev.detail.initiator as HaMdMenuItem).clickAction?.(ev.detail.initiator);
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--md-sys-color-surface-container: var(--card-background-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-menu": HaMdMenu;
|
||||
}
|
||||
|
||||
interface HTMLElementEventMap {
|
||||
"close-menu": CloseMenuEvent;
|
||||
}
|
||||
}
|
||||
@@ -135,9 +135,7 @@ class HaQrScanner extends LitElement {
|
||||
(camera) => html`
|
||||
<ha-dropdown-item
|
||||
.value=${camera.id}
|
||||
class=${this._selectedCamera === camera.id
|
||||
? "selected"
|
||||
: ""}
|
||||
.selected=${this._selectedCamera === camera.id}
|
||||
>
|
||||
${camera.label}
|
||||
</ha-dropdown-item>
|
||||
@@ -380,9 +378,6 @@ class HaQrScanner extends LitElement {
|
||||
color: white;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
}
|
||||
ha-dropdown-item.selected {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -94,10 +94,8 @@ export class HaSelect extends LitElement {
|
||||
.disabled=${typeof option === "string"
|
||||
? false
|
||||
: (option.disabled ?? false)}
|
||||
class=${this.value ===
|
||||
(typeof option === "string" ? option : option.value)
|
||||
? "selected"
|
||||
: ""}
|
||||
.selected=${this.value ===
|
||||
(typeof option === "string" ? option : option.value)}
|
||||
>
|
||||
${option.iconPath
|
||||
? html`<ha-svg-icon
|
||||
@@ -182,10 +180,6 @@ export class HaSelect extends LitElement {
|
||||
ha-picker-field.opened {
|
||||
--mdc-text-field-idle-line-color: var(--primary-color);
|
||||
}
|
||||
ha-dropdown-item.selected:hover {
|
||||
background-color: var(--ha-color-fill-primary-quiet-hover);
|
||||
}
|
||||
|
||||
ha-dropdown-item .content {
|
||||
display: flex;
|
||||
gap: var(--ha-space-1);
|
||||
@@ -200,14 +194,6 @@ export class HaSelect extends LitElement {
|
||||
ha-dropdown::part(menu) {
|
||||
min-width: var(--select-menu-width);
|
||||
}
|
||||
|
||||
:host ::slotted(ha-dropdown-item.selected),
|
||||
ha-dropdown-item.selected {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--primary-color);
|
||||
background-color: var(--ha-color-fill-primary-quiet-resting);
|
||||
--icon-primary-color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
declare global {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { FrontendLocaleData } from "../data/translation";
|
||||
import "./ha-base-time-input";
|
||||
import type { TimeChangedEvent } from "./ha-base-time-input";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
|
||||
@customElement("ha-time-input")
|
||||
export class HaTimeInput extends LitElement {
|
||||
@@ -69,7 +70,7 @@ export class HaTimeInput extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _timeChanged(ev: CustomEvent<{ value?: TimeChangedEvent }>) {
|
||||
private _timeChanged(ev: ValueChangedEvent<TimeChangedEvent | undefined>) {
|
||||
ev.stopPropagation();
|
||||
const eventValue = ev.detail.value;
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { isIosApp } from "../util/is_ios";
|
||||
import "./ha-dialog-header";
|
||||
import "./ha-icon-button";
|
||||
|
||||
@@ -197,22 +198,21 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
await this.updateComplete;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
// temporary disabled because of issues with focus in iOS app, can be reenabled in 2026.2.0
|
||||
// if (isIosApp(this.hass)) {
|
||||
// const element = this.querySelector("[autofocus]");
|
||||
// if (element !== null) {
|
||||
// if (!element.id) {
|
||||
// element.id = "ha-wa-dialog-autofocus";
|
||||
// }
|
||||
// this.hass.auth.external!.fireMessage({
|
||||
// type: "focus_element",
|
||||
// payload: {
|
||||
// element_id: element.id,
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
if (isIosApp(this.hass)) {
|
||||
const element = this.querySelector("[autofocus]");
|
||||
if (element !== null) {
|
||||
if (!element.id) {
|
||||
element.id = "ha-wa-dialog-autofocus";
|
||||
}
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "focus_element",
|
||||
payload: {
|
||||
element_id: element.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -24,6 +24,8 @@ import "../ha-icon-button";
|
||||
import "./hat-logbook-note";
|
||||
import type { NodeInfo } from "./hat-script-graph";
|
||||
import { traceTabStyles } from "./trace-tab-styles";
|
||||
import type { Trigger } from "../../data/automation";
|
||||
import { migrateAutomationTrigger } from "../../data/automation";
|
||||
|
||||
const TRACE_PATH_TABS = [
|
||||
"step_config",
|
||||
@@ -166,7 +168,9 @@ export class HaTracePathDetails extends LitElement {
|
||||
: selectedType === "trigger"
|
||||
? html`<h2>
|
||||
${describeTrigger(
|
||||
currentDetail,
|
||||
migrateAutomationTrigger({
|
||||
...currentDetail,
|
||||
}) as Trigger,
|
||||
this.hass,
|
||||
this._entityReg
|
||||
)}
|
||||
|
||||
@@ -32,12 +32,13 @@ export class VoiceAssistantBrandicon extends LitElement {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: inline;
|
||||
}
|
||||
.logo {
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
height: 24px;
|
||||
margin-right: 16px;
|
||||
margin-inline-end: 16px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
+5
-20
@@ -14,6 +14,7 @@ import {
|
||||
import type { Collection, HassEntity } from "home-assistant-js-websocket";
|
||||
import { getCollection } from "home-assistant-js-websocket";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { normalizeValueBySIPrefix } from "../common/number/normalize-by-si-prefix";
|
||||
import {
|
||||
calcDate,
|
||||
calcDateProperty,
|
||||
@@ -1431,26 +1432,10 @@ export const getPowerFromState = (stateObj: HassEntity): number | undefined => {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Normalize to watts (W) based on unit of measurement (case-sensitive)
|
||||
// Supported units: GW, kW, MW, mW, TW, W
|
||||
const unit = stateObj.attributes.unit_of_measurement;
|
||||
switch (unit) {
|
||||
case "W":
|
||||
return value;
|
||||
case "kW":
|
||||
return value * 1000;
|
||||
case "mW":
|
||||
return value / 1000;
|
||||
case "MW":
|
||||
return value * 1_000_000;
|
||||
case "GW":
|
||||
return value * 1_000_000_000;
|
||||
case "TW":
|
||||
return value * 1_000_000_000_000;
|
||||
default:
|
||||
// Assume value is in watts (W) if no unit or an unsupported unit is provided
|
||||
return value;
|
||||
}
|
||||
return normalizeValueBySIPrefix(
|
||||
value,
|
||||
stateObj.attributes.unit_of_measurement
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+2
-1
@@ -142,7 +142,7 @@ export const subscribeHistory = (
|
||||
);
|
||||
};
|
||||
|
||||
class HistoryStream {
|
||||
export class HistoryStream {
|
||||
hass: HomeAssistant;
|
||||
|
||||
hoursToShow?: number;
|
||||
@@ -221,6 +221,7 @@ class HistoryStream {
|
||||
// only expire the rest of the history as it ages.
|
||||
const lastExpiredState = expiredStates[expiredStates.length - 1];
|
||||
lastExpiredState.lu = purgeBeforePythonTime;
|
||||
delete lastExpiredState.lc;
|
||||
newHistory[entityId].unshift(lastExpiredState);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -41,12 +41,16 @@ export const enum TodoListEntityFeature {
|
||||
SET_DESCRIPTION_ON_ITEM = 64,
|
||||
}
|
||||
|
||||
export const getTodoLists = (hass: HomeAssistant): TodoList[] =>
|
||||
export const getTodoLists = (
|
||||
hass: HomeAssistant,
|
||||
includeHidden = true
|
||||
): TodoList[] =>
|
||||
Object.keys(hass.states)
|
||||
.filter(
|
||||
(entityId) =>
|
||||
computeDomain(entityId) === "todo" &&
|
||||
!isUnavailableState(hass.states[entityId].state)
|
||||
!isUnavailableState(hass.states[entityId].state) &&
|
||||
(includeHidden || hass.entities[entityId]?.hidden !== true)
|
||||
)
|
||||
.map((entityId) => ({
|
||||
...hass.states[entityId],
|
||||
|
||||
@@ -213,9 +213,7 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
(source) =>
|
||||
html`<ha-dropdown-item
|
||||
.value=${source}
|
||||
class=${source === this.stateObj?.attributes.source
|
||||
? "selected"
|
||||
: ""}
|
||||
.selected=${source === this.stateObj?.attributes.source}
|
||||
>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj!,
|
||||
@@ -250,9 +248,7 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
(soundMode) =>
|
||||
html`<ha-dropdown-item
|
||||
.value=${soundMode}
|
||||
class=${soundMode === this.stateObj?.attributes.sound_mode
|
||||
? "selected"
|
||||
: ""}
|
||||
.selected=${soundMode === this.stateObj?.attributes.sound_mode}
|
||||
>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj!,
|
||||
@@ -678,13 +674,6 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
align-self: center;
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
ha-dropdown-item.selected {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--primary-color);
|
||||
background-color: var(--ha-color-fill-primary-quiet-resting);
|
||||
--icon-primary-color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
|
||||
private _handleClick(e: MouseEvent): void {
|
||||
|
||||
@@ -313,113 +313,119 @@ class MoreInfoWeather extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<div class="section">
|
||||
${this.hass.localize("ui.card.weather.forecast")}:
|
||||
</div>
|
||||
${supportedForecasts?.length > 1
|
||||
? html`<ha-tab-group @wa-tab-show=${this._handleForecastTypeChanged}>
|
||||
${supportedForecasts.map(
|
||||
(forecastType) =>
|
||||
html`<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.panel=${forecastType}
|
||||
.active=${this._forecastType === forecastType}
|
||||
${supportedForecasts?.length
|
||||
? html`
|
||||
<div class="section">
|
||||
${this.hass.localize("ui.card.weather.forecast")}:
|
||||
</div>
|
||||
${supportedForecasts?.length > 1
|
||||
? html`<ha-tab-group
|
||||
@wa-tab-show=${this._handleForecastTypeChanged}
|
||||
>
|
||||
${this.hass!.localize(`ui.card.weather.${forecastType}`)}
|
||||
</ha-tab-group-tab>`
|
||||
)}
|
||||
</ha-tab-group>`
|
||||
: nothing}
|
||||
<div class="forecast">
|
||||
${forecast?.length
|
||||
? this._groupForecastByDay(forecast).map((dayForecast) => {
|
||||
const showDayHeader = hourly || dayNight;
|
||||
return html`
|
||||
<div class="forecast-day">
|
||||
${showDayHeader
|
||||
? html`<div class="forecast-day-header">
|
||||
${formatDateWeekdayShort(
|
||||
new Date(dayForecast[0].datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
${supportedForecasts.map(
|
||||
(forecastType) =>
|
||||
html`<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.panel=${forecastType}
|
||||
.active=${this._forecastType === forecastType}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.card.weather.${forecastType}`
|
||||
)}
|
||||
</div>`
|
||||
: nothing}
|
||||
<div class="forecast-day-content">
|
||||
${dayForecast.map((item) =>
|
||||
this._showValue(item.templow) ||
|
||||
this._showValue(item.temperature)
|
||||
? html`
|
||||
<div class="forecast-item">
|
||||
<div
|
||||
class="forecast-item-label ${showDayHeader
|
||||
? ""
|
||||
: "no-header"}"
|
||||
>
|
||||
${hourly
|
||||
? formatTime(
|
||||
new Date(item.datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)
|
||||
: dayNight
|
||||
? html`<div class="daynight">
|
||||
${item.is_daytime !== false
|
||||
? this.hass!.localize(
|
||||
"ui.card.weather.day"
|
||||
)
|
||||
: this.hass!.localize(
|
||||
"ui.card.weather.night"
|
||||
</ha-tab-group-tab>`
|
||||
)}
|
||||
</ha-tab-group>`
|
||||
: nothing}
|
||||
<div class="forecast">
|
||||
${forecast?.length
|
||||
? this._groupForecastByDay(forecast).map((dayForecast) => {
|
||||
const showDayHeader = hourly || dayNight;
|
||||
return html`
|
||||
<div class="forecast-day">
|
||||
${showDayHeader
|
||||
? html`<div class="forecast-day-header">
|
||||
${formatDateWeekdayShort(
|
||||
new Date(dayForecast[0].datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)}
|
||||
</div>`
|
||||
: nothing}
|
||||
<div class="forecast-day-content">
|
||||
${dayForecast.map((item) =>
|
||||
this._showValue(item.templow) ||
|
||||
this._showValue(item.temperature)
|
||||
? html`
|
||||
<div class="forecast-item">
|
||||
<div
|
||||
class="forecast-item-label ${showDayHeader
|
||||
? ""
|
||||
: "no-header"}"
|
||||
>
|
||||
${hourly
|
||||
? formatTime(
|
||||
new Date(item.datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)
|
||||
: dayNight
|
||||
? html`<div class="daynight">
|
||||
${item.is_daytime !== false
|
||||
? this.hass!.localize(
|
||||
"ui.card.weather.day"
|
||||
)
|
||||
: this.hass!.localize(
|
||||
"ui.card.weather.night"
|
||||
)}
|
||||
</div>`
|
||||
: formatDateWeekdayShort(
|
||||
new Date(item.datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)}
|
||||
</div>`
|
||||
: formatDateWeekdayShort(
|
||||
new Date(item.datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)}
|
||||
</div>
|
||||
${this._showValue(item.condition)
|
||||
? html`
|
||||
<div class="forecast-image-icon">
|
||||
${getWeatherStateIcon(
|
||||
item.condition!,
|
||||
this,
|
||||
!(
|
||||
item.is_daytime ||
|
||||
item.is_daytime === undefined
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div class="temp">
|
||||
${this._showValue(item.temperature)
|
||||
? html`${formatNumber(
|
||||
item.temperature,
|
||||
this.hass!.locale
|
||||
)}°`
|
||||
: "—"}
|
||||
</div>
|
||||
<div class="templow">
|
||||
${this._showValue(item.templow)
|
||||
? html`${formatNumber(
|
||||
item.templow!,
|
||||
this.hass!.locale
|
||||
)}°`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
: html`<ha-spinner size="medium"></ha-spinner>`}
|
||||
</div>
|
||||
|
||||
${this._showValue(item.condition)
|
||||
? html`
|
||||
<div class="forecast-image-icon">
|
||||
${getWeatherStateIcon(
|
||||
item.condition!,
|
||||
this,
|
||||
!(
|
||||
item.is_daytime ||
|
||||
item.is_daytime === undefined
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div class="temp">
|
||||
${this._showValue(item.temperature)
|
||||
? html`${formatNumber(
|
||||
item.temperature,
|
||||
this.hass!.locale
|
||||
)}°`
|
||||
: "—"}
|
||||
</div>
|
||||
<div class="templow">
|
||||
${this._showValue(item.templow)
|
||||
? html`${formatNumber(
|
||||
item.templow!,
|
||||
this.hass!.locale
|
||||
)}°`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
: html`<ha-spinner size="medium"></ha-spinner>`}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${this.stateObj.attributes.attribution
|
||||
? html`
|
||||
<div class="attribution">
|
||||
|
||||
@@ -129,11 +129,12 @@ export class CloudStepIntro extends LitElement {
|
||||
}
|
||||
.feature .logos {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
gap: var(--ha-space-4);
|
||||
}
|
||||
.feature .logos > * {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 0 4px;
|
||||
}
|
||||
.round-icon {
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
|
||||
@@ -196,7 +196,7 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
(lang) =>
|
||||
html`<ha-dropdown-item
|
||||
.value=${lang.id}
|
||||
class=${this._language === lang.id ? "selected" : ""}
|
||||
.selected=${this._language === lang.id}
|
||||
>
|
||||
${lang.primary}
|
||||
</ha-dropdown-item>`
|
||||
@@ -407,13 +407,6 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
margin-inline-end: 12px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
ha-dropdown-item.selected {
|
||||
border: 1px solid var(--primary-color);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--primary-color);
|
||||
background-color: var(--ha-color-fill-primary-quiet-resting);
|
||||
--icon-primary-color: var(--primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ export interface MockHomeAssistant extends HomeAssistant {
|
||||
mockEvent(event);
|
||||
mockTheme(theme: Record<string, string> | null);
|
||||
formatEntityState(stateObj: HassEntity, state?: string): string;
|
||||
formatEntityStateToParts(stateObj: HassEntity, state?: string): ValuePart[];
|
||||
formatEntityAttributeValue(
|
||||
stateObj: HassEntity,
|
||||
attribute: string,
|
||||
@@ -117,6 +118,7 @@ export const provideHass = (
|
||||
async function updateFormatFunctions() {
|
||||
const {
|
||||
formatEntityState,
|
||||
formatEntityStateToParts,
|
||||
formatEntityAttributeName,
|
||||
formatEntityAttributeValue,
|
||||
formatEntityAttributeValueToParts,
|
||||
@@ -133,6 +135,7 @@ export const provideHass = (
|
||||
);
|
||||
hass().updateHass({
|
||||
formatEntityState,
|
||||
formatEntityStateToParts,
|
||||
formatEntityAttributeName,
|
||||
formatEntityAttributeValue,
|
||||
formatEntityAttributeValueToParts,
|
||||
@@ -375,6 +378,12 @@ export const provideHass = (
|
||||
floors: {},
|
||||
formatEntityState: (stateObj, state) =>
|
||||
(state !== null ? state : stateObj.state) ?? "",
|
||||
formatEntityStateToParts: (stateObj, state) => [
|
||||
{
|
||||
type: "value",
|
||||
value: (state !== null ? state : stateObj.state) ?? "",
|
||||
},
|
||||
],
|
||||
formatEntityAttributeName: (_stateObj, attribute) => attribute,
|
||||
formatEntityAttributeValue: (stateObj, attribute, value) =>
|
||||
value !== null ? value : (stateObj.attributes[attribute] ?? ""),
|
||||
|
||||
@@ -74,6 +74,8 @@ export class HAFullCalendar extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@property({ attribute: "add-fab", type: Boolean }) public addFab = false;
|
||||
|
||||
@property({ attribute: false }) public events: CalendarEvent[] = [];
|
||||
|
||||
@property({ attribute: false }) public calendars: CalendarData[] = [];
|
||||
@@ -208,7 +210,7 @@ export class HAFullCalendar extends LitElement {
|
||||
: ""}
|
||||
|
||||
<div id="calendar"></div>
|
||||
${this._hasMutableCalendars
|
||||
${this.addFab && this._hasMutableCalendars
|
||||
? html`<ha-fab
|
||||
slot="fab"
|
||||
.label=${this.hass.localize("ui.components.calendar.event.add")}
|
||||
|
||||
@@ -193,6 +193,7 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
|
||||
</ha-list-item>`
|
||||
: nothing}
|
||||
<ha-full-calendar
|
||||
add-fab
|
||||
.events=${this._events}
|
||||
.calendars=${this._calendars}
|
||||
.narrow=${this.narrow}
|
||||
@@ -330,6 +331,8 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
|
||||
|
||||
ha-dropdown-item {
|
||||
padding-left: 32px;
|
||||
padding-inline-start: 32px;
|
||||
padding-inline-end: initial;
|
||||
--icon-primary-color: var(--ha-color-fill-neutral-loud-resting);
|
||||
}
|
||||
|
||||
@@ -339,6 +342,8 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
|
||||
|
||||
:host([mobile]) {
|
||||
padding-left: unset;
|
||||
padding-inline-start: unset;
|
||||
padding-inline-end: initial;
|
||||
}
|
||||
.loading {
|
||||
display: flex;
|
||||
|
||||
@@ -63,6 +63,7 @@ class SupervisorAppDocumentationDashboard extends LitElement {
|
||||
margin: auto;
|
||||
padding: var(--ha-space-2);
|
||||
max-width: 1024px;
|
||||
direction: ltr;
|
||||
}
|
||||
ha-markdown {
|
||||
padding: var(--ha-space-4);
|
||||
|
||||
@@ -102,7 +102,7 @@ export class HaConditionAction
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dropdown-item .value=${opt} class=${selected ? "selected" : ""}>
|
||||
<ha-dropdown-item .value=${opt} .selected=${selected}>
|
||||
<ha-condition-icon
|
||||
.hass=${this.hass}
|
||||
slot="icon"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiAppleKeyboardCommand,
|
||||
@@ -42,7 +43,6 @@ import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-button-prev";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-md-divider";
|
||||
import "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import type { PickerComboBoxItem } from "../../../components/ha-picker-combo-box";
|
||||
@@ -657,10 +657,7 @@ class DialogAddAutomationElement
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>
|
||||
</ha-md-list-item>
|
||||
<ha-md-divider
|
||||
role="separator"
|
||||
tabindex="-1"
|
||||
></ha-md-divider>`
|
||||
<wa-divider></wa-divider>`
|
||||
: nothing}
|
||||
${collections.map(
|
||||
(collection, index) => html`
|
||||
@@ -2177,8 +2174,8 @@ class DialogAddAutomationElement
|
||||
width: var(--ha-space-6);
|
||||
}
|
||||
|
||||
ha-md-list-item.paste {
|
||||
border-bottom: 1px solid var(--ha-color-border-neutral-quiet);
|
||||
wa-divider {
|
||||
--spacing: 0;
|
||||
}
|
||||
|
||||
ha-svg-icon.plus {
|
||||
|
||||
@@ -47,6 +47,10 @@ import type {
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/entity/ha-entity-toggle";
|
||||
import "../../../components/ha-dropdown";
|
||||
import type {
|
||||
HaDropdown,
|
||||
HaDropdownSelectEvent,
|
||||
} from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-filter-blueprints";
|
||||
@@ -57,10 +61,6 @@ import "../../../components/ha-filter-floor-areas";
|
||||
import "../../../components/ha-filter-labels";
|
||||
import "../../../components/ha-filter-voice-assistants";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-md-menu";
|
||||
import type { HaMdMenu } from "../../../components/ha-md-menu";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
import type { HaMdMenuItem } from "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
@@ -84,9 +84,9 @@ import { fullEntitiesContext } from "../../../data/context";
|
||||
import type { DataTableFilters } from "../../../data/data_table_filters";
|
||||
import {
|
||||
deserializeFilters,
|
||||
serializeFilters,
|
||||
isFilterUsed,
|
||||
isRelatedItemsFilterUsed,
|
||||
serializeFilters,
|
||||
} from "../../../data/data_table_filters";
|
||||
import { UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type {
|
||||
@@ -111,16 +111,16 @@ import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route, ServiceCallResponse } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { turnOnOffEntity } from "../../lovelace/common/entity/turn-on-off-entity";
|
||||
import {
|
||||
getEntityIdHiddenTableColumn,
|
||||
getAreaTableColumn,
|
||||
getCategoryTableColumn,
|
||||
getLabelsTableColumn,
|
||||
getTriggeredAtTableColumn,
|
||||
} from "../common/data-table-columns";
|
||||
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
|
||||
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
|
||||
import {
|
||||
getAreaTableColumn,
|
||||
getCategoryTableColumn,
|
||||
getEntityIdHiddenTableColumn,
|
||||
getLabelsTableColumn,
|
||||
getTriggeredAtTableColumn,
|
||||
} from "../common/data-table-columns";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import {
|
||||
@@ -129,7 +129,6 @@ import {
|
||||
} from "../voice-assistants/expose/assistants-table-column";
|
||||
import { getAvailableAssistants } from "../voice-assistants/expose/available-assistants";
|
||||
import { showNewAutomationDialog } from "./show-dialog-new-automation";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
|
||||
type AutomationItem = AutomationEntity & {
|
||||
name: string;
|
||||
@@ -223,7 +222,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
@query("#overflow-menu") private _overflowMenu!: HaMdMenu;
|
||||
@query("#overflow-menu") private _overflowMenu!: HaDropdown;
|
||||
|
||||
private _sizeController = new ResizeController(this, {
|
||||
callback: (entries) => entries[0]?.contentRect.width,
|
||||
@@ -233,6 +232,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
return getAvailableAssistants(this.cloudStatus, this.hass);
|
||||
}
|
||||
|
||||
private _openingOverflow = false;
|
||||
|
||||
private _automations = memoizeOne(
|
||||
(
|
||||
automations: AutomationEntity[],
|
||||
@@ -371,16 +372,27 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
|
||||
private _showOverflowMenu = (ev) => {
|
||||
if (
|
||||
this._overflowMenu.open &&
|
||||
ev.target === this._overflowMenu.anchorElement
|
||||
) {
|
||||
this._overflowMenu.close();
|
||||
if (this._overflowMenu.anchorElement === ev.target) {
|
||||
this._overflowMenu.anchorElement = undefined;
|
||||
return;
|
||||
}
|
||||
this._overflowAutomation = ev.target.automation;
|
||||
this._openingOverflow = true;
|
||||
this._overflowMenu.anchorElement = ev.target;
|
||||
this._overflowMenu.show();
|
||||
this._overflowAutomation = ev.target.automation;
|
||||
this._overflowMenu.open = true;
|
||||
};
|
||||
|
||||
private _overflowMenuOpened = () => {
|
||||
this._openingOverflow = false;
|
||||
};
|
||||
|
||||
private _overflowMenuClosed = () => {
|
||||
// changing the anchorElement triggers a close event, ignore it
|
||||
if (this._openingOverflow) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._overflowMenu.anchorElement = undefined;
|
||||
};
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
@@ -697,74 +709,58 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
</hass-tabs-subpage-data-table>
|
||||
<ha-md-menu id="overflow-menu" positioning="fixed">
|
||||
<ha-md-menu-item .clickAction=${this._showInfo}>
|
||||
<ha-svg-icon
|
||||
.path=${mdiInformationOutline}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.automation.editor.show_info")}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-dropdown
|
||||
id="overflow-menu"
|
||||
@wa-select=${this._handleOverflowAction}
|
||||
@wa-after-show=${this._overflowMenuOpened}
|
||||
@wa-after-hide=${this._overflowMenuClosed}
|
||||
>
|
||||
<ha-dropdown-item value="show_info">
|
||||
<ha-svg-icon .path=${mdiInformationOutline} slot="icon"></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.show_info")}
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-md-menu-item .clickAction=${this._showSettings}>
|
||||
<ha-svg-icon .path=${mdiCog} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.show_settings"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._editCategory}>
|
||||
<ha-svg-icon .path=${mdiTag} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.picker.${this._overflowAutomation?.category ? "edit_category" : "assign_category"}`
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._runActions}>
|
||||
<ha-svg-icon .path=${mdiPlay} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.automation.editor.run")}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._showTrace}>
|
||||
<ha-svg-icon .path=${mdiTransitConnection} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.show_trace"
|
||||
)}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item .clickAction=${this._duplicate}>
|
||||
<ha-svg-icon .path=${mdiContentDuplicate} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.automation.picker.duplicate")}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._toggle}>
|
||||
<ha-dropdown-item value="show_settings">
|
||||
<ha-svg-icon .path=${mdiCog} slot="icon"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.show_settings"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="edit_category">
|
||||
<ha-svg-icon .path=${mdiTag} slot="icon"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.picker.${this._overflowAutomation?.category ? "edit_category" : "assign_category"}`
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="run_actions">
|
||||
<ha-svg-icon .path=${mdiPlay} slot="icon"></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.run")}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="show_trace">
|
||||
<ha-svg-icon .path=${mdiTransitConnection} slot="icon"></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.show_trace")}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider></wa-divider>
|
||||
<ha-dropdown-item value="duplicate">
|
||||
<ha-svg-icon .path=${mdiContentDuplicate} slot="icon"></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.automation.picker.duplicate")}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="toggle">
|
||||
<ha-svg-icon
|
||||
.path=${this._overflowAutomation?.state === "off"
|
||||
? mdiToggleSwitch
|
||||
: mdiToggleSwitchOffOutline}
|
||||
slot="start"
|
||||
slot="icon"
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this._overflowAutomation?.state === "off"
|
||||
? this.hass.localize("ui.panel.config.automation.editor.enable")
|
||||
: this.hass.localize("ui.panel.config.automation.editor.disable")}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._deleteConfirm} class="warning">
|
||||
<ha-svg-icon .path=${mdiDelete} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.automation.picker.delete")}
|
||||
</div>
|
||||
</ha-md-menu-item>
|
||||
</ha-md-menu>
|
||||
${this._overflowAutomation?.state === "off"
|
||||
? this.hass.localize("ui.panel.config.automation.editor.enable")
|
||||
: this.hass.localize("ui.panel.config.automation.editor.disable")}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="delete" variant="danger">
|
||||
<ha-svg-icon .path=${mdiDelete} slot="icon"></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.automation.picker.delete")}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -901,33 +897,59 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
this._applyFilters();
|
||||
}
|
||||
|
||||
private _showInfo = (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
fireEvent(this, "hass-more-info", { entityId: automation.entity_id });
|
||||
private _handleOverflowAction = (ev: HaDropdownSelectEvent) => {
|
||||
const action = ev.detail.item.value;
|
||||
|
||||
if (!action || !this._overflowAutomation) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case "show_info":
|
||||
this._showInfo(this._overflowAutomation);
|
||||
break;
|
||||
case "show_settings":
|
||||
this._showSettings(this._overflowAutomation);
|
||||
break;
|
||||
case "edit_category":
|
||||
this._editCategory(this._overflowAutomation);
|
||||
break;
|
||||
case "run_actions":
|
||||
this._runActions(this._overflowAutomation);
|
||||
break;
|
||||
case "show_trace":
|
||||
this._showTrace(this._overflowAutomation);
|
||||
break;
|
||||
case "toggle":
|
||||
this._toggle(this._overflowAutomation);
|
||||
break;
|
||||
case "delete":
|
||||
this._deleteConfirm(this._overflowAutomation);
|
||||
break;
|
||||
case "duplicate":
|
||||
this._duplicate(this._overflowAutomation);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private _showSettings = (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
private _showInfo = (automation: AutomationItem) => {
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId: automation.entity_id,
|
||||
});
|
||||
};
|
||||
|
||||
private _showSettings = (automation: AutomationItem) => {
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId: automation.entity_id,
|
||||
view: "settings",
|
||||
});
|
||||
};
|
||||
|
||||
private _runActions = (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
private _runActions = (automation: AutomationItem) => {
|
||||
triggerAutomationActions(this.hass, automation.entity_id);
|
||||
};
|
||||
|
||||
private _editCategory = (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
private _editCategory = (automation: AutomationItem) => {
|
||||
const entityReg = this._entityReg.find(
|
||||
(reg) => reg.entity_id === automation.entity_id
|
||||
);
|
||||
@@ -948,10 +970,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
};
|
||||
|
||||
private _showTrace = (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
private _showTrace = (automation: AutomationItem) => {
|
||||
if (!automation.attributes.id) {
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
@@ -965,20 +984,14 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
};
|
||||
|
||||
private _toggle = async (item: HaMdMenuItem): Promise<void> => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
private _toggle = async (automation: AutomationItem): Promise<void> => {
|
||||
const service = automation.state === "off" ? "turn_on" : "turn_off";
|
||||
await this.hass.callService("automation", service, {
|
||||
entity_id: automation.entity_id,
|
||||
});
|
||||
};
|
||||
|
||||
private _deleteConfirm = async (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
private _deleteConfirm = async (automation: AutomationItem) => {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.picker.delete_confirm_title"
|
||||
@@ -994,9 +1007,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
};
|
||||
|
||||
private async _delete(automation) {
|
||||
private async _delete(automation: AutomationItem) {
|
||||
try {
|
||||
await deleteAutomation(this.hass, automation.attributes.id);
|
||||
await deleteAutomation(this.hass, automation.attributes.id!);
|
||||
this._selected = this._selected.filter(
|
||||
(entityId) => entityId !== automation.entity_id
|
||||
);
|
||||
@@ -1015,14 +1028,11 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _duplicate = async (item: HaMdMenuItem) => {
|
||||
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
|
||||
.automation;
|
||||
|
||||
private _duplicate = async (automation: AutomationItem) => {
|
||||
try {
|
||||
const config = await fetchAutomationFileConfig(
|
||||
this.hass,
|
||||
automation.attributes.id
|
||||
automation.attributes.id!
|
||||
);
|
||||
duplicateAutomation(config);
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -438,7 +438,6 @@ export class HaManualAutomationEditor extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _saveAutomation() {
|
||||
this.triggerCloseSidebar();
|
||||
fireEvent(this, "save-automation");
|
||||
}
|
||||
|
||||
|
||||
@@ -40,9 +40,6 @@ export const rowStyles = css`
|
||||
.warning ul {
|
||||
margin: 4px 0;
|
||||
}
|
||||
ha-md-menu-item > ha-svg-icon {
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
ha-tooltip {
|
||||
cursor: default;
|
||||
}
|
||||
@@ -272,7 +269,4 @@ export const overflowStyles = css`
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
ha-md-menu-item {
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -4,7 +4,8 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import { createCloseHeading } from "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-wa-dialog";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
@@ -36,13 +37,20 @@ class LocalBackupLocationDialog extends LitElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public async showDialog(
|
||||
dialogParams: LocalBackupLocationDialogParams
|
||||
): Promise<void> {
|
||||
this._dialogParams = dialogParams;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._data = undefined;
|
||||
this._error = undefined;
|
||||
this._waiting = undefined;
|
||||
@@ -55,17 +63,13 @@ class LocalBackupLocationDialog extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize(
|
||||
`ui.panel.config.backup.dialogs.local_backup_location.title`
|
||||
)
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this.hass.localize(
|
||||
`ui.panel.config.backup.dialogs.local_backup_location.title`
|
||||
)}
|
||||
@closed=${this.closeDialog}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
@@ -77,34 +81,35 @@ class LocalBackupLocationDialog extends LitElement {
|
||||
)}
|
||||
</p>
|
||||
<ha-form
|
||||
autofocus
|
||||
.hass=${this.hass}
|
||||
.data=${this._data}
|
||||
.schema=${SCHEMA}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
dialogInitialFocus
|
||||
></ha-form>
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.backup.dialogs.local_backup_location.note`
|
||||
)}
|
||||
</ha-alert>
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
dialogInitialFocus
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
.disabled=${this._waiting || !this._data}
|
||||
slot="primaryAction"
|
||||
@click=${this._changeMount}
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
.disabled=${this._waiting || !this._data}
|
||||
slot="primaryAction"
|
||||
@click=${this._changeMount}
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -143,9 +148,6 @@ class LocalBackupLocationDialog extends LitElement {
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 500px;
|
||||
}
|
||||
ha-form {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
|
||||
@@ -26,15 +26,16 @@ import type {
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dropdown";
|
||||
import type {
|
||||
HaDropdown,
|
||||
HaDropdownSelectEvent,
|
||||
} from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-filter-states";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/ha-md-menu";
|
||||
import type { HaMdMenu } from "../../../components/ha-md-menu";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type {
|
||||
@@ -73,7 +74,6 @@ import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup"
|
||||
import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup";
|
||||
import { showUploadBackupDialog } from "./dialogs/show-dialog-upload-backup";
|
||||
import { downloadBackup } from "./helper/download_backup";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
|
||||
interface BackupRow extends DataTableRowData, BackupContent {
|
||||
formatted_type: string;
|
||||
@@ -123,7 +123,11 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
@query("hass-tabs-subpage-data-table", true)
|
||||
private _dataTable!: HaTabsSubpageDataTable;
|
||||
|
||||
@query("#overflow-menu") private _overflowMenu?: HaMdMenu;
|
||||
@query("#overflow-menu") private _overflowMenu?: HaDropdown;
|
||||
|
||||
private _openingOverflow = false;
|
||||
|
||||
private _overflowBackup?: BackupRow;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
@@ -287,12 +291,27 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._overflowMenu.open) {
|
||||
this._overflowMenu.close();
|
||||
if (this._overflowMenu.anchorElement === ev.target) {
|
||||
this._overflowMenu.anchorElement = undefined;
|
||||
return;
|
||||
}
|
||||
this._openingOverflow = true;
|
||||
this._overflowMenu.anchorElement = ev.target;
|
||||
this._overflowMenu.show();
|
||||
this._overflowBackup = ev.target.backup;
|
||||
this._overflowMenu.open = true;
|
||||
};
|
||||
|
||||
private _overflowMenuOpened = () => {
|
||||
this._openingOverflow = false;
|
||||
};
|
||||
|
||||
private _overflowMenuClosed = () => {
|
||||
// changing the anchorElement triggers a close event, ignore it
|
||||
if (this._openingOverflow || !this._overflowMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._overflowMenu.anchorElement = undefined;
|
||||
};
|
||||
|
||||
private _handleGroupingChanged(ev: CustomEvent) {
|
||||
@@ -477,16 +496,21 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
`
|
||||
: nothing}
|
||||
</hass-tabs-subpage-data-table>
|
||||
<ha-md-menu id="overflow-menu" positioning="fixed">
|
||||
<ha-md-menu-item .clickAction=${this._downloadBackup}>
|
||||
<ha-svg-icon slot="start" .path=${mdiDownload}></ha-svg-icon>
|
||||
<ha-dropdown
|
||||
id="overflow-menu"
|
||||
@wa-select=${this._handleOverflowAction}
|
||||
@wa-after-show=${this._overflowMenuOpened}
|
||||
@wa-after-hide=${this._overflowMenuClosed}
|
||||
>
|
||||
<ha-dropdown-item value="download">
|
||||
<ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon>
|
||||
${this.hass.localize("ui.common.download")}
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item class="warning" .clickAction=${this._deleteBackup}>
|
||||
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item variant="danger" value="delete">
|
||||
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
|
||||
${this.hass.localize("ui.common.delete")}
|
||||
</ha-md-menu-item>
|
||||
</ha-md-menu>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -556,16 +580,29 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
navigate(`/config/backup/details/${id}`);
|
||||
}
|
||||
|
||||
private _downloadBackup = async (ev): Promise<void> => {
|
||||
const backup = ev.parentElement.anchorElement.backup;
|
||||
private _handleOverflowAction = (ev: HaDropdownSelectEvent) => {
|
||||
const action = ev.detail.item.value;
|
||||
|
||||
if (action === "download") {
|
||||
this._downloadBackup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "delete") {
|
||||
this._deleteBackup();
|
||||
}
|
||||
};
|
||||
|
||||
private _downloadBackup = async (): Promise<void> => {
|
||||
const backup = this._overflowBackup;
|
||||
if (!backup) {
|
||||
return;
|
||||
}
|
||||
downloadBackup(this.hass, this, backup, this.config);
|
||||
};
|
||||
|
||||
private _deleteBackup = async (ev): Promise<void> => {
|
||||
const backup = ev.parentElement.anchorElement.backup;
|
||||
private _deleteBackup = async (): Promise<void> => {
|
||||
const backup = this._overflowBackup;
|
||||
if (!backup) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6,17 +6,19 @@ import { documentationUrl } from "../../../util/documentation-url";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-code-editor";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-dialog-header";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-textfield";
|
||||
import "../../../components/ha-wa-dialog";
|
||||
import type { HaTextField } from "../../../components/ha-textfield";
|
||||
import type { BlueprintImportResult } from "../../../data/blueprint";
|
||||
import { importBlueprint, saveBlueprint } from "../../../data/blueprint";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { withViewTransition } from "../../../common/util/view-transition";
|
||||
|
||||
@customElement("ha-dialog-import-blueprint")
|
||||
class DialogImportBlueprint extends LitElement {
|
||||
@@ -26,6 +28,8 @@ class DialogImportBlueprint extends LitElement {
|
||||
|
||||
@state() private _params?;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _importing = false;
|
||||
|
||||
@state() private _saving = false;
|
||||
@@ -43,9 +47,14 @@ class DialogImportBlueprint extends LitElement {
|
||||
this._error = undefined;
|
||||
this._url = this._params.url;
|
||||
this.large = false;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._error = undefined;
|
||||
this._result = undefined;
|
||||
this._params = undefined;
|
||||
@@ -59,11 +68,16 @@ class DialogImportBlueprint extends LitElement {
|
||||
}
|
||||
const heading = this.hass.localize("ui.panel.config.blueprint.add.header");
|
||||
return html`
|
||||
<ha-dialog open .heading=${heading} @closed=${this.closeDialog}>
|
||||
<ha-dialog-header slot="heading">
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
width=${this.large ? "full" : "medium"}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-dialog-header slot="header">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
dialogAction="cancel"
|
||||
@click=${this.closeDialog}
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
@@ -104,6 +118,7 @@ class DialogImportBlueprint extends LitElement {
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.blueprint.add.file_name"
|
||||
)}
|
||||
autofocus
|
||||
></ha-textfield>
|
||||
`}
|
||||
<ha-expansion-panel
|
||||
@@ -157,59 +172,63 @@ class DialogImportBlueprint extends LitElement {
|
||||
"ui.panel.config.blueprint.add.url"
|
||||
)}
|
||||
.value=${this._url || ""}
|
||||
dialogInitialFocus
|
||||
autofocus
|
||||
></ha-textfield>
|
||||
`}
|
||||
</div>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
.disabled=${this._saving}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
${!this._result
|
||||
? html`
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._import}
|
||||
.disabled=${this._importing}
|
||||
.loading=${this._importing}
|
||||
.ariaLabel=${this.hass.localize(
|
||||
`ui.panel.config.blueprint.add.${this._importing ? "importing" : "import_btn"}`
|
||||
)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.blueprint.add.import_btn"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
: html`
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._save}
|
||||
.disabled=${this._saving || !!this._result.validation_errors}
|
||||
.loading=${this._saving}
|
||||
.ariaLabel=${this.hass.localize(
|
||||
`ui.panel.config.blueprint.add.${this._saving ? "saving" : this._result.exists ? "save_btn_override" : "save_btn"}`
|
||||
)}
|
||||
>
|
||||
${this._result.exists
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.blueprint.add.save_btn_override"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.blueprint.add.save_btn"
|
||||
)}
|
||||
</ha-button>
|
||||
`}
|
||||
</ha-dialog>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
.disabled=${this._saving}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
${!this._result
|
||||
? html`
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._import}
|
||||
.disabled=${this._importing}
|
||||
.loading=${this._importing}
|
||||
.ariaLabel=${this.hass.localize(
|
||||
`ui.panel.config.blueprint.add.${this._importing ? "importing" : "import_btn"}`
|
||||
)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.blueprint.add.import_btn"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
: html`
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._save}
|
||||
.disabled=${this._saving || !!this._result.validation_errors}
|
||||
.loading=${this._saving}
|
||||
.ariaLabel=${this.hass.localize(
|
||||
`ui.panel.config.blueprint.add.${this._saving ? "saving" : this._result.exists ? "save_btn_override" : "save_btn"}`
|
||||
)}
|
||||
>
|
||||
${this._result.exists
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.blueprint.add.save_btn_override"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.blueprint.add.save_btn"
|
||||
)}
|
||||
</ha-button>
|
||||
`}
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _enlarge() {
|
||||
this.large = !this.large;
|
||||
withViewTransition(() => {
|
||||
this.large = !this.large;
|
||||
});
|
||||
}
|
||||
|
||||
private async _import() {
|
||||
@@ -273,10 +292,6 @@ class DialogImportBlueprint extends LitElement {
|
||||
a ha-svg-icon {
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
:host([large]) ha-dialog {
|
||||
--mdc-dialog-min-width: 90vw;
|
||||
--mdc-dialog-max-width: 90vw;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--expansion-panel-content-padding: 0px;
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
|
||||
{
|
||||
id: ADD_NEW_ID + searchString,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.category-picker.add_new_sugestion",
|
||||
"ui.components.category-picker.add_new_suggestion",
|
||||
{
|
||||
name: searchString,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import "./ai-task-pref";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("ha-config-section-ai-tasks")
|
||||
class HaConfigSectionAITasks extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/system"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.ai_tasks.caption")}
|
||||
>
|
||||
<div class="content">
|
||||
<ai-task-pref
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ai-task-pref>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.content {
|
||||
padding: var(--ha-space-7) var(--ha-space-5) 0;
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
ai-task-pref {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-section-ai-tasks": HaConfigSectionAITasks;
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,7 @@ class HaConfigSectionAnalytics extends LitElement {
|
||||
display: block;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { UNIT_C } from "../../../common/const";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
@@ -17,8 +17,6 @@ import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-language-picker";
|
||||
import "../../../components/ha-radio";
|
||||
import type { HaRadio } from "../../../components/ha-radio";
|
||||
import "../../../components/ha-select";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../components/ha-textfield";
|
||||
import "../../../components/ha-timezone-picker";
|
||||
@@ -26,8 +24,7 @@ import type { ConfigUpdateValues } from "../../../data/core";
|
||||
import { saveCoreConfig } from "../../../data/core";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import "./ai-task-pref";
|
||||
import type { AITaskPref } from "./ai-task-pref";
|
||||
import "../../../components/map/ha-map";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
|
||||
@@ -37,7 +34,9 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _submitting = false;
|
||||
@state() private _submittingName = false;
|
||||
|
||||
@state() private _submittingRegional = false;
|
||||
|
||||
@state() private _unitSystem?: ConfigUpdateValues["unit_system"];
|
||||
|
||||
@@ -59,13 +58,10 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
|
||||
@state() private _updateUnits?: boolean;
|
||||
|
||||
@query("ai-task-pref") private _aiTaskPref!: AITaskPref;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const canEdit = ["storage", "default"].includes(
|
||||
this.hass.config.config_source
|
||||
);
|
||||
const disabled = this._submitting || !canEdit;
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/system"
|
||||
@@ -77,211 +73,269 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
${!canEdit
|
||||
? html`
|
||||
<ha-alert>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.edit_requires_storage"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
<ha-textfield
|
||||
name="name"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.location_name"
|
||||
)}
|
||||
.disabled=${disabled}
|
||||
.value=${this._name}
|
||||
@change=${this._handleChange}
|
||||
></ha-textfield>
|
||||
<ha-timezone-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.time_zone"
|
||||
)}
|
||||
name="timeZone"
|
||||
.disabled=${disabled}
|
||||
.value=${this._timeZone}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
hide-clear-icon
|
||||
></ha-timezone-picker>
|
||||
<ha-textfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation"
|
||||
)}
|
||||
name="elevation"
|
||||
type="number"
|
||||
.disabled=${disabled}
|
||||
.value=${this._elevation}
|
||||
.suffix=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation_meters"
|
||||
)}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
</ha-textfield>
|
||||
<div>
|
||||
<div>
|
||||
${!canEdit
|
||||
? html`
|
||||
<ha-alert>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system"
|
||||
"ui.panel.config.core.section.core.core_config.edit_requires_storage"
|
||||
)}
|
||||
</div>
|
||||
<ha-formfield
|
||||
.label=${html`
|
||||
<span style="font-size: 14px">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.metric_example"
|
||||
)}
|
||||
</span>
|
||||
<div style="color: var(--secondary-text-color)">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_metric"
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
>
|
||||
<ha-radio
|
||||
name="unit_system"
|
||||
value="metric"
|
||||
.checked=${this._unitSystem === "metric"}
|
||||
@change=${this._unitSystemChanged}
|
||||
.disabled=${disabled}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${html`
|
||||
<span style="font-size: 14px">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.us_customary_example"
|
||||
)}
|
||||
</span>
|
||||
<div style="color: var(--secondary-text-color)">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_us_customary"
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
>
|
||||
<ha-radio
|
||||
name="unit_system"
|
||||
value="us_customary"
|
||||
.checked=${this._unitSystem === "us_customary"}
|
||||
@change=${this._unitSystemChanged}
|
||||
.disabled=${disabled}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
${this._unitSystem !== this._configuredUnitSystem()
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.update_units_label"
|
||||
)}
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this._updateUnits}
|
||||
.disabled=${this._submitting}
|
||||
@change=${this._updateUnitsChanged}
|
||||
></ha-checkbox>
|
||||
</ha-formfield>
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.update_units_text_1"
|
||||
)}
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.update_units_text_2"
|
||||
)} <br /><br />
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.update_units_text_3"
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div>
|
||||
<ha-currency-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.currency"
|
||||
)}
|
||||
name="currency"
|
||||
.disabled=${disabled}
|
||||
.value=${this._currency}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
></ha-currency-picker>
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/ISO_4217#Active_codes"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="find-value"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.find_currency_value"
|
||||
)}</a
|
||||
>
|
||||
</div>
|
||||
<ha-country-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.country"
|
||||
)}
|
||||
name="country"
|
||||
.disabled=${disabled}
|
||||
.value=${this._country}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
></ha-country-picker>
|
||||
<ha-language-picker
|
||||
.hass=${this.hass}
|
||||
native-name
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.language"
|
||||
)}
|
||||
name="language"
|
||||
.value=${this._language}
|
||||
.disabled=${disabled}
|
||||
@closed=${stopPropagation}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
>
|
||||
</ha-language-picker>
|
||||
</div>
|
||||
|
||||
<ha-settings-row>
|
||||
<div slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.edit_location"
|
||||
)}
|
||||
</div>
|
||||
<div slot="description" class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.edit_location_description"
|
||||
)}
|
||||
</div>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="small"
|
||||
@click=${this._editLocation}
|
||||
.disabled=${disabled}
|
||||
>${this.hass.localize("ui.common.edit")}</ha-button
|
||||
>
|
||||
</ha-settings-row>
|
||||
<div class="card-actions">
|
||||
<ha-progress-button
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${disabled}
|
||||
>
|
||||
${this.hass!.localize("ui.common.save")}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ai-task-pref
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ai-task-pref>
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
${this._renderHomeNameCard(canEdit)}
|
||||
${this._renderLocationCard(canEdit)}
|
||||
${this._renderRegionalSettingsCard(canEdit)}
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderHomeNameCard(canEdit: boolean): TemplateResult {
|
||||
const disabled = this._submittingName || !canEdit;
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.home_name_card.header"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
<ha-textfield
|
||||
name="name"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.location_name"
|
||||
)}
|
||||
.disabled=${disabled}
|
||||
.value=${this._name}
|
||||
@change=${this._handleChange}
|
||||
></ha-textfield>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-progress-button
|
||||
appearance="filled"
|
||||
@click=${this._updateHomeName}
|
||||
.disabled=${disabled}
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderLocationCard(canEdit: boolean): TemplateResult {
|
||||
const hasHomeZone = "zone.home" in this.hass.states;
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.location_card.header"
|
||||
)}
|
||||
>
|
||||
${hasHomeZone
|
||||
? html`
|
||||
<div class="card-content">
|
||||
<ha-map
|
||||
.hass=${this.hass}
|
||||
.entities=${["zone.home"]}
|
||||
.zoom=${14}
|
||||
.autoFit=${true}
|
||||
.fitZones=${true}
|
||||
.themeMode=${"auto"}
|
||||
.renderPassive=${false}
|
||||
.interactiveZones=${false}
|
||||
class="map-preview"
|
||||
></ha-map>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div class="card-actions">
|
||||
<ha-button
|
||||
appearance="filled"
|
||||
@click=${this._editLocation}
|
||||
.disabled=${!canEdit}
|
||||
>
|
||||
${this.hass.localize("ui.common.edit")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderRegionalSettingsCard(canEdit: boolean): TemplateResult {
|
||||
const disabled = this._submittingRegional || !canEdit;
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.regional_settings_card.header"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
<ha-timezone-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.time_zone"
|
||||
)}
|
||||
name="timeZone"
|
||||
.disabled=${disabled}
|
||||
.value=${this._timeZone}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
hide-clear-icon
|
||||
></ha-timezone-picker>
|
||||
<ha-textfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation"
|
||||
)}
|
||||
name="elevation"
|
||||
type="number"
|
||||
.disabled=${disabled}
|
||||
.value=${this._elevation}
|
||||
.suffix=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.elevation_meters"
|
||||
)}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
</ha-textfield>
|
||||
<div>
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system"
|
||||
)}
|
||||
</div>
|
||||
<ha-formfield
|
||||
.label=${html`
|
||||
<span style="font-size: 14px">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.metric_example"
|
||||
)}
|
||||
</span>
|
||||
<div style="color: var(--secondary-text-color)">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_metric"
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
>
|
||||
<ha-radio
|
||||
name="unit_system"
|
||||
value="metric"
|
||||
.checked=${this._unitSystem === "metric"}
|
||||
@change=${this._unitSystemChanged}
|
||||
.disabled=${disabled}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${html`
|
||||
<span style="font-size: 14px">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.us_customary_example"
|
||||
)}
|
||||
</span>
|
||||
<div style="color: var(--secondary-text-color)">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.unit_system_us_customary"
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
>
|
||||
<ha-radio
|
||||
name="unit_system"
|
||||
value="us_customary"
|
||||
.checked=${this._unitSystem === "us_customary"}
|
||||
@change=${this._unitSystemChanged}
|
||||
.disabled=${disabled}
|
||||
></ha-radio>
|
||||
</ha-formfield>
|
||||
${this._unitSystem !== this._configuredUnitSystem()
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.update_units_label"
|
||||
)}
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this._updateUnits}
|
||||
.disabled=${this._submittingRegional}
|
||||
@change=${this._updateUnitsChanged}
|
||||
></ha-checkbox>
|
||||
</ha-formfield>
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.update_units_text_1"
|
||||
)}
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.update_units_text_2"
|
||||
)}
|
||||
<br /><br />
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.update_units_text_3"
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div>
|
||||
<ha-currency-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.currency"
|
||||
)}
|
||||
name="currency"
|
||||
.disabled=${disabled}
|
||||
.value=${this._currency}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
></ha-currency-picker>
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/ISO_4217#Active_codes"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="find-value"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.find_currency_value"
|
||||
)}</a
|
||||
>
|
||||
</div>
|
||||
<ha-country-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.country"
|
||||
)}
|
||||
name="country"
|
||||
.disabled=${disabled}
|
||||
.value=${this._country}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
></ha-country-picker>
|
||||
<ha-language-picker
|
||||
.hass=${this.hass}
|
||||
native-name
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.language"
|
||||
)}
|
||||
name="language"
|
||||
.value=${this._language}
|
||||
.disabled=${disabled}
|
||||
@closed=${stopPropagation}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
>
|
||||
</ha-language-picker>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-progress-button
|
||||
appearance="filled"
|
||||
@click=${this._updateRegionalSettings}
|
||||
.disabled=${disabled}
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _configuredUnitSystem() {
|
||||
return this.hass.config.unit_system.temperature === UNIT_C
|
||||
? "metric"
|
||||
@@ -297,12 +351,6 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
this._timeZone = this.hass.config.time_zone || "Etc/GMT";
|
||||
this._name = this.hass.config.location_name;
|
||||
this._updateUnits = true;
|
||||
|
||||
if (window.location.hash === "#ai-task") {
|
||||
this._aiTaskPref.updateComplete.then(() => {
|
||||
this._aiTaskPref.scrollIntoView();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _handleValueChanged(ev: ValueChangedEvent<string>) {
|
||||
@@ -325,7 +373,31 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
this._updateUnits = (ev.target as HaCheckbox).checked;
|
||||
}
|
||||
|
||||
private async _updateEntry(ev: CustomEvent) {
|
||||
private async _updateHomeName(ev: CustomEvent) {
|
||||
const button = ev.target as HaProgressButton;
|
||||
if (button.progress) {
|
||||
return;
|
||||
}
|
||||
|
||||
button.progress = true;
|
||||
this._submittingName = true;
|
||||
this._error = undefined;
|
||||
|
||||
try {
|
||||
await saveCoreConfig(this.hass, {
|
||||
location_name: this._name,
|
||||
});
|
||||
button.actionSuccess();
|
||||
} catch (err: any) {
|
||||
button.actionError();
|
||||
this._error = err.message;
|
||||
} finally {
|
||||
button.progress = false;
|
||||
this._submittingName = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _updateRegionalSettings(ev: CustomEvent) {
|
||||
const button = ev.target as HaProgressButton;
|
||||
if (button.progress) {
|
||||
return;
|
||||
@@ -350,6 +422,8 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
}
|
||||
}
|
||||
button.progress = true;
|
||||
this._submittingRegional = true;
|
||||
this._error = undefined;
|
||||
|
||||
let locationConfig;
|
||||
|
||||
@@ -364,14 +438,13 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
|
||||
try {
|
||||
await saveCoreConfig(this.hass, {
|
||||
currency: this._currency,
|
||||
time_zone: this._timeZone,
|
||||
elevation: Number(this._elevation),
|
||||
unit_system: this._unitSystem,
|
||||
update_units: this._updateUnits && unitSystemChanged,
|
||||
time_zone: this._timeZone,
|
||||
location_name: this._name,
|
||||
language: this._language,
|
||||
currency: this._currency,
|
||||
country: this._country,
|
||||
language: this._language,
|
||||
...locationConfig,
|
||||
});
|
||||
button.actionSuccess();
|
||||
@@ -380,6 +453,7 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
this._error = err.message;
|
||||
} finally {
|
||||
button.progress = false;
|
||||
this._submittingRegional = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,48 +465,39 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
haStyle,
|
||||
css`
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
padding: var(--ha-space-7) var(--ha-space-5) 0;
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-card,
|
||||
ai-task-pref {
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
}
|
||||
ha-card,
|
||||
ai-task-pref {
|
||||
margin-bottom: 24px;
|
||||
margin: 0 auto var(--ha-space-6);
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
padding: 16px 16px 0 16px;
|
||||
}
|
||||
.card-actions {
|
||||
text-align: right;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.card-content > * {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
}
|
||||
ha-select {
|
||||
display: block;
|
||||
.card-content > *:not(:first-child) {
|
||||
margin-top: var(--ha-space-4);
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
a.find-value {
|
||||
margin-top: 8px;
|
||||
margin-top: var(--ha-space-2);
|
||||
display: inline-block;
|
||||
}
|
||||
.map-preview {
|
||||
height: 200px;
|
||||
width: 100%;
|
||||
display: block;
|
||||
border-radius: var(--ha-card-border-radius, 8px);
|
||||
overflow: hidden;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -87,7 +87,9 @@ class HaConfigSystemNavigation extends LitElement {
|
||||
description = this._storageInfo
|
||||
? this.hass.localize("ui.panel.config.storage.description", {
|
||||
percent_used: `${Math.round(
|
||||
(this._storageInfo.used / this._storageInfo.total) * 100
|
||||
((this._storageInfo.total - this._storageInfo.free) /
|
||||
this._storageInfo.total) *
|
||||
100
|
||||
)}${blankBeforePercent(this.hass.locale)}%`,
|
||||
free_space: `${this._storageInfo.free} GB`,
|
||||
})
|
||||
|
||||
@@ -5,7 +5,8 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import { createCloseHeading } from "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-wa-dialog";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -21,14 +22,21 @@ export class DialogJoinBeta
|
||||
|
||||
@state() private _dialogParams?: JoinBetaDialogParams;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public showDialog(dialogParams: JoinBetaDialogParams): void {
|
||||
this._dialogParams = dialogParams;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
this._dialogParams = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
return true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -37,13 +45,11 @@ export class DialogJoinBeta
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.dialogs.join_beta_channel.title")
|
||||
)}
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this.hass.localize("ui.dialogs.join_beta_channel.title")}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-alert alert-type="warning">
|
||||
${this.hass.localize("ui.dialogs.join_beta_channel.backup")}
|
||||
@@ -67,17 +73,19 @@ export class DialogJoinBeta
|
||||
)}
|
||||
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</a>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="primaryAction"
|
||||
@click=${this._cancel}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._join}>
|
||||
${this.hass.localize("ui.dialogs.join_beta_channel.join")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this._cancel}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._join}>
|
||||
${this.hass.localize("ui.dialogs.join_beta_channel.join")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,11 @@ import type {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../../../common/translations/localize";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-wa-dialog";
|
||||
import "../../../components/search-input";
|
||||
import type { LovelaceConfig } from "../../../data/lovelace/config/types";
|
||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import { haStyle, haStyleDialog } from "../../../resources/styles";
|
||||
import { haStyleScrollbar } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { generateDefaultView } from "../../lovelace/views/default-view";
|
||||
import "./dashboard-card";
|
||||
@@ -65,7 +65,7 @@ const STRATEGIES = [
|
||||
class DialogNewDashboard extends LitElement implements HassDialog {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _opened = false;
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _params?: NewDashboardDialogParams;
|
||||
|
||||
@@ -77,7 +77,7 @@ class DialogNewDashboard extends LitElement implements HassDialog {
|
||||
})[] = [];
|
||||
|
||||
public showDialog(params: NewDashboardDialogParams): void {
|
||||
this._opened = true;
|
||||
this._open = true;
|
||||
this._params = params;
|
||||
this._localizedStrategies = STRATEGIES.map((strategy) => ({
|
||||
...strategy,
|
||||
@@ -89,14 +89,15 @@ class DialogNewDashboard extends LitElement implements HassDialog {
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
if (this._opened) {
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
this._opened = false;
|
||||
this._params = undefined;
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
private _generateDefaultConfig = memoizeOne(
|
||||
(localize: LocalizeFunc): LovelaceConfig => ({
|
||||
views: [generateDefaultView(localize, true)],
|
||||
@@ -104,98 +105,100 @@ class DialogNewDashboard extends LitElement implements HassDialog {
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this._opened) {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const defaultConfig = this._generateDefaultConfig(this.hass.localize);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
hideActions
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.dialog_new.header`
|
||||
)
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
flexcontent
|
||||
width="large"
|
||||
header-title=${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.dialog_new.header`
|
||||
)}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<search-input
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.dialog_new.search_dashboards`
|
||||
)}
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
></search-input>
|
||||
<div class="content">
|
||||
${this._filter
|
||||
? html`
|
||||
<div class="cards-container">
|
||||
${this._filterStrategies(
|
||||
this._localizedStrategies,
|
||||
this._filter
|
||||
).map(
|
||||
(strategy) => html`
|
||||
<dashboard-card
|
||||
.name=${strategy.localizedName}
|
||||
.description=${strategy.localizedDescription}
|
||||
.img=${this.hass.themes.darkMode
|
||||
? strategy.images.dark
|
||||
: strategy.images.light}
|
||||
.alt=${strategy.localizedName}
|
||||
@click=${this._selected}
|
||||
.strategy=${strategy.type}
|
||||
></dashboard-card>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="cards-container">
|
||||
<dashboard-card
|
||||
.name=${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.dialog_new.create_empty`
|
||||
)}
|
||||
.description=${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.dialog_new.create_empty_description`
|
||||
)}
|
||||
.img=${this.hass.themes.darkMode
|
||||
? "/static/images/dashboard-options/dark/icon-dashboard-new.svg"
|
||||
: "/static/images/dashboard-options/light/icon-dashboard-new.svg"}
|
||||
.alt=${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.dialog_new.create_empty`
|
||||
)}
|
||||
@click=${this._selected}
|
||||
.config=${defaultConfig}
|
||||
></dashboard-card>
|
||||
</div>
|
||||
<div class="cards-container">
|
||||
<div class="cards-container-header">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.dialog_new.heading.default`
|
||||
<div class="content-wrapper">
|
||||
<search-input
|
||||
autofocus
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.dialog_new.search_dashboards`
|
||||
)}
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
></search-input>
|
||||
<div class="content ha-scrollbar">
|
||||
${this._filter
|
||||
? html`
|
||||
<div class="cards-container">
|
||||
${this._filterStrategies(
|
||||
this._localizedStrategies,
|
||||
this._filter
|
||||
).map(
|
||||
(strategy) => html`
|
||||
<dashboard-card
|
||||
.name=${strategy.localizedName}
|
||||
.description=${strategy.localizedDescription}
|
||||
.img=${this.hass.themes.darkMode
|
||||
? strategy.images.dark
|
||||
: strategy.images.light}
|
||||
.alt=${strategy.localizedName}
|
||||
@click=${this._selected}
|
||||
.strategy=${strategy.type}
|
||||
></dashboard-card>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
${this._localizedStrategies.map(
|
||||
(strategy) => html`
|
||||
<dashboard-card
|
||||
.name=${strategy.localizedName}
|
||||
.description=${strategy.localizedDescription}
|
||||
.img=${this.hass.themes.darkMode
|
||||
? strategy.images.dark
|
||||
: strategy.images.light}
|
||||
.alt=${strategy.localizedName}
|
||||
@click=${this._selected}
|
||||
.strategy=${strategy.type}
|
||||
></dashboard-card>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
: html`
|
||||
<div class="cards-container">
|
||||
<dashboard-card
|
||||
.name=${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.dialog_new.create_empty`
|
||||
)}
|
||||
.description=${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.dialog_new.create_empty_description`
|
||||
)}
|
||||
.img=${this.hass.themes.darkMode
|
||||
? "/static/images/dashboard-options/dark/icon-dashboard-new.svg"
|
||||
: "/static/images/dashboard-options/light/icon-dashboard-new.svg"}
|
||||
.alt=${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.dialog_new.create_empty`
|
||||
)}
|
||||
@click=${this._selected}
|
||||
.config=${defaultConfig}
|
||||
></dashboard-card>
|
||||
</div>
|
||||
<div class="cards-container">
|
||||
<div class="cards-container-header">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.dialog_new.heading.default`
|
||||
)}
|
||||
</div>
|
||||
${this._localizedStrategies.map(
|
||||
(strategy) => html`
|
||||
<dashboard-card
|
||||
.name=${strategy.localizedName}
|
||||
.description=${strategy.localizedDescription}
|
||||
.img=${this.hass.themes.darkMode
|
||||
? strategy.images.dark
|
||||
: strategy.images.light}
|
||||
.alt=${strategy.localizedName}
|
||||
@click=${this._selected}
|
||||
.strategy=${strategy.type}
|
||||
></dashboard-card>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
</ha-dialog>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -253,33 +256,16 @@ class DialogNewDashboard extends LitElement implements HassDialog {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
/* overrule the ha-style-dialog max-height on small screens */
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 850px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 845px;
|
||||
--mdc-dialog-min-height: calc(
|
||||
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
|
||||
);
|
||||
--mdc-dialog-max-height: calc(
|
||||
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 845px;
|
||||
ha-wa-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
--dialog-z-index: 6;
|
||||
--ha-dialog-min-height: 60svh;
|
||||
}
|
||||
ha-wa-dialog::part(body) {
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
.cards-container-header {
|
||||
font-size: var(--ha-font-size-l);
|
||||
@@ -315,8 +301,17 @@ class DialogNewDashboard extends LitElement implements HassDialog {
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
margin-top: 20px;
|
||||
}
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.content {
|
||||
padding: 0 24px 0 24px;
|
||||
padding: 0 var(--ha-space-6) var(--ha-space-6) var(--ha-space-6);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -2,6 +2,7 @@ import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/entity/ha-entity-picker";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
@@ -22,8 +23,6 @@ import type { ExtEntityRegistryEntry } from "../../../../data/entity/entity_regi
|
||||
class HaPanelDevDebug extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _entityId?: string;
|
||||
|
||||
protected render() {
|
||||
@@ -34,14 +33,14 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.developer-tools.tabs.debug.title"
|
||||
)}
|
||||
>
|
||||
<ha-debug-connection-row
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-debug-connection-row>
|
||||
<ha-debug-disable-view-transition-row
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-debug-disable-view-transition-row>
|
||||
<ha-md-list>
|
||||
<ha-debug-connection-row
|
||||
.hass=${this.hass}
|
||||
></ha-debug-connection-row>
|
||||
<ha-debug-disable-view-transition-row
|
||||
.hass=${this.hass}
|
||||
></ha-debug-disable-view-transition-row>
|
||||
</ha-md-list>
|
||||
</ha-card>
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
@@ -128,6 +127,11 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-md-list {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
background: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../components/ha-settings-row";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-switch";
|
||||
import type { HaSwitch } from "../../../../components/ha-switch";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -11,26 +11,25 @@ import { storeState } from "../../../../util/ha-pref-storage";
|
||||
class HaDebugConnectionRow extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.debug.debug_connection.title"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass.localize(
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.debug.debug_connection.description"
|
||||
)}
|
||||
</span>
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.checked=${this.hass.debugConnection}
|
||||
@change=${this._checkedChanged}
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { setViewTransitionDisabled } from "../../../../common/util/view-transition";
|
||||
import "../../../../components/ha-settings-row";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-switch";
|
||||
import type { HaSwitch } from "../../../../components/ha-switch";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -12,29 +12,28 @@ import type { HomeAssistant } from "../../../../types";
|
||||
class HaDebugDisableViewTransitionRow extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@storage({ key: "disableViewTransition", state: true, subscribe: false })
|
||||
private _disabled = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.debug.disable_view_transition.title"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass.localize(
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.debug.disable_view_transition.description"
|
||||
)}
|
||||
</span>
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.checked=${this._disabled}
|
||||
@change=${this._checkedChanged}
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ import {
|
||||
mdiUnfoldMoreHorizontal,
|
||||
} from "@mdi/js";
|
||||
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, type CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
@@ -28,15 +28,11 @@ import type {
|
||||
SortingDirection,
|
||||
} from "../../../../components/data-table/ha-data-table";
|
||||
import { showDataTableSettingsDialog } from "../../../../components/data-table/show-dialog-data-table-settings";
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import type { HaMdMenu } from "../../../../components/ha-md-menu";
|
||||
import "../../../../components/ha-md-menu-item";
|
||||
import "../../../../components/ha-md-menu";
|
||||
import "../../../../components/ha-md-divider";
|
||||
import "../../../../components/search-input-outlined";
|
||||
import type {
|
||||
StatisticsMetaData,
|
||||
@@ -112,20 +108,8 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
@query("ha-data-table", true) private _dataTable!: HaDataTable;
|
||||
|
||||
@query("#group-by-menu") private _groupByMenu!: HaMdMenu;
|
||||
|
||||
@query("#sort-by-menu") private _sortByMenu!: HaMdMenu;
|
||||
|
||||
@query("search-input-outlined") private _searchInput!: HTMLElement;
|
||||
|
||||
private _toggleGroupBy() {
|
||||
this._groupByMenu.open = !this._groupByMenu.open;
|
||||
}
|
||||
|
||||
private _toggleSortBy() {
|
||||
this._sortByMenu.open = !this._sortByMenu.open;
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
this._validateStatistics();
|
||||
}
|
||||
@@ -278,37 +262,106 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
const sortByMenu = Object.values(columns).find((col) => col.sortable)
|
||||
? html`
|
||||
<ha-assist-chip
|
||||
.label=${localize("ui.components.subpage-data-table.sort_by", {
|
||||
sortColumn: this._sortColumn
|
||||
? ` ${columns[this._sortColumn]?.title || columns[this._sortColumn]?.label}` ||
|
||||
""
|
||||
: "",
|
||||
})}
|
||||
id="sort-by-anchor"
|
||||
@click=${this._toggleSortBy}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
<ha-dropdown @wa-select=${this._handleSortBy}>
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${localize("ui.components.subpage-data-table.sort_by", {
|
||||
sortColumn: this._sortColumn
|
||||
? ` ${columns[this._sortColumn]?.title || columns[this._sortColumn]?.label}` ||
|
||||
""
|
||||
: "",
|
||||
})}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${Object.entries(columns).map(([id, column]) =>
|
||||
column.sortable
|
||||
? html`
|
||||
<ha-dropdown-item
|
||||
.value=${id}
|
||||
.selected=${id === this._sortColumn}
|
||||
>
|
||||
${this._sortColumn === id
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="details"
|
||||
.path=${this._sortDirection === "desc"
|
||||
? mdiArrowDown
|
||||
: mdiArrowUp}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
${column.title || column.label}
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
</ha-dropdown>
|
||||
`
|
||||
: nothing;
|
||||
|
||||
const groupByMenu = Object.values(columns).find((col) => col.groupable)
|
||||
? html`
|
||||
<ha-assist-chip
|
||||
.label=${localize("ui.components.subpage-data-table.group_by", {
|
||||
groupColumn: this._groupColumn
|
||||
? ` ${columns[this._groupColumn].title || columns[this._groupColumn].label}`
|
||||
: "",
|
||||
})}
|
||||
id="group-by-anchor"
|
||||
@click=${this._toggleGroupBy}
|
||||
>
|
||||
<ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon
|
||||
></ha-assist-chip>
|
||||
<ha-dropdown @wa-select=${this._handleOverflowGroupBy}>
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${localize("ui.components.subpage-data-table.group_by", {
|
||||
groupColumn: this._groupColumn
|
||||
? ` ${columns[this._groupColumn].title || columns[this._groupColumn].label}`
|
||||
: "",
|
||||
})}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon
|
||||
></ha-assist-chip>
|
||||
${Object.entries(columns).map(([id, column]) =>
|
||||
column.groupable
|
||||
? html`
|
||||
<ha-dropdown-item
|
||||
.value=${id}
|
||||
.selected=${id === this._groupColumn}
|
||||
>
|
||||
${column.title || column.label}
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
<ha-dropdown-item
|
||||
value="none"
|
||||
.selected=${this._groupColumn === undefined}
|
||||
>
|
||||
${localize("ui.components.subpage-data-table.dont_group_by")}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider></wa-divider>
|
||||
<ha-dropdown-item
|
||||
value="collapse_all"
|
||||
@click=${this._collapseAllGroups}
|
||||
.disabled=${this._groupColumn === undefined}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiUnfoldLessHorizontal}
|
||||
></ha-svg-icon>
|
||||
${localize(
|
||||
"ui.components.subpage-data-table.collapse_all_groups"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item
|
||||
@click=${this._expandAllGroups}
|
||||
.disabled=${this._groupColumn === undefined}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiUnfoldMoreHorizontal}
|
||||
></ha-svg-icon>
|
||||
${localize("ui.components.subpage-data-table.expand_all_groups")}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
`
|
||||
: nothing;
|
||||
|
||||
@@ -417,6 +470,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
.hiddenColumns=${this.hiddenColumns}
|
||||
@row-click=${this._rowClicked}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
@sorting-changed=${this._handleTableSortingChanged}
|
||||
>
|
||||
${!this.narrow
|
||||
? html`
|
||||
@@ -434,82 +488,6 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
</div>`}
|
||||
</ha-data-table>
|
||||
</div>
|
||||
<ha-md-menu
|
||||
anchor="group-by-anchor"
|
||||
id="group-by-menu"
|
||||
positioning="fixed"
|
||||
>
|
||||
${Object.entries(columns).map(([id, column]) =>
|
||||
column.groupable
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
.value=${id}
|
||||
@click=${this._handleGroupBy}
|
||||
.selected=${id === this._groupColumn}
|
||||
class=${classMap({ selected: id === this._groupColumn })}
|
||||
>
|
||||
${column.title || column.label}
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
<ha-md-menu-item
|
||||
.value=${undefined}
|
||||
@click=${this._handleGroupBy}
|
||||
.selected=${this._groupColumn === undefined}
|
||||
class=${classMap({ selected: this._groupColumn === undefined })}
|
||||
>
|
||||
${localize("ui.components.subpage-data-table.dont_group_by")}
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item
|
||||
@click=${this._collapseAllGroups}
|
||||
.disabled=${this._groupColumn === undefined}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiUnfoldLessHorizontal}
|
||||
></ha-svg-icon>
|
||||
${localize("ui.components.subpage-data-table.collapse_all_groups")}
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
@click=${this._expandAllGroups}
|
||||
.disabled=${this._groupColumn === undefined}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiUnfoldMoreHorizontal}
|
||||
></ha-svg-icon>
|
||||
${localize("ui.components.subpage-data-table.expand_all_groups")}
|
||||
</ha-md-menu-item>
|
||||
</ha-md-menu>
|
||||
<ha-md-menu anchor="sort-by-anchor" id="sort-by-menu" positioning="fixed">
|
||||
${Object.entries(columns).map(([id, column]) =>
|
||||
column.sortable
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
.value=${id}
|
||||
@click=${this._handleSortBy}
|
||||
keep-open
|
||||
.selected=${id === this._sortColumn}
|
||||
class=${classMap({ selected: id === this._sortColumn })}
|
||||
>
|
||||
${this._sortColumn === id
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${this._sortDirection === "desc"
|
||||
? mdiArrowDown
|
||||
: mdiArrowUp}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
${column.title || column.label}
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
</ha-md-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -526,8 +504,17 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
this._selected = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleSortBy(ev) {
|
||||
const columnId = ev.currentTarget.value;
|
||||
private _handleTableSortingChanged(
|
||||
ev: CustomEvent<{ column: string; direction: SortingDirection }>
|
||||
) {
|
||||
const { column, direction } = ev.detail;
|
||||
this._sortColumn = column;
|
||||
this._sortDirection = direction;
|
||||
}
|
||||
|
||||
private _handleSortBy(ev: HaDropdownSelectEvent) {
|
||||
ev.preventDefault(); // keep dropdown open
|
||||
const columnId = ev.detail.item.value;
|
||||
if (!this._sortDirection || this._sortColumn !== columnId) {
|
||||
this._sortDirection = "asc";
|
||||
} else if (this._sortDirection === "asc") {
|
||||
@@ -538,11 +525,29 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
this._sortColumn = columnId;
|
||||
}
|
||||
|
||||
private _handleGroupBy(ev) {
|
||||
this._setGroupColumn(ev.currentTarget.value);
|
||||
}
|
||||
private _handleOverflowGroupBy = (ev: HaDropdownSelectEvent) => {
|
||||
const action = ev.detail.item.value;
|
||||
|
||||
private _setGroupColumn(columnId: string) {
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case "collapse_all":
|
||||
this._collapseAllGroups();
|
||||
return;
|
||||
case "expand_all":
|
||||
this._expandAllGroups();
|
||||
return;
|
||||
case "none":
|
||||
this._setGroupColumn();
|
||||
return;
|
||||
default:
|
||||
this._setGroupColumn(action);
|
||||
}
|
||||
};
|
||||
|
||||
private _setGroupColumn(columnId?: string) {
|
||||
this._groupColumn = columnId;
|
||||
}
|
||||
|
||||
@@ -797,8 +802,6 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
#sort-by-anchor,
|
||||
#group-by-anchor,
|
||||
ha-dropdown ha-assist-chip {
|
||||
--md-assist-chip-trailing-space: 8px;
|
||||
}
|
||||
|
||||
@@ -30,12 +30,12 @@ import "../../../components/ha-labels-picker";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-radio";
|
||||
import "../../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../../components/ha-select";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-state-icon";
|
||||
import "../../../components/ha-switch";
|
||||
import type { HaSwitch } from "../../../components/ha-switch";
|
||||
import "../../../components/ha-textfield";
|
||||
import type { HaSelectSelectEvent } from "../../../components/ha-select";
|
||||
import {
|
||||
CAMERA_ORIENTATIONS,
|
||||
CAMERA_SUPPORT_STREAM,
|
||||
@@ -434,19 +434,15 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
>
|
||||
<ha-dropdown-item
|
||||
value="switch"
|
||||
class=${this._switchAsDomain === "switch" &&
|
||||
(!this._deviceClass || this._deviceClass === "switch")
|
||||
? "selected"
|
||||
: ""}
|
||||
.selected=${this._switchAsDomain === "switch" &&
|
||||
(!this._deviceClass || this._deviceClass === "switch")}
|
||||
>
|
||||
${domainToName(this.hass.localize, "switch")}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item
|
||||
value="outlet"
|
||||
class=${this._switchAsDomain === "switch" &&
|
||||
this._deviceClass === "outlet"
|
||||
? "selected"
|
||||
: ""}
|
||||
.selected=${this._switchAsDomain === "switch" &&
|
||||
this._deviceClass === "outlet"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.device_classes.switch.outlet"
|
||||
@@ -460,9 +456,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
(entry) => html`
|
||||
<ha-dropdown-item
|
||||
.value=${entry.domain}
|
||||
class=${this._switchAsDomain === entry.domain
|
||||
? "selected"
|
||||
: ""}
|
||||
.selected=${this._switchAsDomain === entry.domain}
|
||||
>
|
||||
${entry.label}
|
||||
</ha-dropdown-item>
|
||||
@@ -479,13 +473,13 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
>
|
||||
<ha-dropdown-item
|
||||
value="switch"
|
||||
class=${this._switchAsDomain === "switch" ? "selected" : ""}
|
||||
.selected=${this._switchAsDomain === "switch"}
|
||||
>
|
||||
${domainToName(this.hass.localize, "switch")}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item
|
||||
.value=${domain}
|
||||
class=${this._switchAsDomain === domain ? "selected" : ""}
|
||||
.selected=${this._switchAsDomain === domain}
|
||||
>
|
||||
${domainToName(this.hass.localize, domain)}
|
||||
</ha-dropdown-item>
|
||||
@@ -499,9 +493,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
: html`
|
||||
<ha-dropdown-item
|
||||
.value=${entry.domain}
|
||||
class=${this._switchAsDomain === entry.domain
|
||||
? "selected"
|
||||
: ""}
|
||||
.selected=${this._switchAsDomain === entry.domain}
|
||||
>
|
||||
${entry.label}
|
||||
</ha-dropdown-item>
|
||||
@@ -551,9 +543,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
(entry) => html`
|
||||
<ha-dropdown-item
|
||||
.value=${entry.deviceClass}
|
||||
class=${entry.deviceClass === this._deviceClass
|
||||
? "selected"
|
||||
: ""}
|
||||
.selected=${entry.deviceClass === this._deviceClass}
|
||||
>
|
||||
${entry.label}
|
||||
</ha-dropdown-item>
|
||||
@@ -571,9 +561,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
(entry) => html`
|
||||
<ha-dropdown-item
|
||||
.value=${entry.deviceClass}
|
||||
class=${entry.deviceClass === this._deviceClass
|
||||
? "selected"
|
||||
: ""}
|
||||
.selected=${entry.deviceClass === this._deviceClass}
|
||||
>
|
||||
${entry.label}
|
||||
</ha-dropdown-item>
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
mdiScriptText,
|
||||
mdiShape,
|
||||
mdiSofa,
|
||||
mdiStarFourPoints,
|
||||
mdiTextBoxOutline,
|
||||
mdiTools,
|
||||
mdiUpdate,
|
||||
@@ -118,6 +119,7 @@ export const configSections: Record<string, PageNavigation[]> = {
|
||||
path: "/config/matter",
|
||||
iconPath:
|
||||
"M7.228375 6.41685c0.98855 0.80195 2.16365 1.3412 3.416275 1.56765V1.30093l1.3612 -0.7854275 1.360125 0.7854275V7.9845c1.252875 -0.226675 2.4283 -0.765875 3.41735 -1.56765l2.471225 1.4293c-4.019075 3.976275 -10.490025 3.976275 -14.5091 0l2.482925 -1.4293Zm3.00335 17.067575c1.43325 -5.47035 -1.8052 -11.074775 -7.2604 -12.564675v2.859675c1.189125 0.455 2.244125 1.202875 3.0672 2.174275L0.25 19.2955v1.5719l1.3611925 0.781175L7.39865 18.3068c0.430175 1.19825 0.550625 2.48575 0.35015 3.743l2.482925 1.434625ZM21.034 10.91975c-5.452225 1.4932 -8.6871 7.09635 -7.254025 12.564675l2.47655 -1.43035c-0.200025 -1.257275 -0.079575 -2.544675 0.35015 -3.743025l5.7832 3.337525L23.75 20.86315V19.2955L17.961475 15.9537c0.8233 -0.97115 1.878225 -1.718975 3.0672 -2.174275l0.005325 -2.859675Z",
|
||||
iconViewBox: "0 1 24 24",
|
||||
iconColor: "#2458B3",
|
||||
component: "matter",
|
||||
translationKey: "matter",
|
||||
@@ -398,6 +400,13 @@ export const configSections: Record<string, PageNavigation[]> = {
|
||||
iconPath: mdiShape,
|
||||
iconColor: "#f1c447",
|
||||
},
|
||||
{
|
||||
path: "/config/ai-tasks",
|
||||
translationKey: "ai_tasks",
|
||||
iconPath: mdiStarFourPoints,
|
||||
iconColor: "#8B69E3",
|
||||
core: true,
|
||||
},
|
||||
{
|
||||
path: "/config/labs",
|
||||
translationKey: "labs",
|
||||
@@ -601,6 +610,10 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
|
||||
tag: "ha-config-labs",
|
||||
load: () => import("./labs/ha-config-labs"),
|
||||
},
|
||||
"ai-tasks": {
|
||||
tag: "ha-config-section-ai-tasks",
|
||||
load: () => import("./core/ha-config-section-ai-tasks"),
|
||||
},
|
||||
zha: {
|
||||
tag: "zha-config-dashboard-router",
|
||||
load: () =>
|
||||
|
||||
@@ -12,10 +12,11 @@ import "../../../components/chart/ha-chart-base";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-fade-in";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-spinner";
|
||||
import type { ConfigEntry } from "../../../data/config_entries";
|
||||
import { subscribeConfigEntries } from "../../../data/config_entries";
|
||||
import type {
|
||||
@@ -365,7 +366,7 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
|
||||
})}
|
||||
</ha-card>`
|
||||
: nothing}
|
||||
${this._systemStatusData
|
||||
${isComponentLoaded(this.hass, "hardware")
|
||||
? html`<ha-card outlined>
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
@@ -374,16 +375,25 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
|
||||
)}
|
||||
</div>
|
||||
<div class="value">
|
||||
${this._systemStatusData.cpu_percent ||
|
||||
"-"}${blankBeforePercent(this.hass.locale)}%
|
||||
${this._systemStatusData
|
||||
? html`${this._systemStatusData
|
||||
.cpu_percent}${blankBeforePercent(
|
||||
this.hass.locale
|
||||
)}%`
|
||||
: "-"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="card-content loading-container">
|
||||
<ha-chart-base
|
||||
.hass=${this.hass}
|
||||
.data=${this._getChartData(this._cpuEntries)}
|
||||
.options=${this._chartOptions}
|
||||
></ha-chart-base>
|
||||
${!this._systemStatusData
|
||||
? html` <ha-fade-in delay="1000" class="loading-overlay">
|
||||
<ha-spinner size="large"></ha-spinner>
|
||||
</ha-fade-in>`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card outlined>
|
||||
@@ -392,37 +402,38 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
|
||||
${this.hass.localize("ui.panel.config.hardware.memory")}
|
||||
</div>
|
||||
<div class="value">
|
||||
${round(this._systemStatusData.memory_used_mb / 1024, 1)}
|
||||
GB /
|
||||
${round(
|
||||
(this._systemStatusData.memory_used_mb! +
|
||||
this._systemStatusData.memory_free_mb!) /
|
||||
1024,
|
||||
0
|
||||
)}
|
||||
GB
|
||||
${this._systemStatusData
|
||||
? html`${round(
|
||||
this._systemStatusData.memory_used_mb / 1024,
|
||||
1
|
||||
)}
|
||||
GB /
|
||||
${round(
|
||||
(this._systemStatusData.memory_used_mb +
|
||||
this._systemStatusData.memory_free_mb) /
|
||||
1024,
|
||||
0
|
||||
)}
|
||||
GB`
|
||||
: "-"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="card-content loading-container">
|
||||
<ha-chart-base
|
||||
.hass=${this.hass}
|
||||
.data=${this._getChartData(this._memoryEntries)}
|
||||
.options=${this._chartOptions}
|
||||
></ha-chart-base>
|
||||
${!this._systemStatusData
|
||||
? html`
|
||||
<ha-fade-in delay="1000" class="loading-overlay">
|
||||
<ha-spinner size="large"></ha-spinner>
|
||||
</ha-fade-in>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-card>`
|
||||
: isComponentLoaded(this.hass, "hardware")
|
||||
? html`<ha-card outlined>
|
||||
<div class="card-content">
|
||||
<ha-alert alert-type="info">
|
||||
<ha-spinner slot="icon"></ha-spinner>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.hardware.loading_system_data"
|
||||
)}
|
||||
</ha-alert>
|
||||
</div>
|
||||
</ha-card>`
|
||||
: nothing}
|
||||
: nothing}
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
@@ -502,6 +513,22 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: rgba(var(--rgb-card-background-color), 0.75);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.card-content img {
|
||||
max-width: 300px;
|
||||
margin: auto;
|
||||
@@ -548,10 +575,6 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
|
||||
ha-alert {
|
||||
--ha-alert-icon-size: 24px;
|
||||
}
|
||||
|
||||
ha-alert ha-spinner {
|
||||
--ha-spinner-size: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import { mdiAlertOutline } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { stringCompare } from "../../../common/string/compare";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import "../../../components/ha-wa-dialog";
|
||||
import "../../../components/search-input";
|
||||
import { getConfigFlowHandlers } from "../../../data/config_flow";
|
||||
import { createCounter } from "../../../data/counter";
|
||||
import { createInputBoolean } from "../../../data/input_boolean";
|
||||
@@ -27,6 +28,7 @@ import { createInputText } from "../../../data/input_text";
|
||||
import {
|
||||
domainToName,
|
||||
fetchIntegrationManifest,
|
||||
type IntegrationManifest,
|
||||
} from "../../../data/integration";
|
||||
import { createSchedule } from "../../../data/schedule";
|
||||
import { createTimer } from "../../../data/timer";
|
||||
@@ -103,7 +105,7 @@ export class DialogHelperDetail extends LitElement {
|
||||
|
||||
@state() private _item?: Helper;
|
||||
|
||||
@state() private _opened = false;
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _domain?: string;
|
||||
|
||||
@@ -111,14 +113,18 @@ export class DialogHelperDetail extends LitElement {
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
@query(".form") private _form?: HTMLDivElement;
|
||||
|
||||
@state() private _helperFlows?: string[];
|
||||
|
||||
@state() private _loading = false;
|
||||
|
||||
@state() private _filter?: string;
|
||||
|
||||
private _pendingConfigFlow?: {
|
||||
startFlowHandler: string;
|
||||
manifest: IntegrationManifest;
|
||||
dialogClosedCallback?: ShowDialogHelperDetailParams["dialogClosedCallback"];
|
||||
};
|
||||
|
||||
private _params?: ShowDialogHelperDetailParams;
|
||||
|
||||
public async showDialog(params: ShowDialogHelperDetailParams): Promise<void> {
|
||||
@@ -128,29 +134,47 @@ export class DialogHelperDetail extends LitElement {
|
||||
if (this._domain && this._domain in HELPERS) {
|
||||
await HELPERS[this._domain].import();
|
||||
}
|
||||
this._opened = true;
|
||||
this._open = true;
|
||||
await this.updateComplete;
|
||||
this.hass.loadFragmentTranslation("config");
|
||||
const flows = await getConfigFlowHandlers(this.hass, ["helper"]);
|
||||
await this.hass.loadBackendTranslation("title", flows, true);
|
||||
// Ensure the titles are loaded before we render the flows.
|
||||
this._helperFlows = flows;
|
||||
|
||||
await this.updateComplete;
|
||||
await this._focusSearchInput();
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._opened = false;
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._open = false;
|
||||
this._error = undefined;
|
||||
this._domain = undefined;
|
||||
this._params = undefined;
|
||||
this._filter = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
|
||||
if (this._pendingConfigFlow) {
|
||||
const pendingConfigFlow = this._pendingConfigFlow;
|
||||
this._pendingConfigFlow = undefined;
|
||||
showConfigFlowDialog(this, {
|
||||
startFlowHandler: pendingConfigFlow.startFlowHandler,
|
||||
manifest: pendingConfigFlow.manifest,
|
||||
dialogClosedCallback: pendingConfigFlow.dialogClosedCallback,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._opened) {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
let content: TemplateResult;
|
||||
let footer: TemplateResult | typeof nothing = nothing;
|
||||
|
||||
if (this._domain) {
|
||||
content = html`
|
||||
@@ -160,25 +184,30 @@ export class DialogHelperDetail extends LitElement {
|
||||
hass: this.hass,
|
||||
item: this._item,
|
||||
new: true,
|
||||
autofocus: true,
|
||||
})}
|
||||
</div>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._createItem}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass!.localize("ui.panel.config.helpers.dialog.create")}
|
||||
</ha-button>
|
||||
${this._params?.domain
|
||||
? nothing
|
||||
: html`<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this._goBack}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass!.localize("ui.common.back")}
|
||||
</ha-button>`}
|
||||
`;
|
||||
footer = html`
|
||||
<ha-dialog-footer slot="footer">
|
||||
${this._params?.domain
|
||||
? nothing
|
||||
: html`<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this._goBack}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass!.localize("ui.common.back")}
|
||||
</ha-button>`}
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._createItem}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass!.localize("ui.panel.config.helpers.dialog.create")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
`;
|
||||
} else if (this._loading || this._helperFlows === undefined) {
|
||||
content = html`<ha-spinner></ha-spinner>`;
|
||||
@@ -191,8 +220,8 @@ export class DialogHelperDetail extends LitElement {
|
||||
|
||||
content = html`
|
||||
<search-input
|
||||
autofocus
|
||||
.hass=${this.hass}
|
||||
dialogInitialFocus="true"
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._filterChanged}
|
||||
.label=${this.hass.localize(
|
||||
@@ -207,7 +236,6 @@ export class DialogHelperDetail extends LitElement {
|
||||
"ui.panel.config.helpers.dialog.create_helper"
|
||||
)}
|
||||
rootTabbable
|
||||
dialogInitialFocus
|
||||
>
|
||||
${items.map(([domain, label]) => {
|
||||
// Only OG helpers need to be loaded prior adding one
|
||||
@@ -255,32 +283,28 @@ export class DialogHelperDetail extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
class=${classMap({ "button-left": !this._domain })}
|
||||
.hideActions=${!this._domain}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this._domain
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.helpers.dialog.create_platform",
|
||||
{
|
||||
platform:
|
||||
(isHelperDomain(this._domain) &&
|
||||
this.hass.localize(
|
||||
`ui.panel.config.helpers.types.${
|
||||
this._domain as HelperDomain
|
||||
}`
|
||||
)) ||
|
||||
this._domain,
|
||||
}
|
||||
)
|
||||
: this.hass.localize("ui.panel.config.helpers.dialog.create_helper")
|
||||
)}
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this._domain
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.helpers.dialog.create_platform",
|
||||
{
|
||||
platform:
|
||||
(isHelperDomain(this._domain) &&
|
||||
this.hass.localize(
|
||||
`ui.panel.config.helpers.types.${
|
||||
this._domain as HelperDomain
|
||||
}`
|
||||
)) ||
|
||||
this._domain,
|
||||
}
|
||||
)
|
||||
: this.hass.localize("ui.panel.config.helpers.dialog.create_helper")}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${content}
|
||||
</ha-dialog>
|
||||
${content} ${footer}
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -381,26 +405,35 @@ export class DialogHelperDetail extends LitElement {
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
this._focusForm();
|
||||
} else {
|
||||
showConfigFlowDialog(this, {
|
||||
this._pendingConfigFlow = {
|
||||
startFlowHandler: domain,
|
||||
manifest: await fetchIntegrationManifest(this.hass, domain),
|
||||
dialogClosedCallback: this._params!.dialogClosedCallback,
|
||||
});
|
||||
dialogClosedCallback: this._params?.dialogClosedCallback,
|
||||
};
|
||||
this.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
private async _focusForm(): Promise<void> {
|
||||
await this.updateComplete;
|
||||
(this._form?.lastElementChild as HTMLElement).focus();
|
||||
}
|
||||
|
||||
private _goBack() {
|
||||
private async _goBack() {
|
||||
this._domain = undefined;
|
||||
this._item = undefined;
|
||||
this._error = undefined;
|
||||
await this.updateComplete;
|
||||
await this._focusSearchInput();
|
||||
}
|
||||
|
||||
private async _focusSearchInput() {
|
||||
const searchInput = this.shadowRoot?.querySelector("search-input") as
|
||||
| (HTMLElement & { updateComplete?: Promise<unknown> })
|
||||
| null;
|
||||
|
||||
if (!searchInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
await searchInput.updateComplete;
|
||||
searchInput.focus();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -408,31 +441,21 @@ export class DialogHelperDetail extends LitElement {
|
||||
haStyleScrollbar,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog.button-left {
|
||||
--justify-action-buttons: flex-start;
|
||||
}
|
||||
ha-dialog {
|
||||
ha-wa-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
--dialog-scroll-divider-color: transparent;
|
||||
--mdc-dialog-max-height: 90vh;
|
||||
}
|
||||
@media all and (min-width: 550px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 500px;
|
||||
}
|
||||
}
|
||||
ha-icon-next {
|
||||
width: 24px;
|
||||
width: var(--ha-space-6);
|
||||
}
|
||||
ha-tooltip {
|
||||
pointer-events: auto;
|
||||
}
|
||||
.form {
|
||||
padding: 24px;
|
||||
padding: var(--ha-space-6);
|
||||
}
|
||||
search-input {
|
||||
display: block;
|
||||
margin: 16px 16px 0;
|
||||
margin: 0 var(--ha-space-4) 0;
|
||||
}
|
||||
ha-list {
|
||||
height: calc(60vh - 184px);
|
||||
|
||||
@@ -3,7 +3,8 @@ import { html, LitElement, nothing } from "lit";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { createCloseHeading } from "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-wa-dialog";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import "../../../../components/ha-button";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
@@ -24,6 +25,8 @@ class DialogScheduleBlockInfo extends LitElement {
|
||||
|
||||
@state() private _params?: ScheduleBlockInfoDialogParams;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
private _expand = false;
|
||||
|
||||
private _schema = memoizeOne((expand: boolean) => [
|
||||
@@ -57,9 +60,14 @@ class DialogScheduleBlockInfo extends LitElement {
|
||||
this._error = undefined;
|
||||
this._data = params.block;
|
||||
this._expand = !!params.block?.data;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
this._data = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
@@ -71,18 +79,17 @@ class DialogScheduleBlockInfo extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass!.localize(
|
||||
"ui.dialogs.helper_settings.schedule.edit_schedule_block"
|
||||
)
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this.hass!.localize(
|
||||
"ui.dialogs.helper_settings.schedule.edit_schedule_block"
|
||||
)}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div>
|
||||
<ha-form
|
||||
autofocus
|
||||
.hass=${this.hass}
|
||||
.schema=${this._schema(this._expand)}
|
||||
.data=${this._data}
|
||||
@@ -91,18 +98,20 @@ class DialogScheduleBlockInfo extends LitElement {
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
</div>
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._deleteBlock}
|
||||
appearance="plain"
|
||||
variant="danger"
|
||||
>
|
||||
${this.hass!.localize("ui.common.delete")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._updateBlock}>
|
||||
${this.hass!.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._deleteBlock}
|
||||
appearance="filled"
|
||||
variant="danger"
|
||||
>
|
||||
${this.hass!.localize("ui.common.delete")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._updateBlock}>
|
||||
${this.hass!.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -369,7 +369,6 @@ export class BluetoothConfigDashboard extends LitElement {
|
||||
padding: 24px 0 32px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
direction: ltr;
|
||||
}
|
||||
ha-card {
|
||||
margin-bottom: 16px;
|
||||
|
||||
@@ -323,9 +323,9 @@ class ZHAConfigDashboard extends LitElement {
|
||||
const configEntries = await getConfigEntries(this.hass, {
|
||||
domain: "zha",
|
||||
});
|
||||
if (configEntries.length) {
|
||||
this._configEntry = configEntries[0];
|
||||
}
|
||||
this._configEntry = configEntries.find(
|
||||
(entry) => entry.disabled_by === null && entry.source !== "ignore"
|
||||
);
|
||||
}
|
||||
|
||||
private async _fetchConfiguration(): Promise<void> {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import {
|
||||
mdiDelete,
|
||||
mdiDevices,
|
||||
@@ -20,13 +21,16 @@ import type {
|
||||
RowClickedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/ha-dropdown";
|
||||
import type {
|
||||
HaDropdown,
|
||||
HaDropdownSelectEvent,
|
||||
} from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import { renderLabelColorBadge } from "../../../components/ha-label-picker";
|
||||
import "../../../components/ha-md-menu";
|
||||
import type { HaMdMenu } from "../../../components/ha-md-menu";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type {
|
||||
LabelRegistryEntry,
|
||||
@@ -44,12 +48,12 @@ import {
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showLabelDetailDialog } from "./show-dialog-label-detail";
|
||||
import {
|
||||
getCreatedAtTableColumn,
|
||||
getModifiedAtTableColumn,
|
||||
} from "../common/data-table-columns";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showLabelDetailDialog } from "./show-dialog-label-detail";
|
||||
|
||||
@customElement("ha-config-labels")
|
||||
export class HaConfigLabels extends LitElement {
|
||||
@@ -93,10 +97,12 @@ export class HaConfigLabels extends LitElement {
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
@query("#overflow-menu") private _overflowMenu?: HaMdMenu;
|
||||
@query("#overflow-menu") private _overflowMenu?: HaDropdown;
|
||||
|
||||
private _overflowLabel!: LabelRegistryEntry;
|
||||
|
||||
private _openingOverflow = false;
|
||||
|
||||
private _columns = memoizeOne((localize: LocalizeFunc, narrow: boolean) => {
|
||||
const columns: DataTableColumnContainer<LabelRegistryEntry> = {
|
||||
icon: {
|
||||
@@ -172,13 +178,27 @@ export class HaConfigLabels extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._overflowMenu.open) {
|
||||
this._overflowMenu.close();
|
||||
if (this._overflowMenu.anchorElement === ev.target) {
|
||||
this._overflowMenu.anchorElement = undefined;
|
||||
return;
|
||||
}
|
||||
this._overflowLabel = ev.target.selected;
|
||||
this._openingOverflow = true;
|
||||
this._overflowMenu.anchorElement = ev.target;
|
||||
this._overflowMenu.show();
|
||||
this._overflowLabel = ev.target.selected;
|
||||
this._overflowMenu.open = true;
|
||||
};
|
||||
|
||||
private _overflowMenuOpened = () => {
|
||||
this._openingOverflow = false;
|
||||
};
|
||||
|
||||
private _overflowMenuClosed = () => {
|
||||
// changing the anchorElement triggers a close event, ignore it
|
||||
if (this._openingOverflow || !this._overflowMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._overflowMenu.anchorElement = undefined;
|
||||
};
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
@@ -224,32 +244,30 @@ export class HaConfigLabels extends LitElement {
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
</hass-tabs-subpage-data-table>
|
||||
<ha-md-menu id="overflow-menu" positioning="fixed">
|
||||
<ha-md-menu-item .clickAction=${this._navigateEntities}>
|
||||
<ha-svg-icon slot="start" .path=${mdiShape}></ha-svg-icon>
|
||||
<ha-dropdown
|
||||
id="overflow-menu"
|
||||
@wa-select=${this._handleOverflowAction}
|
||||
@wa-after-show=${this._overflowMenuOpened}
|
||||
@wa-after-hide=${this._overflowMenuClosed}
|
||||
>
|
||||
<ha-dropdown-item value="navigate-entities">
|
||||
<ha-svg-icon slot="icon" .path=${mdiShape}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.entities.caption")}
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._navigateDevices}>
|
||||
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="navigate-devices">
|
||||
<ha-svg-icon slot="icon" .path=${mdiDevices}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.devices.caption")}
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item .clickAction=${this._navigateAutomations}>
|
||||
<ha-svg-icon slot="start" .path=${mdiRobot}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="navigate-automations">
|
||||
<ha-svg-icon slot="icon" .path=${mdiRobot}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.automation.caption")}
|
||||
</ha-md-menu-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
<ha-md-menu-item
|
||||
class="warning"
|
||||
.clickAction=${this._handleRemoveLabelClick}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
class="warning"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
<wa-divider></wa-divider>
|
||||
<ha-dropdown-item variant="danger" value="remove">
|
||||
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
|
||||
${this.hass.localize("ui.common.delete")}
|
||||
</ha-md-menu-item>
|
||||
</ha-md-menu>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -341,6 +359,28 @@ export class HaConfigLabels extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleOverflowAction = (ev: HaDropdownSelectEvent) => {
|
||||
const action = ev.detail.item.value;
|
||||
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
switch (action) {
|
||||
case "navigate-entities":
|
||||
this._navigateEntities();
|
||||
break;
|
||||
case "navigate-devices":
|
||||
this._navigateDevices();
|
||||
break;
|
||||
case "navigate-automations":
|
||||
this._navigateAutomations();
|
||||
break;
|
||||
case "remove":
|
||||
this._handleRemoveLabelClick();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private _navigateEntities = () => {
|
||||
navigate(
|
||||
`/config/entities?historyBack=1&label=${this._overflowLabel.label_id}`
|
||||
|
||||
@@ -169,7 +169,7 @@ class ErrorLogCard extends LitElement {
|
||||
(boot) => html`
|
||||
<ha-dropdown-item
|
||||
.value=${`boot_${boot}`}
|
||||
class=${boot === this._boot ? "selected" : ""}
|
||||
.selected=${boot === this._boot}
|
||||
>
|
||||
${boot === 0
|
||||
? localize("ui.panel.config.logs.current")
|
||||
@@ -846,12 +846,6 @@ class ErrorLogCard extends LitElement {
|
||||
.download-link {
|
||||
color: var(--text-color);
|
||||
}
|
||||
ha-dropdown-item.selected {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--primary-color);
|
||||
background-color: var(--ha-color-fill-primary-quiet-resting);
|
||||
--icon-primary-color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@ import type { CSSResultGroup } 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 { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-wa-dialog";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "./integrations-startup-time";
|
||||
@@ -12,44 +11,47 @@ import "./integrations-startup-time";
|
||||
class DialogIntegrationStartup extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _opened = false;
|
||||
@state() private _open = false;
|
||||
|
||||
public showDialog(): void {
|
||||
this._opened = true;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._opened = false;
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._open = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._opened) {
|
||||
if (!this._open) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
hideActions
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.panel.config.repairs.integration_startup_time")
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.repairs.integration_startup_time"
|
||||
)}
|
||||
@closed=${this.closeDialog}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<integrations-startup-time
|
||||
.hass=${this.hass}
|
||||
narrow
|
||||
></integrations-startup-time>
|
||||
</ha-dialog>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
ha-wa-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -9,8 +9,8 @@ import { copyToClipboard } from "../../../common/util/copy-clipboard";
|
||||
import { subscribePollingCollection } from "../../../common/util/subscribe-polling";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import "../../../components/ha-wa-dialog";
|
||||
import "../../../components/ha-metric";
|
||||
import "../../../components/ha-spinner";
|
||||
import type { HassioStats } from "../../../data/hassio/common";
|
||||
@@ -62,20 +62,24 @@ class DialogSystemInformation extends LitElement {
|
||||
|
||||
@state() private _coreStats?: HassioStats;
|
||||
|
||||
@state() private _opened = false;
|
||||
@state() private _open = false;
|
||||
|
||||
private _systemHealthSubscription?: Promise<UnsubscribeFunc>;
|
||||
|
||||
private _hassIOSubscription?: UnsubscribeFunc;
|
||||
|
||||
public showDialog(): void {
|
||||
this._opened = true;
|
||||
this._open = true;
|
||||
this.hass!.loadBackendTranslation("system_health");
|
||||
this._subscribe();
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._opened = false;
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._open = false;
|
||||
this._unsubscribe();
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
@@ -126,20 +130,20 @@ class DialogSystemInformation extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._opened) {
|
||||
if (!this._open) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const sections = this._getSections();
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.panel.config.repairs.system_information")
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.repairs.system_information"
|
||||
)}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div>
|
||||
${this._resolutionInfo
|
||||
@@ -224,10 +228,12 @@ class DialogSystemInformation extends LitElement {
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
<ha-button slot="primaryAction" @click=${this._copyInfo}>
|
||||
${this.hass.localize("ui.panel.config.repairs.copy")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button slot="primaryAction" @click=${this._copyInfo}>
|
||||
${this.hass.localize("ui.panel.config.repairs.copy")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||
import { capitalizeFirstLetter } from "../../../common/string/capitalize-first-letter";
|
||||
import { STRINGS_SEPARATOR_DOT } from "../../../common/const";
|
||||
import "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
@@ -99,7 +100,7 @@ class HaConfigRepairs extends LitElement {
|
||||
${(issue.severity === "critical" ||
|
||||
issue.severity === "error") &&
|
||||
issue.created
|
||||
? " · "
|
||||
? STRINGS_SEPARATOR_DOT
|
||||
: ""}
|
||||
${createdBy
|
||||
? html`<span .title=${createdBy}>${createdBy}</span>`
|
||||
|
||||
@@ -52,7 +52,6 @@ import "../../../components/ha-filter-labels";
|
||||
import "../../../components/ha-filter-voice-assistants";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/ha-md-divider";
|
||||
import "../../../components/ha-state-icon";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
|
||||
@@ -54,7 +54,6 @@ import "../../../components/ha-filter-labels";
|
||||
import "../../../components/ha-filter-voice-assistants";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/ha-md-divider";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
|
||||
@@ -571,7 +571,6 @@ export class HaManualScriptEditor extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _saveScript() {
|
||||
this.triggerCloseSidebar();
|
||||
fireEvent(this, "save-script");
|
||||
}
|
||||
|
||||
|
||||
@@ -55,17 +55,16 @@ export class StorageBreakdownChart extends LitElement {
|
||||
<span class="heading">${heading}</span>
|
||||
<span class="description">${description}</span>
|
||||
</div>
|
||||
${hasChildren
|
||||
? html`<ha-icon-button
|
||||
.path=${this._chartType === "sunburst"
|
||||
? mdiViewArray
|
||||
: mdiChartDonutVariant}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.storage.change_chart_type"
|
||||
)}
|
||||
@click=${this._handleChartTypeChange}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
<ha-icon-button
|
||||
.path=${this._chartType === "sunburst"
|
||||
? mdiViewArray
|
||||
: mdiChartDonutVariant}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.storage.change_chart_type"
|
||||
)}
|
||||
.disabled=${!hasChildren}
|
||||
@click=${this._handleChartTypeChange}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
|
||||
<div class="chart-container ${this._chartType}">
|
||||
@@ -106,9 +105,11 @@ export class StorageBreakdownChart extends LitElement {
|
||||
storageInfo: HostDisksUsage | null | undefined
|
||||
) => {
|
||||
let totalSpaceGB = hostInfo.disk_total;
|
||||
let usedSpaceGB = hostInfo.disk_used;
|
||||
let freeSpaceGB =
|
||||
hostInfo.disk_free || hostInfo.disk_total - hostInfo.disk_used;
|
||||
// hostInfo.disk_used doesn't include system reserved space,
|
||||
// so we calculate used space based on total and free space
|
||||
let usedSpaceGB = totalSpaceGB - freeSpaceGB;
|
||||
|
||||
if (storageInfo) {
|
||||
const totalSpace =
|
||||
@@ -213,26 +214,24 @@ export class StorageBreakdownChart extends LitElement {
|
||||
static styles = css`
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--ha-space-2);
|
||||
align-items: flex-end;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.heading-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-1);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-weight: 500;
|
||||
font-size: var(--ha-font-size-m);
|
||||
color: var(--primary-text-color);
|
||||
line-height: var(--ha-line-height-expanded);
|
||||
margin-right: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: var(--ha-font-size-s);
|
||||
color: var(--secondary-text-color);
|
||||
line-height: var(--ha-line-height-expanded);
|
||||
}
|
||||
|
||||
ha-icon-button {
|
||||
|
||||
@@ -454,7 +454,7 @@ export class AssistPref extends LitElement {
|
||||
align-items: center;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
img {
|
||||
voice-assistant-brand-icon {
|
||||
height: 28px;
|
||||
margin-right: 16px;
|
||||
margin-inline-end: 16px;
|
||||
|
||||
@@ -280,7 +280,7 @@ export class CloudAlexaPref extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
img {
|
||||
voice-assistant-brand-icon {
|
||||
height: 28px;
|
||||
margin-right: 16px;
|
||||
margin-inline-end: 16px;
|
||||
|
||||
@@ -152,11 +152,12 @@ export class CloudDiscover extends LitElement {
|
||||
}
|
||||
.feature .logos {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
gap: var(--ha-space-4);
|
||||
}
|
||||
.feature .logos > * {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 0 4px;
|
||||
}
|
||||
.round-icon {
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
|
||||
@@ -350,7 +350,7 @@ export class CloudGooglePref extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
img {
|
||||
voice-assistant-brand-icon {
|
||||
height: 28px;
|
||||
margin-right: 16px;
|
||||
margin-inline-end: 16px;
|
||||
|
||||
@@ -26,25 +26,29 @@ export function getAssistantsTableColumn<T>(
|
||||
valueColumn: "assistants_sortable_key",
|
||||
template: (entry: any) =>
|
||||
html`${entry.assistants.length !== 0
|
||||
? availableAssistants.map((vaId) => {
|
||||
const supported =
|
||||
!supportedEntities?.[vaId] ||
|
||||
supportedEntities[vaId].includes(entry.entity_id);
|
||||
const manual = entry.manAssistants?.includes(vaId);
|
||||
return getAssistantsTableColumnIcon(
|
||||
entry.assistants.includes(vaId),
|
||||
vaId,
|
||||
hass,
|
||||
entitiesToCheck,
|
||||
manual,
|
||||
!supported
|
||||
);
|
||||
})
|
||||
? html`<div style="display: flex; gap: var(--ha-space-4);">
|
||||
${availableAssistants.map((vaId) => {
|
||||
const supported =
|
||||
!supportedEntities?.[vaId] ||
|
||||
supportedEntities[vaId].includes(entry.entity_id);
|
||||
const manual = entry.manAssistants?.includes(vaId);
|
||||
return getAssistantsTableColumnIcon(
|
||||
entry.entity_id,
|
||||
entry.assistants.includes(vaId),
|
||||
vaId,
|
||||
hass,
|
||||
entitiesToCheck,
|
||||
manual,
|
||||
!supported
|
||||
);
|
||||
})}
|
||||
</div>`
|
||||
: nothing}`,
|
||||
};
|
||||
}
|
||||
|
||||
export const getAssistantsTableColumnIcon = (
|
||||
id: string,
|
||||
show: boolean,
|
||||
vaId: string,
|
||||
hass: HomeAssistant,
|
||||
@@ -57,6 +61,7 @@ export const getAssistantsTableColumnIcon = (
|
||||
);
|
||||
return show
|
||||
? html`<voice-assistants-expose-assistant-icon
|
||||
.id=${id}
|
||||
.assistant=${vaId}
|
||||
.hass=${hass}
|
||||
.manual=${manual ?? false}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { mdiAlertCircle } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { slugify } from "../../../../common/string/slugify";
|
||||
import { voiceAssistants } from "../../../../data/expose";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
@@ -23,8 +24,9 @@ export class VoiceAssistantExposeAssistantIcon extends LitElement {
|
||||
|
||||
render() {
|
||||
if (!this.assistant || !voiceAssistants[this.assistant]) return nothing;
|
||||
const id = slugify(this.id) + "-" + this.assistant;
|
||||
return html`
|
||||
<div class="container" id="container">
|
||||
<div class="container" id=${id}>
|
||||
<voice-assistant-brand-icon
|
||||
style=${styleMap({
|
||||
filter: this.manual ? "grayscale(100%)" : undefined,
|
||||
@@ -43,7 +45,7 @@ export class VoiceAssistantExposeAssistantIcon extends LitElement {
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-tooltip
|
||||
for="container"
|
||||
for=${id}
|
||||
placement="left"
|
||||
.disabled=${!this.unsupported && !this.manual}
|
||||
>
|
||||
@@ -66,13 +68,6 @@ export class VoiceAssistantExposeAssistantIcon extends LitElement {
|
||||
.container {
|
||||
position: relative;
|
||||
}
|
||||
.logo {
|
||||
position: relative;
|
||||
height: 24px;
|
||||
margin-right: 16px;
|
||||
margin-inline-end: 16px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
.unsupported {
|
||||
color: var(--error-color);
|
||||
position: absolute;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import {
|
||||
differenceInMonths,
|
||||
subHours,
|
||||
differenceInDays,
|
||||
differenceInMonths,
|
||||
differenceInCalendarMonths,
|
||||
differenceInYears,
|
||||
startOfYear,
|
||||
addMilliseconds,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
addHours,
|
||||
startOfDay,
|
||||
addDays,
|
||||
subDays,
|
||||
} from "date-fns";
|
||||
import type {
|
||||
BarSeriesOption,
|
||||
@@ -33,10 +35,22 @@ import { filterXSS } from "../../../../../common/util/xss";
|
||||
import type { StatisticPeriod } from "../../../../../data/recorder";
|
||||
import { getSuggestedPeriod } from "../../../../../data/energy";
|
||||
|
||||
export function getSuggestedMax(period: StatisticPeriod, end: Date): Date {
|
||||
// Number of days of padding when showing time axis in months
|
||||
const MONTH_TIME_AXIS_PADDING = 5;
|
||||
|
||||
export function getSuggestedMax(
|
||||
period: StatisticPeriod,
|
||||
end: Date,
|
||||
noRounding: boolean
|
||||
): Date {
|
||||
// Maximum period depends on whether plotting a line chart or discrete bars.
|
||||
// - For line charts we must be plotting all the way to end of a given period,
|
||||
// otherwise we cut off the last period of data.
|
||||
// - For bar charts we need to round down to the start of the final bars period
|
||||
// to avoid unnecessary padding of the chart.
|
||||
let suggestedMax = new Date(end);
|
||||
|
||||
if (period === "5minute") {
|
||||
if (noRounding || period === "5minute") {
|
||||
return suggestedMax;
|
||||
}
|
||||
suggestedMax.setMinutes(0, 0, 0);
|
||||
@@ -82,17 +96,44 @@ export function getCommonOptions(
|
||||
detailedDailyData = false
|
||||
): ECOption {
|
||||
const suggestedPeriod = getSuggestedPeriod(start, end, detailedDailyData);
|
||||
const suggestedMax = getSuggestedMax(suggestedPeriod, end, detailedDailyData);
|
||||
|
||||
const compare = compareStart !== undefined && compareEnd !== undefined;
|
||||
const showCompareYear =
|
||||
compare && start.getFullYear() !== compareStart.getFullYear();
|
||||
|
||||
const options: ECOption = {
|
||||
const monthTimeAxis: ECOption = {
|
||||
xAxis: {
|
||||
type: "time",
|
||||
min: subDays(start, MONTH_TIME_AXIS_PADDING),
|
||||
max: addDays(suggestedMax, MONTH_TIME_AXIS_PADDING),
|
||||
axisLabel: {
|
||||
formatter: {
|
||||
year: "{yearStyle|{MMMM} {yyyy}}",
|
||||
month: "{MMMM}",
|
||||
},
|
||||
rich: {
|
||||
yearStyle: {
|
||||
fontWeight: "bold",
|
||||
},
|
||||
},
|
||||
},
|
||||
// For shorter month ranges, force splitting to ensure time axis renders
|
||||
// as whole month intervals. Limit the number of forced ticks to 6 months
|
||||
// (so a max calendar difference of 5) to reduce clutter.
|
||||
splitNumber: Math.min(differenceInCalendarMonths(end, start), 5),
|
||||
},
|
||||
};
|
||||
const normalTimeAxis: ECOption = {
|
||||
xAxis: {
|
||||
type: "time",
|
||||
min: start,
|
||||
max: getSuggestedMax(suggestedPeriod, end),
|
||||
max: suggestedMax,
|
||||
},
|
||||
};
|
||||
|
||||
const options: ECOption = {
|
||||
...(suggestedPeriod === "month" ? monthTimeAxis : normalTimeAxis),
|
||||
yAxis: {
|
||||
type: "value",
|
||||
name: unit,
|
||||
|
||||
@@ -13,7 +13,7 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { BINARY_STATE_ON } from "../../../common/const";
|
||||
import { BINARY_STATE_ON, STRINGS_SEPARATOR_DOT } from "../../../common/const";
|
||||
import { computeAreaName } from "../../../common/entity/compute_area_name";
|
||||
import { generateEntityFilter } from "../../../common/entity/entity_filter";
|
||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
@@ -522,7 +522,7 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
||||
return `${formattedValue}${formattedUnit}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
.join(STRINGS_SEPARATOR_DOT);
|
||||
|
||||
return sensorStates;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { getGraphColorByIndex } from "../../../common/color/colors";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { normalizeValueBySIPrefix } from "../../../common/number/normalize-by-si-prefix";
|
||||
import { MobileAwareMixin } from "../../../mixins/mobile-aware-mixin";
|
||||
import type { EntityNameItem } from "../../../common/entity/compute_entity_name_display";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
@@ -33,6 +35,7 @@ const LEGEND_OVERFLOW_LIMIT_MOBILE = 6;
|
||||
interface ProcessedEntity {
|
||||
entity: string;
|
||||
name?: string | EntityNameItem | EntityNameItem[];
|
||||
color?: string;
|
||||
}
|
||||
|
||||
interface LegendItem {
|
||||
@@ -147,6 +150,7 @@ export class HuiDistributionCard
|
||||
this._configEntities = entities.map((entity) => ({
|
||||
entity: entity.entity,
|
||||
name: entity.name,
|
||||
color: entity.color,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -227,10 +231,16 @@ export class HuiDistributionCard
|
||||
const stateObj = this.hass!.states[entity.entity];
|
||||
if (!stateObj) return;
|
||||
|
||||
const value = Number(stateObj.state);
|
||||
if (value <= 0 || isNaN(value)) return;
|
||||
const rawValue = Number(stateObj.state);
|
||||
if (rawValue <= 0 || isNaN(rawValue)) return;
|
||||
const value = normalizeValueBySIPrefix(
|
||||
rawValue,
|
||||
stateObj.attributes.unit_of_measurement
|
||||
);
|
||||
|
||||
const color = getGraphColorByIndex(entity.originalIndex, computedStyles);
|
||||
const color = entity.color
|
||||
? computeCssColor(entity.color)
|
||||
: getGraphColorByIndex(entity.originalIndex, computedStyles);
|
||||
const name = computeLovelaceEntityName(this.hass!, stateObj, entity.name);
|
||||
const formattedValue = this.hass!.formatEntityState(stateObj);
|
||||
|
||||
@@ -279,7 +289,9 @@ export class HuiDistributionCard
|
||||
name: name,
|
||||
value: value,
|
||||
formattedValue: formattedValue,
|
||||
color: getGraphColorByIndex(index, computedStyles),
|
||||
color: entity.color
|
||||
? computeCssColor(entity.color)
|
||||
: getGraphColorByIndex(index, computedStyles),
|
||||
isHidden: isHidden,
|
||||
isDisabled: isZeroOrNegative,
|
||||
};
|
||||
|
||||
@@ -13,11 +13,6 @@ import {
|
||||
stateColorCss,
|
||||
} from "../../../common/entity/state_color";
|
||||
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
||||
import {
|
||||
formatNumber,
|
||||
getNumberFormatOptions,
|
||||
isNumericState,
|
||||
} from "../../../common/number/format_number";
|
||||
import { iconColorCSS } from "../../../common/style/icon_color_css";
|
||||
import "../../../components/ha-attribute-value";
|
||||
import "../../../components/ha-card";
|
||||
@@ -125,6 +120,7 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
const domain = computeStateDomain(stateObj);
|
||||
const stateParts = this.hass.formatEntityStateToParts(stateObj);
|
||||
|
||||
let unit;
|
||||
if (
|
||||
@@ -134,9 +130,9 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
|
||||
) {
|
||||
unit = this._config.unit;
|
||||
if (!unit) {
|
||||
if (!this._config.attribute)
|
||||
unit = stateObj.attributes.unit_of_measurement;
|
||||
else {
|
||||
if (!this._config.attribute) {
|
||||
unit = stateParts.find((part) => part.type === "unit")?.value;
|
||||
} else {
|
||||
const parts = this.hass.formatEntityAttributeValueToParts(
|
||||
stateObj,
|
||||
this._config.attribute
|
||||
@@ -205,17 +201,7 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
|
||||
>
|
||||
</ha-attribute-value>`
|
||||
: this.hass.localize("state.default.unknown")
|
||||
: (isNumericState(stateObj) || this._config.unit) &&
|
||||
stateObj.attributes.device_class !== "duration"
|
||||
? formatNumber(
|
||||
stateObj.state,
|
||||
this.hass.locale,
|
||||
getNumberFormatOptions(
|
||||
stateObj,
|
||||
this.hass.entities[this._config.entity]
|
||||
)
|
||||
)
|
||||
: this.hass.formatEntityState(stateObj)}</span
|
||||
: stateParts.find((part) => part.type === "value")?.value}</span
|
||||
>
|
||||
${unit ? html`<span class="measurement">${unit}</span>` : nothing}
|
||||
</div>
|
||||
|
||||
@@ -332,7 +332,11 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
|
||||
.maxYAxis=${this._config.max_y_axis}
|
||||
.startTime=${this._energyStart}
|
||||
.endTime=${this._energyEnd && this._energyStart
|
||||
? getSuggestedMax(this._period!, this._energyEnd)
|
||||
? getSuggestedMax(
|
||||
this._period!,
|
||||
this._energyEnd,
|
||||
(this._config.chart_type ?? "line") === "line"
|
||||
)
|
||||
: undefined}
|
||||
.fitYData=${this._config.fit_y_data || false}
|
||||
.hideLegend=${this._config.hide_legend || false}
|
||||
|
||||
@@ -94,6 +94,7 @@ export interface EntitiesCardEntityConfig extends EntityConfig {
|
||||
| "last-changed"
|
||||
| "last-triggered"
|
||||
| "last-updated"
|
||||
| "area"
|
||||
| "position"
|
||||
| "state"
|
||||
| "tilt-position"
|
||||
@@ -664,7 +665,9 @@ export interface HomeSummaryCard extends LovelaceCardConfig {
|
||||
double_tap_action?: ActionConfig;
|
||||
}
|
||||
|
||||
export interface DistributionEntityConfig extends EntityConfig {}
|
||||
export interface DistributionEntityConfig extends EntityConfig {
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface DistributionCardConfig extends LovelaceCardConfig {
|
||||
type: "distribution";
|
||||
|
||||
@@ -157,7 +157,7 @@ class HuiWaterSankeyCard
|
||||
}
|
||||
|
||||
nodes.push({
|
||||
id: source.stat_energy_from,
|
||||
id: `source-${source.stat_energy_from}`,
|
||||
label: getStatisticLabel(
|
||||
this.hass,
|
||||
source.stat_energy_from,
|
||||
@@ -169,7 +169,7 @@ class HuiWaterSankeyCard
|
||||
});
|
||||
|
||||
links.push({
|
||||
source: source.stat_energy_from,
|
||||
source: `source-${source.stat_energy_from}`,
|
||||
target: "home",
|
||||
value,
|
||||
});
|
||||
|
||||
@@ -246,6 +246,7 @@ export class HuiEntityEditor extends LitElement {
|
||||
}
|
||||
ha-md-list {
|
||||
gap: 8px;
|
||||
padding-top: 0;
|
||||
}
|
||||
ha-md-list-item {
|
||||
border: 1px solid var(--divider-color);
|
||||
|
||||
@@ -8,6 +8,8 @@ import { uid } from "../../../common/util/uid";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { toggleAttribute } from "../../../common/dom/toggle_attribute";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeAreaName } from "../../../common/entity/compute_area_name";
|
||||
import { getEntityContext } from "../../../common/entity/context/get_entity_context";
|
||||
import { formatDateTimeWithSeconds } from "../../../common/datetime/format_date_time";
|
||||
import "../../../components/entity/state-badge";
|
||||
import "../../../components/ha-relative-time";
|
||||
@@ -199,7 +201,9 @@ export class HuiGenericEntityRow extends LitElement {
|
||||
? html`${this.hass.formatEntityState(
|
||||
stateObj
|
||||
)}`
|
||||
: nothing)}
|
||||
: this.config.secondary_info === "area"
|
||||
? (this._getArea(stateObj) ?? nothing)
|
||||
: nothing)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
@@ -235,6 +239,17 @@ export class HuiGenericEntityRow extends LitElement {
|
||||
handleAction(this, this.hass!, this.config!, ev.detail.action!);
|
||||
}
|
||||
|
||||
private _getArea(stateObj) {
|
||||
const context = getEntityContext(
|
||||
stateObj,
|
||||
this.hass!.entities,
|
||||
this.hass!.devices,
|
||||
this.hass!.areas,
|
||||
this.hass!.floors
|
||||
);
|
||||
return context.area ? computeAreaName(context.area) : undefined;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user