Compare commits

..

1 Commits

Author SHA1 Message Date
Petar Petrov 128c5bddeb Show progress on the Zigbee backup button while the backup is created 2026-07-31 13:25:14 +03:00
48 changed files with 575 additions and 1381 deletions
+1 -1
View File
@@ -62,7 +62,7 @@ Managed app, demo, gallery, and E2E app workflows share one lifetime lock, so on
`yarn dev:serve` also serves locally and supports `-c` for the core URL and `-p` for the port. The default is 8124, or 8123 in a devcontainer.
Dev server commands support `--background`, `--status`, `--stop`, and `--logs [--follow]`. `yarn dev`, `yarn dev:serve`, `yarn dev:demo`, and `yarn dev:gallery` also support `--fetch-translations`; this runs translation fetching, including first-time GitHub device authentication, under the workflow lock before starting the watcher. It works in foreground and background modes. Prefer managed background mode while iterating so the watcher stays available across test runs without occupying the terminal. `yarn dev` and `yarn dev:serve` share one managed process slot because both write the app output.
Dev server commands support `--background`, `--status`, `--stop`, and `--logs [--follow]`. Prefer managed background mode while iterating so the watcher stays available across test runs without occupying the terminal. `yarn dev` and `yarn dev:serve` share one managed process slot because both write the app output.
## Playwright E2E
+8 -26
View File
@@ -19,7 +19,6 @@ import {
acquireProcessRecord,
isProcessRecordAlive,
outputLog,
offerToStopProcessRecord,
processStartTime,
readProcessRecord,
releaseProcessRecord,
@@ -101,16 +100,9 @@ const readBuild = () => readProcessRecord(lockFile);
const releaseBuild = (token) => releaseProcessRecord(lockFile, token);
const stopCommandFor = (owner) =>
owner?.kind === "build"
? "yarn build --stop"
: owner?.kind === "dev"
? `yarn ${devCommand(owner.suite)} --stop`
: undefined;
const acquireBuild = async (modern, foreground) => {
const acquireBuild = (modern, foreground) => {
const token = `${process.pid}-${Date.now()}-${Math.random()}`;
const record = {
const result = acquireProcessRecord(lockFile, {
pid: process.pid,
startTime: processStartTime(process.pid),
processGroup: false,
@@ -119,20 +111,8 @@ const acquireBuild = async (modern, foreground) => {
modern,
starting: true,
token,
};
const result = acquireProcessRecord(lockFile, record);
if (result.acquired) {
return { token };
}
reportExisting(result.existing);
return (await offerToStopProcessRecord({
file: lockFile,
owner: result.existing,
ownerDescription: describeOutputOwner(result.existing),
stopCommand: stopCommandFor(result.existing),
}))
? acquireBuild(modern, foreground)
: { existing: result.existing };
});
return result.acquired ? { token } : { existing: result.existing };
};
const updateBuild = (token, child, processGroup) => {
@@ -177,8 +157,9 @@ const reportExisting = (existing) => {
};
const runForeground = async (modern) => {
const lock = await acquireBuild(modern, true);
const lock = acquireBuild(modern, true);
if (!lock.token) {
reportExisting(lock.existing);
return 1;
}
try {
@@ -196,8 +177,9 @@ const runForeground = async (modern) => {
};
const runBackground = async (modern) => {
const lock = await acquireBuild(modern, false);
const lock = acquireBuild(modern, false);
if (!lock.token) {
reportExisting(lock.existing);
return 1;
}
let child;
+43 -124
View File
@@ -8,9 +8,6 @@
// --status Report whether the suite's dev server is running.
// --stop Stop a running background dev server.
// --logs [--follow] Print (or follow) the background dev server log.
// --fetch-translations
// Fetch nightly translations before starting (app,
// app-serve, demo, and gallery only).
//
// Extra args (for example -p or -c on app-serve) are forwarded to the underlying
// script. Suites use one of two liveness models:
@@ -27,10 +24,8 @@ import { fileURLToPath } from "node:url";
import {
LIFECYCLE_MODE_FLAGS,
acquireProcessRecord,
detectCodingAgent,
isProcessRecordAlive,
outputLog,
offerToStopProcessRecord,
processStartTime,
readProcessRecord,
releaseProcessRecord,
@@ -78,7 +73,6 @@ const SUITES = new Map([
"demo",
{
alias: "dev:demo",
fetchTranslations: true,
liveness: "health",
port: 8090,
spawn: { cmd: gulpBin, args: ["develop-demo"] },
@@ -88,7 +82,6 @@ const SUITES = new Map([
"gallery",
{
alias: "dev:gallery",
fetchTranslations: true,
liveness: "health",
port: 8100,
spawn: { cmd: gulpBin, args: ["develop-gallery"] },
@@ -98,7 +91,6 @@ const SUITES = new Map([
"app",
{
alias: "dev",
fetchTranslations: true,
liveness: "process",
readyLog: /Build done @/,
spawn: { cmd: gulpBin, args: ["develop-app"] },
@@ -110,7 +102,6 @@ const SUITES = new Map([
alias: "dev:serve",
liveness: "process",
acceptsArgs: true,
fetchTranslations: true,
readyLog: /Build done @/,
spawn: { cmd: developAndServeScript, args: [] },
},
@@ -125,18 +116,40 @@ const READY_TIMEOUT_MS =
? readyTimeoutSeconds * 1000
: 180_000;
// Detect a coding agent from a small set of environment markers set by common
// agent CLIs (env-only; no process-ancestry detection).
const detectAgent = () => {
const env = process.env;
const has = (name) => Boolean(env[name]);
const eq = (name, value) => env[name] === value;
const signals = {
opencode: () =>
[
"OPENCODE",
"OPENCODE_BIN_PATH",
"OPENCODE_SERVER",
"OPENCODE_APP_INFO",
].some(has),
"claude-code": () => has("CLAUDECODE"),
cursor: () => has("CURSOR_TRACE_ID"),
"github-copilot": () =>
eq("TERM_PROGRAM", "vscode") && eq("GIT_PAGER", "cat"),
// Convention shared by several agents (Crush, Amp, ...).
generic: () => has("AGENT") || has("AI_AGENT"),
};
return Object.keys(signals).find((id) => signals[id]());
};
const usage = () => {
const suites = [...SUITES.keys()].join("|");
process.stderr.write(
`Usage: node build-scripts/dev-server.mjs --suite <${suites}> ` +
`[--background | --status | --stop | --logs [--follow]] ` +
`[--fetch-translations]\n`
`[--background | --status | --stop | --logs [--follow]]\n`
);
};
const parseArgs = (argv) => {
const args = {
fetchTranslations: false,
mode: "foreground",
follow: false,
modes: [],
@@ -152,9 +165,6 @@ const parseArgs = (argv) => {
case "--follow":
args.follow = true;
break;
case "--fetch-translations":
args.fetchTranslations = true;
break;
default:
if (LIFECYCLE_MODE_FLAGS.has(arg)) {
args.mode = LIFECYCLE_MODE_FLAGS.get(arg);
@@ -168,35 +178,12 @@ const parseArgs = (argv) => {
return args;
};
const translationPrebuildEnv = (token) => {
const env = workflowLockEnv(token);
delete env.SKIP_FETCH_NIGHTLY_TRANSLATIONS;
return env;
};
const suiteEnv = (token, fetchTranslations = false) => ({
...workflowLockEnv(token),
...(fetchTranslations && { SKIP_FETCH_NIGHTLY_TRANSLATIONS: "1" }),
});
const runPrebuild = (token, fetchTranslations = false) =>
fetchTranslations
? spawnForeground({
cmd: gulpBin,
args: ["setup-and-fetch-nightly-translations"],
cwd: repoRoot,
env: translationPrebuildEnv(token),
processGroup: true,
})
: Promise.resolve(0);
const logFileFor = (suite) => path.join(logDir, `${suite}.log`);
const acquireSuite = (suite) => {
const token = `${process.pid}-${Date.now()}-${Math.random()}`;
const record = {
pid: process.pid,
startTime: processStartTime(process.pid),
processGroup: false,
kind: "dev",
suite,
starting: true,
@@ -267,29 +254,12 @@ const reportProcessConflict = (suite, existing) => {
);
};
const stopCommandFor = (owner) =>
owner?.kind === "build"
? "yarn build --stop"
: owner?.kind === "dev"
? `yarn ${SUITES.get(owner.suite)?.alias ?? "dev"} --stop`
: undefined;
const acquireSuiteForStart = async (suite) => {
const acquireSuiteForStart = (suite) => {
const lock = acquireSuite(suite);
if (lock.token) {
return lock;
}
reportProcessConflict(suite, lock.existing);
if (
await offerToStopProcessRecord({
file: workflowLockFile,
owner: lock.existing,
ownerDescription: describeOutputOwner(lock.existing),
stopCommand: stopCommandFor(lock.existing),
})
) {
return acquireSuiteForStart(suite);
}
return {
code:
lock.existing?.kind === "dev" &&
@@ -435,9 +405,9 @@ const isHttpServing = async (port, timeoutMs = 1000) => {
return probeHost(0);
};
const runForegroundHealth = async (suite, cfg, fetchTranslations = false) => {
const runForegroundHealth = async (suite, cfg) => {
const { port } = cfg;
const lock = await acquireSuiteForStart(suite);
const lock = acquireSuiteForStart(suite);
if (!lock.token) {
return lock.code;
}
@@ -457,15 +427,11 @@ const runForegroundHealth = async (suite, cfg, fetchTranslations = false) => {
return 1;
}
try {
const prebuildCode = await runPrebuild(lock.token, fetchTranslations);
if (prebuildCode !== 0) {
return prebuildCode;
}
return await spawnForeground({
cmd: cfg.spawn.cmd,
args: cfg.spawn.args,
cwd: repoRoot,
env: suiteEnv(lock.token, fetchTranslations),
env: workflowLockEnv(lock.token),
processGroup: true,
onSpawn: (child) => updateSuite(suite, lock.token, child, port),
});
@@ -474,9 +440,9 @@ const runForegroundHealth = async (suite, cfg, fetchTranslations = false) => {
}
};
const runBackgroundHealth = async (suite, cfg, fetchTranslations = false) => {
const runBackgroundHealth = async (suite, cfg) => {
const { port } = cfg;
const lock = await acquireSuiteForStart(suite);
const lock = acquireSuiteForStart(suite);
if (!lock.token) {
return lock.code;
}
@@ -497,17 +463,12 @@ const runBackgroundHealth = async (suite, cfg, fetchTranslations = false) => {
}
let child;
try {
const prebuildCode = await runPrebuild(lock.token, fetchTranslations);
if (prebuildCode !== 0) {
releaseSuite(lock.token);
return prebuildCode;
}
const logFile = logFileFor(suite);
child = await spawnDetachedToLog({
cmd: cfg.spawn.cmd,
args: cfg.spawn.args,
cwd: repoRoot,
env: suiteEnv(lock.token, fetchTranslations),
env: workflowLockEnv(lock.token),
logFile,
});
updateSuite(suite, lock.token, child, port);
@@ -559,26 +520,17 @@ const spawnArgs = (cfg, passthrough) => [
...(cfg.acceptsArgs ? passthrough : []),
];
const runForegroundProcess = async (
suite,
cfg,
passthrough,
fetchTranslations = false
) => {
const lock = await acquireSuiteForStart(suite);
const runForegroundProcess = async (suite, cfg, passthrough) => {
const lock = acquireSuiteForStart(suite);
if (!lock.token) {
return lock.code;
}
try {
const prebuildCode = await runPrebuild(lock.token, fetchTranslations);
if (prebuildCode !== 0) {
return prebuildCode;
}
return await spawnForeground({
cmd: cfg.spawn.cmd,
args: spawnArgs(cfg, passthrough),
cwd: repoRoot,
env: suiteEnv(lock.token, fetchTranslations),
env: workflowLockEnv(lock.token),
processGroup: true,
onSpawn: (child) => updateSuite(suite, lock.token, child),
});
@@ -587,30 +539,20 @@ const runForegroundProcess = async (
}
};
const runBackgroundProcess = async (
suite,
cfg,
passthrough,
fetchTranslations = false
) => {
const lock = await acquireSuiteForStart(suite);
const runBackgroundProcess = async (suite, cfg, passthrough) => {
const lock = acquireSuiteForStart(suite);
if (!lock.token) {
return lock.code;
}
let child;
try {
const prebuildCode = await runPrebuild(lock.token, fetchTranslations);
if (prebuildCode !== 0) {
releaseSuite(lock.token);
return prebuildCode;
}
const logFile = logFileFor(suite);
child = await spawnDetachedToLog({
cmd: cfg.spawn.cmd,
args: spawnArgs(cfg, passthrough),
cwd: repoRoot,
env: suiteEnv(lock.token, fetchTranslations),
env: workflowLockEnv(lock.token),
logFile,
});
@@ -688,17 +630,6 @@ const main = async () => {
usage();
return 1;
}
if (
args.fetchTranslations &&
(!["foreground", "background"].includes(args.mode) ||
!cfg.fetchTranslations)
) {
process.stderr.write(
"--fetch-translations is only supported when starting app, app-serve, demo, or gallery.\n"
);
usage();
return 1;
}
if (args.passthrough.length && !cfg.acceptsArgs) {
process.stderr.write(
`Ignoring unexpected arguments: ${args.passthrough.join(" ")}\n`
@@ -712,7 +643,7 @@ const main = async () => {
mode === "foreground" &&
!["0", "false"].includes(process.env.HA_DEV_BACKGROUND)
) {
const agent = detectCodingAgent();
const agent = detectAgent();
if (agent) {
process.stdout.write(
`Detected coding agent (${agent}); starting in the background. ` +
@@ -734,26 +665,14 @@ const main = async () => {
const handlers =
cfg.liveness === "health"
? {
foreground: () =>
runForegroundHealth(args.suite, cfg, args.fetchTranslations),
background: () =>
runBackgroundHealth(args.suite, cfg, args.fetchTranslations),
foreground: () => runForegroundHealth(args.suite, cfg),
background: () => runBackgroundHealth(args.suite, cfg),
}
: {
foreground: () =>
runForegroundProcess(
args.suite,
cfg,
args.passthrough,
args.fetchTranslations
),
runForegroundProcess(args.suite, cfg, args.passthrough),
background: () =>
runBackgroundProcess(
args.suite,
cfg,
args.passthrough,
args.fetchTranslations
),
runBackgroundProcess(args.suite, cfg, args.passthrough),
};
return handlers[mode]();
};
-12
View File
@@ -58,12 +58,6 @@ const getCommonTemplateVars = () => {
return {
modernRegex: compileRegex(browserRegexes.concat(haMacOSRegex)).toString(),
hassUrl: process.env.HASS_URL || "",
// Single source for the stale-build recovery patterns, shared with the
// bundled src/util/recover-stale-build.ts and injected into the inline
// boot guard (_bootstrap_recovery.html.template).
staleBuildPatterns: fs.readJsonSync(
resolve(paths.root_dir, "src/util/stale-build-patterns.json")
),
};
};
@@ -113,9 +107,6 @@ const genPagesDevTask =
resolve(inputRoot, inputSub, `${page}.template`),
{
...commonVars,
// Dev entries are unhashed, so the stale-index recovery guard has
// nothing to key off and rebuild churn could cause spurious reloads.
useCacheRecovery: false,
latestEntryJS: entries.map(
(entry) => `${publicRoot}/frontend_latest/${entry}.js`
),
@@ -155,9 +146,6 @@ const genPagesProdTask =
resolve(inputRoot, inputSub, `${page}.template`),
{
...commonVars,
// Recover from a stale index.html that pins deleted hashed entry
// bundles (see _bootstrap_recovery.html.template).
useCacheRecovery: true,
latestEntryJS: entries.map((entry) => latestManifest[`${entry}.js`]),
es5EntryJS: outputES5
? entries.map((entry) => es5Manifest[`${entry}.js`])
-184
View File
@@ -1,7 +1,6 @@
import { spawn, execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { createInterface } from "node:readline/promises";
export const LIFECYCLE_MODE_FLAGS = new Map([
["--background", "background"],
@@ -10,134 +9,6 @@ export const LIFECYCLE_MODE_FLAGS = new Map([
["--logs", "logs"],
]);
const AGENT_PROVIDERS = [
{
id: "opencode",
env: [
"OPENCODE",
"OPENCODE_BIN_PATH",
"OPENCODE_SERVER",
"OPENCODE_APP_INFO",
"OPENCODE_MODES",
],
processes: ["opencode"],
},
{
id: "claude-code",
env: ["CLAUDECODE"],
processes: ["claude"],
},
{
id: "cursor",
env: ["CURSOR_TRACE_ID"],
processes: [],
},
{
id: "github-copilot",
matchesEnv: (env) =>
env.TERM_PROGRAM === "vscode" && env.GIT_PAGER === "cat",
processes: [],
},
{
id: "generic",
env: ["AGENT", "AI_AGENT"],
processes: [],
},
];
const MAX_ANCESTRY_HOPS = 24;
const readProcessParent = (pid) => {
try {
const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
const commandEnd = stat.lastIndexOf(")");
const commandStart = stat.indexOf("(");
if (commandStart === -1 || commandEnd === -1) {
return undefined;
}
const parentPid = Number.parseInt(
stat.slice(commandEnd + 2).split(" ")[1] ?? "",
10
);
return Number.isFinite(parentPid)
? {
command: stat.slice(commandStart + 1, commandEnd).toLowerCase(),
parentPid,
}
: undefined;
} catch {
return undefined;
}
};
export const detectCodingAgent = (env = process.env, pid = process.pid) => {
if (env.HA_CODING_AGENT === "0") {
return undefined;
}
const envMatch = AGENT_PROVIDERS.find(
(provider) =>
provider.matchesEnv?.(env) || provider.env?.some((name) => env[name])
);
if (envMatch) {
return envMatch.id;
}
if (env.HA_CODING_AGENT === "1") {
return "unknown";
}
let currentPid = pid;
for (let hop = 0; hop < MAX_ANCESTRY_HOPS; hop++) {
const processInfo = readProcessParent(currentPid);
if (!processInfo) {
return undefined;
}
const processMatch = AGENT_PROVIDERS.find((provider) =>
provider.processes.some((name) => processInfo.command.includes(name))
);
if (processMatch) {
return processMatch.id;
}
if (processInfo.parentPid <= 1) {
return undefined;
}
currentPid = processInfo.parentPid;
}
return undefined;
};
const canPromptForConflict = () =>
Boolean(process.stdin.isTTY && process.stderr.isTTY && !detectCodingAgent());
const formatCommand = (command) =>
!("NO_COLOR" in process.env)
? `\u001b[1;36m${command}\u001b[0m`
: `\`${command}\``;
const confirmStopConflict = async (ownerDescription, stopCommand) => {
const readline = createInterface({
input: process.stdin,
output: process.stderr,
});
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30_000);
try {
process.stderr.write(
`${ownerDescription} can be stopped with ${formatCommand(stopCommand)}.\n`
);
const answer = await readline.question("Stop it and continue? [y/N] ", {
signal: controller.signal,
});
return ["y", "yes"].includes(answer.trim().toLowerCase());
} catch (err) {
if (err?.name !== "AbortError") {
throw err;
}
process.stderr.write("\n");
return false;
} finally {
clearTimeout(timeout);
readline.close();
}
};
export const sleep = (ms) =>
new Promise((resolve) => {
setTimeout(resolve, ms);
@@ -449,61 +320,6 @@ export const terminateProcess = async ({
return await isStopped();
};
const sameProcessRecord = (current, expected) =>
Boolean(expected?.token) && current?.token === expected.token;
const stopProcessRecord = async (file, owner) => {
let current = readProcessRecord(file);
if (!current) {
return true;
}
if (!sameProcessRecord(current, owner)) {
return false;
}
if (!isProcessRecordAlive(current)) {
releaseProcessRecord(file, current.token);
return true;
}
const stopped = await terminateProcess({
pid: current.pid,
processGroup: current.processGroup ?? false,
isStopped: () => {
const latest = readProcessRecord(file);
return (
!sameProcessRecord(latest, owner) ||
!latest ||
!isProcessRecordAlive(latest)
);
},
});
current = readProcessRecord(file);
if (current && !sameProcessRecord(current, owner)) {
return false;
}
if (!stopped && current) {
return false;
}
releaseProcessRecord(file, owner.token);
return true;
};
export const offerToStopProcessRecord = async ({
file,
owner,
ownerDescription,
stopCommand,
}) => {
if (
!owner ||
!stopCommand ||
!canPromptForConflict() ||
!(await confirmStopConflict(ownerDescription, stopCommand))
) {
return false;
}
return stopProcessRecord(file, owner);
};
export const terminateDetachedProcess = (child) =>
terminateProcess({
pid: child.pid,
@@ -23,7 +23,7 @@ export class DemoHaProgressButton extends LitElement {
<ha-progress-button @click=${this._clickedFail}>
Fail
</ha-progress-button>
<ha-progress-button size="small" @click=${this._clickedSuccess}>
<ha-progress-button size="s" @click=${this._clickedSuccess}>
small
</ha-progress-button>
<ha-progress-button
+2 -2
View File
@@ -152,7 +152,7 @@
"@octokit/rest": "22.0.1",
"@playwright/test": "1.62.0",
"@rsdoctor/rspack-plugin": "1.6.1",
"@rspack/core": "2.1.7",
"@rspack/core": "2.1.6",
"@rspack/dev-server": "2.1.0",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.26",
@@ -193,7 +193,7 @@
"gulp-rename": "2.1.0",
"html-minifier-terser": "7.2.0",
"husky": "9.1.7",
"jsdom": "30.0.1",
"jsdom": "30.0.0",
"jszip": "3.10.1",
"license-checker-rseidelsohn": "5.0.1",
"lint-staged": "17.2.0",
+10 -6
View File
@@ -18,6 +18,8 @@ export class HaProgressButton extends LitElement {
@property() appearance: Appearance = "accent";
@property() size: "xs" | "s" | "m" | "l" | "xl" = "m";
@property({ attribute: false }) public iconPath?: string;
@property() variant: "brand" | "danger" | "neutral" | "warning" | "success" =
@@ -32,6 +34,7 @@ export class HaProgressButton extends LitElement {
return html`
<ha-button
.appearance=${appearance}
.size=${this.size}
.disabled=${this.disabled}
.loading=${this.progress}
.variant=${
@@ -118,15 +121,16 @@ export class HaProgressButton extends LitElement {
width: 100%;
}
/* Fade the content out rather than hiding it, so the button keeps its
accessible name while the result icon covers it. */
/* The icon lives in this shadow root, so callers cannot size it themselves. */
ha-button[size="xs"] ha-svg-icon[slot="start"],
ha-button[size="s"] ha-svg-icon[slot="start"] {
--mdc-icon-size: 16px;
}
ha-button.result::part(start),
ha-button.result::part(end),
ha-button.result::part(label),
ha-button.result::part(caret) {
opacity: 0;
}
ha-button.result::part(caret),
ha-button.result::part(spinner) {
visibility: hidden;
}
@@ -45,7 +45,6 @@ import {
} from "../../data/media_source";
import { isTTSMediaSource } from "../../data/tts";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { panelIsReady } from "../../layouts/panel-ready";
import { haStyle, haStyleScrollbar } from "../../resources/styles";
import { loadVirtualizer } from "../../resources/virtualizer";
import type { HomeAssistant } from "../../types";
@@ -162,8 +161,6 @@ export class HaMediaPlayerBrowse extends LitElement {
private _resizeObserver?: ResizeObserver;
private _initialReady = false;
public connectedCallback(): void {
super.connectedCallback();
this.updateComplete.then(() => this._attachResizeObserver());
@@ -277,7 +274,6 @@ export class HaMediaPlayerBrowse extends LitElement {
ids: navigateIds,
current: this._currentItem,
});
this._signalInitialReady();
} else {
if (!currentProm) {
currentProm = this._fetchData(
@@ -293,7 +289,6 @@ export class HaMediaPlayerBrowse extends LitElement {
ids: navigateIds,
current: item,
});
this._signalInitialReady();
},
(err) => {
// When we change entity ID, we will first try to see if the new entity is
@@ -327,10 +322,8 @@ export class HaMediaPlayerBrowse extends LitElement {
),
code: "entity_not_found",
});
this._signalInitialReady();
} else {
this._setError(err);
this._signalInitialReady();
}
}
);
@@ -1152,21 +1145,6 @@ export class HaMediaPlayerBrowse extends LitElement {
fireEvent(this, "close-dialog");
}
private _signalInitialReady(): void {
if (this._initialReady) {
return;
}
this._initialReady = true;
const root = this.getRootNode();
panelIsReady(
root instanceof ShadowRoot &&
root.host instanceof HTMLElement &&
root.host.tagName.startsWith("HA-PANEL-")
? root.host
: this
);
}
private _setError(error: any) {
if (!this.dialog) {
this._error = error;
@@ -1231,8 +1209,8 @@ export class HaMediaPlayerBrowse extends LitElement {
}
private _animateHeaderHeight() {
let start: number | undefined;
const animate = (time: number) => {
let start;
const animate = (time) => {
if (start === undefined) {
start = time;
}
-1
View File
@@ -23,7 +23,6 @@ export interface LovelaceViewElement extends HTMLElement {
badges?: HuiBadge[];
sections?: HuiSection[];
isStrategy: boolean;
initialRenderComplete?: Promise<void>;
setConfig(config: LovelaceViewConfig): void;
}
+1 -12
View File
@@ -37,26 +37,15 @@ declare global {
}
const clearUrlParams = () => {
const searchParams = new URLSearchParams(location.search);
let changed = false;
// Clear auth data from url if we have been able to establish a connection
if (location.search.includes("auth_callback=1")) {
const searchParams = new URLSearchParams(location.search);
// https://github.com/home-assistant/home-assistant-js-websocket/blob/master/lib/auth.ts
// Remove all data from QueryCallbackData type
searchParams.delete("auth_callback");
searchParams.delete("code");
searchParams.delete("state");
searchParams.delete("storeToken");
changed = true;
}
// Remove the cache-busting param added by the stale-index recovery guard in
// index.html once we have booted successfully, so it doesn't linger in the
// URL (and stops acting as the guard's one-shot loop marker).
if (searchParams.has("ha_cache_bust")) {
searchParams.delete("ha_cache_bust");
changed = true;
}
if (changed) {
const search = searchParams.toString();
history.replaceState(
null,
-130
View File
@@ -1,130 +0,0 @@
<script>
/*
* Boot-time recovery from a stale index.html.
*
* index.html pins the content-hashed entry bundles (core.<hash>.js /
* app.<hash>.js). After a frontend release those files are deleted from
* disk, so a cached (stale) index.html imports URLs that now 404 and the
* app never boots - the launch screen stays up forever. No bundled JS can
* recover this, because the bundle itself failed to load, so this guard has
* to live inline in the HTML document.
*
* On a failed entry load we navigate once to the same URL with a
* cache-busting query param (and, on https, after dropping the service
* worker + its caches) so the browser fetches a fresh index.html that points
* at the current hashes. The param doubles as a one-shot loop guard: if the
* freshly fetched index still fails we stop and show a message instead of
* reloading forever. core.ts strips the param again after a successful boot.
*/
(function () {
var BUST_PARAM = "ha_cache_bust";
// Only production entry bundles carry a content hash. Requiring the hash
// keeps the guard inert in development (unhashed core.js / app.js) and
// scoped to the entry chunks whose 404 is fatal to boot - not translations
// or lazily loaded panels, which fail non-fatally and are handled elsewhere.
// Injected from stale-build-patterns.json (the single source shared with
// the bundled util/recover-stale-build.ts) so the two never drift.
var HASHED_ENTRY = new RegExp(<%= JSON.stringify(staleBuildPatterns.hashedEntry) %>, "i");
var MODULE_ERROR = new RegExp(<%= JSON.stringify(staleBuildPatterns.moduleError) %>, "i");
function booted() {
// The launch screen is only removed once the app has taken over the
// page. If it is gone, any later chunk failure is post-boot (a lazy
// panel/dialog) and is not the stale-index boot failure this guard
// targets, so we leave it alone.
return !document.getElementById("ha-launch-screen");
}
function showFatal() {
var box = document.getElementById("ha-launch-screen-info-box");
if (box) {
box.textContent =
"Could not load Home Assistant. Please refresh the page, and clear your browser cache if the problem persists.";
}
}
function recover() {
if (location.search.indexOf(BUST_PARAM + "=") !== -1) {
// We already reloaded once with a fresh index and it still failed;
// stop to avoid a reload loop.
showFatal();
return;
}
var target =
location.pathname +
location.search +
(location.search ? "&" : "?") +
BUST_PARAM +
"=" +
Date.now() +
location.hash;
// On https the service worker "/" route ignores the query string
// (ignoreSearch), so a bust param alone would still be answered with the
// stale cached index. Drop the worker and its caches first so the reload
// is served fresh from the network.
if ("serviceWorker" in navigator && navigator.serviceWorker.controller) {
var go = function () {
location.replace(target);
};
Promise.all([
navigator.serviceWorker
.getRegistrations()
.then(function (regs) {
return Promise.all(
regs.map(function (r) {
return r.unregister();
})
);
})
.catch(function () {}),
self.caches
? caches
.keys()
.then(function (keys) {
return Promise.all(
keys.map(function (k) {
return caches.delete(k);
})
);
})
.catch(function () {})
: null,
]).then(go, go);
} else {
location.replace(target);
}
}
function maybeRecover(url, message) {
if (booted()) {
return;
}
if (
(url && HASHED_ENTRY.test(url)) ||
(message && (HASHED_ENTRY.test(message) || MODULE_ERROR.test(message)))
) {
recover();
}
}
// Resource-load errors (<script>, modulepreload <link>) do not bubble, so
// they are only observable in the capture phase at the window.
window.addEventListener(
"error",
function (ev) {
var el = ev.target;
if (el && (el.tagName === "SCRIPT" || el.tagName === "LINK")) {
maybeRecover(el.href || el.src, null);
}
},
true
);
// A failed dynamic import() rejects; the inline entry imports do not catch
// it, so the rejection surfaces here.
window.addEventListener("unhandledrejection", function (ev) {
var reason = ev && ev.reason;
maybeRecover(null, reason && (reason.message || String(reason)));
});
})();
</script>
-1
View File
@@ -2,7 +2,6 @@
<html>
<head>
<title>Home Assistant</title>
<% if (useCacheRecovery) { %><%= renderTemplate("_bootstrap_recovery.html.template") %><% } %>
<%= renderTemplate("_header.html.template") %>
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#18bcf2" />
<link
+1 -4
View File
@@ -1,7 +1,6 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "../components/animation/ha-fade-in";
import "../components/ha-top-app-bar-fixed";
import "../components/ha-spinner";
import type { HomeAssistant } from "../types";
@@ -37,9 +36,7 @@ class HassLoadingScreen extends LitElement {
private _renderContent(): TemplateResult {
return html`
<div class="content">
<ha-fade-in .delay=${500}>
<ha-spinner></ha-spinner>
</ha-fade-in>
<ha-spinner></ha-spinner>
${
this.message
? html`<div id="loading-text">${this.message}</div>`
-57
View File
@@ -1,5 +1,3 @@
import { ContextProvider, createContext } from "@lit/context";
import type { ReactiveController, ReactiveControllerHost } from "lit";
import { ReactiveElement } from "lit";
import { fireEvent } from "../common/dom/fire_event";
@@ -17,61 +15,6 @@ export const panelIsReady = async (element: HTMLElement) => {
fireEvent(element, "hass-panel-ready");
};
export type RegisterChildPanelReady = (ready: Promise<void>) => void;
export const childPanelReadyContext =
createContext<RegisterChildPanelReady>("child-panel-ready");
export class ChildPanelReady implements ReactiveController {
private _promises: Promise<void>[] = [];
private _host: ReactiveControllerHost & HTMLElement;
private _resolveReady?: () => void;
private _completing = false;
public ready = new Promise<void>((resolve) => {
this._resolveReady = resolve;
});
public constructor(host: ReactiveControllerHost & HTMLElement) {
this._host = host;
host.addController(this);
new ContextProvider(host, {
context: childPanelReadyContext,
initialValue: (ready) => this._promises.push(ready),
});
}
public hostUpdated() {
if (this._completing) {
return;
}
this._completing = true;
this._host.removeController(this);
void this._complete();
}
private async _complete() {
// Children created/updated during this render register after the host's
// update commits. Wait for that before snapshotting readiness promises.
await this._host.updateComplete;
await this._waitForPromises();
this._resolveReady?.();
await panelIsReady(this._host);
}
private _waitForPromises(): Promise<void> {
const count = this._promises.length;
return Promise.allSettled(this._promises).then(() =>
this._promises.length > count ? this._waitForPromises() : undefined
);
}
}
export class PanelReady {
public ready?: Promise<void>;
+28 -67
View File
@@ -19,66 +19,29 @@ import { HassRouterPage } from "./hass-router-page";
const CACHE_URL_PATHS = ["lovelace", "home", "config"];
const PANEL_READY_TIMEOUT = 2000;
const DASHBOARD_READY_TIMEOUT = 5000;
const COMPONENTS = {
app: { load: () => import("../panels/app/ha-panel-app") },
energy: {
load: () => import("../panels/energy/ha-panel-energy"),
waitForReady: true,
readyTimeout: DASHBOARD_READY_TIMEOUT,
},
calendar: {
load: () => import("../panels/calendar/ha-panel-calendar"),
waitForReady: true,
},
config: { load: () => import("../panels/config/ha-panel-config") },
custom: { load: () => import("../panels/custom/ha-panel-custom") },
lovelace: {
load: () => import("../panels/lovelace/ha-panel-lovelace"),
waitForReady: true,
readyTimeout: DASHBOARD_READY_TIMEOUT,
},
history: { load: () => import("../panels/history/ha-panel-history") },
iframe: { load: () => import("../panels/iframe/ha-panel-iframe") },
logbook: { load: () => import("../panels/logbook/ha-panel-logbook") },
map: { load: () => import("../panels/map/ha-panel-map") },
my: { load: () => import("../panels/my/ha-panel-my") },
profile: { load: () => import("../panels/profile/ha-panel-profile") },
todo: { load: () => import("../panels/todo/ha-panel-todo") },
"media-browser": {
load: () => import("../panels/media-browser/ha-panel-media-browser"),
waitForReady: true,
},
light: {
load: () => import("../panels/light/ha-panel-light"),
waitForReady: true,
readyTimeout: DASHBOARD_READY_TIMEOUT,
},
security: {
load: () => import("../panels/security/ha-panel-security"),
waitForReady: true,
readyTimeout: DASHBOARD_READY_TIMEOUT,
},
climate: {
load: () => import("../panels/climate/ha-panel-climate"),
waitForReady: true,
readyTimeout: DASHBOARD_READY_TIMEOUT,
},
maintenance: {
load: () => import("../panels/maintenance/ha-panel-maintenance"),
waitForReady: true,
readyTimeout: DASHBOARD_READY_TIMEOUT,
},
home: {
load: () => import("../panels/home/ha-panel-home"),
waitForReady: true,
readyTimeout: DASHBOARD_READY_TIMEOUT,
},
notfound: { load: () => import("../panels/notfound/ha-panel-notfound") },
} satisfies Record<
string,
Pick<RouteOptions, "load" | "waitForReady"> & { readyTimeout?: number }
>;
app: () => import("../panels/app/ha-panel-app"),
energy: () => import("../panels/energy/ha-panel-energy"),
calendar: () => import("../panels/calendar/ha-panel-calendar"),
config: () => import("../panels/config/ha-panel-config"),
custom: () => import("../panels/custom/ha-panel-custom"),
lovelace: () => import("../panels/lovelace/ha-panel-lovelace"),
history: () => import("../panels/history/ha-panel-history"),
iframe: () => import("../panels/iframe/ha-panel-iframe"),
logbook: () => import("../panels/logbook/ha-panel-logbook"),
map: () => import("../panels/map/ha-panel-map"),
my: () => import("../panels/my/ha-panel-my"),
profile: () => import("../panels/profile/ha-panel-profile"),
todo: () => import("../panels/todo/ha-panel-todo"),
"media-browser": () =>
import("../panels/media-browser/ha-panel-media-browser"),
light: () => import("../panels/light/ha-panel-light"),
security: () => import("../panels/security/ha-panel-security"),
climate: () => import("../panels/climate/ha-panel-climate"),
maintenance: () => import("../panels/maintenance/ha-panel-maintenance"),
home: () => import("../panels/home/ha-panel-home"),
notfound: () => import("../panels/notfound/ha-panel-notfound"),
};
@customElement("partial-panel-resolver")
class PartialPanelResolver extends HassRouterPage {
@@ -163,12 +126,13 @@ class PartialPanelResolver extends HassRouterPage {
private _getRoutes(panels: Panels): RouterOptions {
const routes: RouterOptions["routes"] = {};
Object.values(panels).forEach((panel) => {
const component = COMPONENTS[panel.component_name];
const data: RouteOptions = {
tag: `ha-panel-${panel.component_name}`,
cache: CACHE_URL_PATHS.includes(panel.url_path),
...(component ?? {}),
};
if (panel.component_name in COMPONENTS) {
data.load = COMPONENTS[panel.component_name];
}
routes[panel.url_path] = data;
});
@@ -257,12 +221,9 @@ class PartialPanelResolver extends HassRouterPage {
)
) {
await this.rebuild();
const component =
COMPONENTS[this.hass.panels[this._currentPage].component_name];
await promiseTimeout(
component?.readyTimeout ?? PANEL_READY_TIMEOUT,
this.pageRendered
).catch(() => undefined);
await promiseTimeout(PANEL_READY_TIMEOUT, this.pageRendered).catch(
() => undefined
);
// Only fire frontend/loaded when this call actually removed the launch
// screen, so later panel updates do not fire it again. Native apps remove
// it instantly because their own splash screen is still visible.
+48 -64
View File
@@ -1,23 +1,14 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
fireEvent,
type HASSDomCurrentTargetEvent,
} from "../../common/dom/fire_event";
import "../../components/ha-button";
import "../../components/ha-dialog";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-dialog-footer";
import "../../components/radio/ha-radio-group";
import type { HaRadioGroup } from "../../components/radio/ha-radio-group";
import "../../components/radio/ha-radio-option";
import "../../components/ha-svg-icon";
import "../../components/ha-switch";
import "../../components/ha-dialog";
import { RecurrenceRange } from "../../data/calendar";
import type { HomeAssistant } from "../../types";
import type { ConfirmEventDialogBoxParams } from "./show-confirm-event-dialog-box";
// `RecurrenceRange.THISEVENT` is "", which the radio group treats as unselected
// on its value-attribute and form-reset paths, so keep the options on their own
// literals and map them when confirming.
type RecurrenceScope = "this" | "future";
import "../../components/ha-button";
@customElement("confirm-event-dialog-box")
class ConfirmEventDialogBox extends LitElement {
@@ -27,14 +18,11 @@ class ConfirmEventDialogBox extends LitElement {
@state() private _open = false;
@state() private _closeState?: "canceled" | "confirmed";
@state() private _scope: RecurrenceScope = "this";
@state()
private _closeState?: "canceled" | "confirmed" | "confirmedFuture";
public async showDialog(params: ConfirmEventDialogBoxParams): Promise<void> {
this._params = params;
this._scope = "this";
this._closeState = undefined;
this._open = true;
}
@@ -51,27 +39,20 @@ class ConfirmEventDialogBox extends LitElement {
return nothing;
}
const { destructive, recurring } = this._params;
return html`
<ha-dialog
.open=${this._open}
header-title=${this._params.title}
width="small"
type="alert"
aria-describedby=${recurring ? nothing : "description"}
@closed=${this._dialogClosed}
>
${
recurring
? this._renderRecurrenceRange()
: html`<p id="description">${this._params.text}</p>`
}
<div>
<p>${this._params.text}</p>
</div>
<ha-dialog-footer slot="footer">
<ha-button
appearance="plain"
@click=${this._dismiss}
?autofocus=${!recurring && destructive}
slot="secondaryAction"
>
${this.hass.localize("ui.common.cancel")}
@@ -79,41 +60,29 @@ class ConfirmEventDialogBox extends LitElement {
<ha-button
slot="primaryAction"
@click=${this._confirm}
?autofocus=${!recurring && !destructive}
variant=${destructive ? "danger" : "brand"}
autofocus
variant="danger"
>
${this._params.confirmText}
</ha-button>
${
this._params.confirmFutureText
? html`
<ha-button
@click=${this._confirmFuture}
slot="primaryAction"
variant="danger"
>
${this._params.confirmFutureText}
</ha-button>
`
: ""
}
</ha-dialog-footer>
</ha-dialog>
`;
}
private _renderRecurrenceRange() {
const thisEvent = this.hass.localize(
"ui.components.calendar.event.recurrence_range.this_event"
);
const thisAndFuture = this.hass.localize(
"ui.components.calendar.event.recurrence_range.this_and_future"
);
return html`
<ha-radio-group
name="recurrence_range"
.label=${this._params!.text ?? this._params!.title}
.value=${this._scope}
@change=${this._scopeChanged}
>
<ha-radio-option value="this" autofocus>${thisEvent}</ha-radio-option>
<ha-radio-option value="future">${thisAndFuture}</ha-radio-option>
</ha-radio-group>
`;
}
private _scopeChanged(ev: HASSDomCurrentTargetEvent<HaRadioGroup>): void {
this._scope = ev.currentTarget.value as RecurrenceScope;
}
private _dismiss(): void {
this._closeState = "canceled";
this.closeDialog();
@@ -121,11 +90,17 @@ class ConfirmEventDialogBox extends LitElement {
private _confirm(): void {
this._closeState = "confirmed";
this._params!.confirm?.(
this._scope === "future"
? RecurrenceRange.THISANDFUTURE
: RecurrenceRange.THISEVENT
);
if (this._params!.confirm) {
this._params!.confirm(RecurrenceRange.THISEVENT);
}
this.closeDialog();
}
private _confirmFuture(): void {
this._closeState = "confirmedFuture";
if (this._params!.confirm) {
this._params!.confirm(RecurrenceRange.THISANDFUTURE);
}
this.closeDialog();
}
@@ -133,7 +108,10 @@ class ConfirmEventDialogBox extends LitElement {
if (!this._params) {
return;
}
if (this._closeState !== "confirmed") {
if (
this._closeState !== "confirmed" &&
this._closeState !== "confirmedFuture"
) {
this._params.cancel?.();
}
this._params = undefined;
@@ -147,12 +125,18 @@ class ConfirmEventDialogBox extends LitElement {
pointer-events: initial !important;
cursor: initial !important;
}
a {
color: var(--primary-color);
}
p {
margin: 0;
color: var(--primary-text-color);
}
ha-radio-group::part(form-control-label) {
font-weight: var(--ha-font-weight-medium);
.no-bottom-padding {
padding-bottom: 0;
}
.secondary {
color: var(--secondary-text-color);
}
ha-dialog {
/* Place above other dialogs */
@@ -224,9 +224,18 @@ class DialogCalendarEventDetail extends LitElement {
: this.hass.localize(
"ui.components.calendar.event.confirm_delete.prompt"
),
confirmText: this.hass.localize("ui.common.delete"),
recurring: !!entry.recurrence_id,
destructive: true,
confirmText: entry.recurrence_id
? this.hass.localize(
"ui.components.calendar.event.confirm_delete.delete_this"
)
: this.hass.localize(
"ui.components.calendar.event.confirm_delete.delete"
),
confirmFutureText: entry.recurrence_id
? this.hass.localize(
"ui.components.calendar.event.confirm_delete.delete_future"
)
: undefined,
});
if (range === undefined) {
// Cancel
@@ -569,8 +569,12 @@ class DialogCalendarEventEditor extends DirtyStateProviderMixin<CalendarEventFor
text: this.hass.localize(
"ui.components.calendar.event.confirm_update.recurring_prompt"
),
confirmText: this.hass.localize("ui.common.update"),
recurring: true,
confirmText: this.hass.localize(
"ui.components.calendar.event.confirm_update.update_this"
),
confirmFutureText: this.hass.localize(
"ui.components.calendar.event.confirm_update.update_future"
),
});
}
if (range === undefined) {
@@ -621,9 +625,18 @@ class DialogCalendarEventEditor extends DirtyStateProviderMixin<CalendarEventFor
: this.hass.localize(
"ui.components.calendar.event.confirm_delete.prompt"
),
confirmText: this.hass.localize("ui.common.delete"),
recurring: !!entry.recurrence_id,
destructive: true,
confirmText: entry.recurrence_id
? this.hass.localize(
"ui.components.calendar.event.confirm_delete.delete_this"
)
: this.hass.localize(
"ui.components.calendar.event.confirm_delete.delete"
),
confirmFutureText: entry.recurrence_id
? this.hass.localize(
"ui.components.calendar.event.confirm_delete.delete_future"
)
: undefined,
});
if (range === undefined) {
// Cancel
-7
View File
@@ -35,7 +35,6 @@ import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
import { subscribeEntityRegistry } from "../../data/entity/entity_registry";
import { fetchIntegrationManifest } from "../../data/integration";
import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow";
import { panelIsReady } from "../../layouts/panel-ready";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { haStyle } from "../../resources/styles";
import type { CalendarViewChanged, HomeAssistant } from "../../types";
@@ -78,8 +77,6 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
private _mql?: MediaQueryList;
private _initialReady = false;
public connectedCallback() {
super.connectedCallback();
this._mql = window.matchMedia(
@@ -106,10 +103,6 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
this._entityRegistry = entities;
// Refresh calendars when entity registry updates (includes color changes)
this._calendars = getCalendars(this.hass, this, this._entityRegistry);
if (!this._initialReady) {
this._initialReady = true;
panelIsReady(this);
}
// Resubscribe events if view dates are available (handles both initial load and color updates)
if (this._start && this._end) {
this._unsubscribeAll().then(() => {
@@ -1,16 +1,14 @@
import type { TemplateResult } from "lit";
import { fireEvent } from "../../common/dom/fire_event";
import type { RecurrenceRange } from "../../data/calendar";
export interface ConfirmEventDialogBoxParams {
title: string;
// Labels the recurrence range options when `recurring` is set
text?: string;
confirmText?: string;
// Let the user pick which occurrences of a recurring event are affected
recurring?: boolean;
destructive?: boolean;
confirmFutureText?: string; // Prompt for future recurring events
confirm?: (recurrenceRange: RecurrenceRange) => void;
cancel?: () => void;
text?: string | TemplateResult;
title: string;
}
export const loadGenericDialog = () => import("./confirm-event-dialog-box");
-4
View File
@@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators";
import { debounce } from "../../common/util/debounce";
import { deepEqual } from "../../common/util/deep-equal";
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
import { ChildPanelReady } from "../../layouts/panel-ready";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
@@ -32,8 +31,6 @@ class PanelClimate extends LitElement {
@state() private _searchParams = new URLSearchParams(window.location.search);
private _childPanelReady?: ChildPanelReady;
public willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
// Initial setup
@@ -130,7 +127,6 @@ class PanelClimate extends LitElement {
return;
}
this._childPanelReady ??= new ChildPanelReady(this);
this._lovelace = {
config: config,
rawConfig: rawConfig,
@@ -36,7 +36,6 @@ import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar";
import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart";
import { showShortcutsDialog } from "../../../dialogs/shortcuts/show-shortcuts-dialog";
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import { ChildPanelReady } from "../../../layouts/panel-ready";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
@@ -158,11 +157,6 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
total: 0,
};
public constructor() {
super();
new ChildPanelReady(this);
}
private _pages = memoizeOne(
(
cloudStatus,
@@ -1,18 +1,12 @@
import { consume } from "@lit/context";
import type { CSSResultGroup, TemplateResult } from "lit";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { filterNavigationPages } from "../../../common/config/filter_navigation_pages";
import "../../../components/ha-card";
import "../../../components/ha-icon-next";
import type { CloudStatus } from "../../../data/cloud";
import { getConfigEntries } from "../../../data/config_entries";
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import {
childPanelReadyContext,
type RegisterChildPanelReady,
} from "../../../layouts/panel-ready";
import type { HomeAssistant } from "../../../types";
import "../components/ha-config-navigation-list";
@@ -24,53 +18,21 @@ class HaConfigNavigation extends LitElement {
@property({ attribute: false }) public pages!: PageNavigation[];
@state() private _visiblePages?: PageNavigation[];
@state() private _hasBluetoothConfigEntries = false;
private _hasBluetoothConfigEntries = false;
private _bluetoothEntriesLoaded?: Promise<void>;
private _childReadyRegistered = false;
private _filterNavigationPages = memoizeOne(
(
hass: HomeAssistant,
pages: PageNavigation[],
hasBluetoothConfigEntries: boolean
) => filterNavigationPages(hass, pages, { hasBluetoothConfigEntries })
);
@consume({ context: childPanelReadyContext, subscribe: true })
private _registerChildPanelReady?: RegisterChildPanelReady;
protected override updated(changedProps: Map<PropertyKey, unknown>) {
super.updated(changedProps);
const pagesOrHassChanged =
changedProps.has("pages") || changedProps.has("hass");
if (pagesOrHassChanged) {
const ready = this._resolveVisiblePages();
if (!this._childReadyRegistered && this._registerChildPanelReady) {
this._registerChildPanelReady(ready);
this._childReadyRegistered = true;
}
return;
}
if (
!this._childReadyRegistered &&
this._registerChildPanelReady &&
this.pages &&
this.hass
) {
this._registerChildPanelReady(this._resolveVisiblePages());
this._childReadyRegistered = true;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
super.firstUpdated(changedProps);
getConfigEntries(this.hass, {
domain: "bluetooth",
}).then((bluetoothEntries) => {
this._hasBluetoothConfigEntries = bluetoothEntries.length > 0;
});
}
protected render(): TemplateResult {
const pages = (this._visiblePages ?? []).map((page) => ({
const pages = filterNavigationPages(this.hass, this.pages, {
hasBluetoothConfigEntries: this._hasBluetoothConfigEntries,
}).map((page) => ({
...page,
name:
page.name ||
@@ -113,42 +75,6 @@ class HaConfigNavigation extends LitElement {
`;
}
private _loadBluetoothEntries(): Promise<void> {
if (!this._bluetoothEntriesLoaded) {
this._bluetoothEntriesLoaded = getConfigEntries(this.hass, {
domain: "bluetooth",
})
.then((entries) => {
this._hasBluetoothConfigEntries = entries.length > 0;
})
.catch(() => {
this._hasBluetoothConfigEntries = false;
});
}
return this._bluetoothEntriesLoaded;
}
private async _resolveVisiblePages(): Promise<void> {
if (this.pages.some((page) => page.component === "bluetooth")) {
await this._loadBluetoothEntries();
}
const visiblePages = this._filterNavigationPages(
this.hass,
this.pages,
this._hasBluetoothConfigEntries
);
const currentVisiblePages = this._visiblePages;
if (
!currentVisiblePages ||
visiblePages.length !== currentVisiblePages.length ||
visiblePages.some((page, index) => page !== currentVisiblePages[index])
) {
this._visiblePages = visiblePages;
}
await this.updateComplete;
}
static styles: CSSResultGroup = css`
/* Accessibility */
.visually-hidden {
-1
View File
@@ -89,7 +89,6 @@ class HaPanelConfig extends HassRouterPage {
dashboard: {
tag: "ha-config-dashboard",
load: () => import("./dashboard/ha-config-dashboard"),
waitForReady: true,
},
entities: {
tag: "ha-config-entities",
@@ -19,6 +19,8 @@ import { animationStyles } from "../../../../../resources/theme/animations.globa
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/buttons/ha-progress-button";
import type { HaProgressButton } from "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-md-list";
@@ -67,6 +69,8 @@ class ZHAConfigDashboard extends LitElement {
@state() private _error?: string;
@state() private _generatingBackup = false;
protected firstUpdated(changedProperties: PropertyValues<this>) {
super.firstUpdated(changedProperties);
if (!this.hass) {
@@ -355,17 +359,18 @@ class ZHAConfigDashboard extends LitElement {
"ui.panel.config.zha.configuration_page.download_backup_description"
)}
</span>
<ha-button
<ha-progress-button
appearance="plain"
slot="end"
size="s"
.iconPath=${mdiDownload}
.progress=${this._generatingBackup}
@click=${this._createAndDownloadBackup}
>
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.zha.configuration_page.download_backup_action"
)}
</ha-button>
</ha-progress-button>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">
@@ -408,30 +413,29 @@ class ZHAConfigDashboard extends LitElement {
this._configuration = await fetchZHAConfiguration(this.hass!);
}
private async _createAndDownloadBackup(): Promise<void> {
private async _createAndDownloadBackup(ev: Event): Promise<void> {
const button = ev.currentTarget as HaProgressButton;
let backup_and_metadata: ZHANetworkBackupAndMetadata;
// Reading the backup from the coordinator can take 5-30 seconds.
this._generatingBackup = true;
try {
backup_and_metadata = await createZHANetworkBackup(this.hass!);
} catch (err: any) {
button.actionError();
showAlertDialog(this, {
title: "Failed to create backup",
title: this.hass.localize(
"ui.panel.config.zha.configuration_page.backup_failed"
),
text: err.message,
warning: true,
});
return;
} finally {
this._generatingBackup = false;
}
if (!backup_and_metadata.is_complete) {
await showAlertDialog(this, {
title: "Backup is incomplete",
text: "A backup has been created but it is incomplete and cannot be restored. This is a coordinator firmware limitation.",
});
}
const backupJSON: string =
"data:text/plain;charset=utf-8," +
encodeURIComponent(JSON.stringify(backup_and_metadata.backup, null, 4));
const backupTime: Date = new Date(
Date.parse(backup_and_metadata.backup.backup_time)
);
@@ -441,7 +445,23 @@ class ZHAConfigDashboard extends LitElement {
basename = `Incomplete ${basename}`;
}
fileDownload(backupJSON, `${basename}.json`);
const blob = new Blob(
[JSON.stringify(backup_and_metadata.backup, null, 4)],
{ type: "application/json" }
);
fileDownload(URL.createObjectURL(blob), `${basename}.json`);
button.actionSuccess();
if (!backup_and_metadata.is_complete) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.zha.configuration_page.backup_incomplete_title"
),
text: this.hass.localize(
"ui.panel.config.zha.configuration_page.backup_incomplete_text"
),
});
}
}
private _openOptionFlow() {
@@ -517,10 +537,6 @@ class ZHAConfigDashboard extends LitElement {
--md-item-overflow: visible;
}
ha-button[size="s"] ha-svg-icon {
--mdc-icon-size: 16px;
}
.network-status div.heading {
display: flex;
align-items: center;
-4
View File
@@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators";
import { debounce } from "../../common/util/debounce";
import { deepEqual } from "../../common/util/deep-equal";
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
import { ChildPanelReady } from "../../layouts/panel-ready";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
@@ -32,8 +31,6 @@ class PanelLight extends LitElement {
@state() private _searchParms = new URLSearchParams(window.location.search);
private _childPanelReady?: ChildPanelReady;
public willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
// Initial setup
@@ -130,7 +127,6 @@ class PanelLight extends LitElement {
return;
}
this._childPanelReady ??= new ChildPanelReady(this);
this._lovelace = {
config: config,
rawConfig: rawConfig,
@@ -3,7 +3,6 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/animation/ha-fade-in";
import "../../../components/ha-card";
import "../../../components/ha-spinner";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
@@ -39,9 +38,7 @@ export class HuiStartingCard extends LitElement implements LovelaceCard {
return html`
<div class="content">
<ha-fade-in .delay=${500}>
<ha-spinner></ha-spinner>
</ha-fade-in>
<ha-spinner></ha-spinner>
${this.hass.localize("ui.panel.lovelace.cards.starting.description")}
</div>
`;
@@ -1,6 +1,5 @@
import { migrateStateColorConfig } from "../common/entity-color-config";
import { migrateTimeFormatConfig } from "../common/entity-time-format-config";
import { migrateSecondaryInfoConfig } from "../common/entity-secondary-info-config";
import type {
ConditionalRowConfig,
LovelaceRowConfig,
@@ -21,13 +20,11 @@ const migrateEntitiesRowConfig = (
let newConf: LovelaceRowConfig = rowConf;
newConf = migrateTimeFormatConfig(newConf as EntitiesCardEntityConfig);
newConf = migrateStateColorConfig(newConf as EntitiesCardEntityConfig);
newConf = migrateSecondaryInfoConfig(newConf as EntitiesCardEntityConfig);
if (newConf.type === "conditional") {
const row = (newConf as ConditionalRowConfig).row;
if (row && typeof row === "object") {
let newRow = migrateTimeFormatConfig(row as EntitiesCardEntityConfig);
newRow = migrateStateColorConfig(newRow);
newRow = migrateSecondaryInfoConfig(newRow);
if (newRow !== row) {
newConf = { ...newConf, row: newRow } as ConditionalRowConfig;
}
+10 -1
View File
@@ -91,7 +91,16 @@ export interface EntityCardConfig extends LovelaceCardConfig {
export interface EntitiesCardEntityConfig extends EntityConfig {
type?: string;
secondary_info?: string | string[];
secondary_info?:
| "entity-id"
| "last-changed"
| "last-triggered"
| "last-updated"
| "area"
| "position"
| "state"
| "tilt-position"
| "brightness";
action_name?: string;
action?: string;
/** @deprecated use "action" instead */
@@ -1,40 +0,0 @@
import { isCustomType } from "../../../data/lovelace_custom_cards";
interface LegacySecondaryInfoConfig {
type?: string;
secondary_info?: string | string[];
}
const SUBSTITUTIONS = {
"last-changed": "last_changed",
"last-updated": "last_updated",
"last-triggered": "last_triggered",
position: "current_position",
"tilt-position": "current_tilt_position",
area: "area_name",
none: undefined,
};
export const migrateSecondaryInfoConfig = <T extends LegacySecondaryInfoConfig>(
config: T
): T => {
// Custom elements own their config schema and may use the same option with
// a different meaning, leave them untouched
if (config.type !== undefined && isCustomType(config.type)) {
return config;
}
if (
config.secondary_info === undefined ||
typeof config.secondary_info !== "string" ||
!(config.secondary_info in SUBSTITUTIONS)
) {
return config;
}
const { secondary_info, ...rest } = config;
return {
...rest,
...(SUBSTITUTIONS[secondary_info] && {
secondary_info: SUBSTITUTIONS[secondary_info],
}),
} as T;
};
@@ -4,9 +4,13 @@ import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { DOMAINS_INPUT_ROW } from "../../../common/const";
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";
import "../../../components/ha-tooltip";
@@ -17,7 +21,6 @@ import { actionHandler } from "../common/directives/action-handler-directive";
import { handleAction } from "../common/handle-action";
import { hasAction, hasAnyAction } from "../common/has-action";
import { createEntityNotFoundWarning } from "./hui-warning";
import "../../../state-display/state-display";
@customElement("hui-generic-entity-row")
export class HuiGenericEntityRow extends LitElement {
@@ -37,6 +40,8 @@ export class HuiGenericEntityRow extends LitElement {
@property({ attribute: "catch-interaction", type: Boolean })
public catchInteraction?;
private _secondaryInfoElementId = "-" + uid();
protected render() {
if (!this.hass || !this.config) {
return nothing;
@@ -96,15 +101,118 @@ export class HuiGenericEntityRow extends LitElement {
<div class="secondary">
${
this.secondaryText ||
html`<state-display
.stateObj=${stateObj}
.hass=${this.hass}
.content=${this.config.secondary_info}
.timeFormat=${this.config.time_format}
.name=${name}
timestamp-tooltip
>
</state-display>`
(this.config.secondary_info === "entity-id"
? stateObj.entity_id
: this.config.secondary_info === "last-changed"
? html`
<ha-tooltip
for="last-changed${
this._secondaryInfoElementId
}"
placement="right"
>
${formatDateTimeWithSeconds(
new Date(stateObj.last_changed),
this.hass.locale,
this.hass.config
)}
</ha-tooltip>
<ha-relative-time
id="last-changed${this._secondaryInfoElementId}"
.datetime=${stateObj.last_changed}
capitalize
></ha-relative-time>
`
: this.config.secondary_info === "last-updated"
? html`
<ha-tooltip
for="last-updated${
this._secondaryInfoElementId
}"
placement="right"
>
${formatDateTimeWithSeconds(
new Date(stateObj.last_updated),
this.hass.locale,
this.hass.config
)}
</ha-tooltip>
<ha-relative-time
id="last-updated${
this._secondaryInfoElementId
}"
.datetime=${stateObj.last_updated}
capitalize
></ha-relative-time>
`
: this.config.secondary_info ===
"last-triggered"
? stateObj.attributes.last_triggered
? html`
<ha-tooltip
for="last-triggered${
this._secondaryInfoElementId
}"
placement="right"
>
${formatDateTimeWithSeconds(
new Date(
stateObj.attributes
.last_triggered
),
this.hass.locale,
this.hass.config
)}
</ha-tooltip>
<ha-relative-time
id="last-triggered${
this._secondaryInfoElementId
}"
.datetime=${
stateObj.attributes.last_triggered
}
capitalize
></ha-relative-time>
`
: this.hass.localize(
"ui.panel.lovelace.cards.entities.never_triggered"
)
: this.config.secondary_info ===
"position" &&
stateObj.attributes.current_position !==
undefined
? `${this.hass.localize(
"ui.card.cover.position"
)}: ${stateObj.attributes.current_position}`
: this.config.secondary_info ===
"tilt-position" &&
stateObj.attributes
.current_tilt_position !== undefined
? `${this.hass.localize(
"ui.card.cover.tilt_position"
)}: ${
stateObj.attributes
.current_tilt_position
}`
: this.config.secondary_info ===
"brightness" &&
stateObj.attributes.brightness
? html`${Math.round(
(stateObj.attributes.brightness /
255) *
100
)}
%`
: this.config.secondary_info ===
"state"
? html`${this.hass.formatEntityState(
stateObj
)}`
: this.config.secondary_info ===
"area"
? (this._getArea(stateObj) ??
nothing)
: nothing)
}
</div>
`
@@ -145,6 +253,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;
@@ -10,7 +10,6 @@ import {
import {
formatDateTime,
formatDateTimeNumeric,
formatDateTimeWithSeconds,
} from "../../../common/datetime/format_date_time";
import {
formatTime,
@@ -30,8 +29,6 @@ import type {
} from "../../../types";
import type { LocalizeFunc } from "../../../common/translations/localize";
import type { TimestampRenderingFormat } from "./types";
import { uid } from "../../../common/util/uid";
import "../../../components/ha-tooltip";
const FORMATS: Record<
string,
@@ -63,8 +60,6 @@ class HuiTimestampDisplay extends LitElement {
@property({ type: Boolean }) public capitalize = false;
@property({ type: Boolean }) public tooltip = false;
@state() private _relative?: string;
@state()
@@ -92,8 +87,6 @@ class HuiTimestampDisplay extends LitElement {
private _interval?: number;
private _uid = "timestamp-display-" + uid();
public connectedCallback(): void {
super.connectedCallback();
this._connected = true;
@@ -121,14 +114,6 @@ class HuiTimestampDisplay extends LitElement {
const formatType = this._formatType;
if (INTERVAL_FORMAT.includes(formatType)) {
if (this.tooltip) {
return html`
<ha-tooltip for=${this._uid} placement="right">
${formatDateTimeWithSeconds(this.ts, this._locale, this._config)}
</ha-tooltip>
<span id=${this._uid}> ${this._relative} </span>
`;
}
return html` ${this._relative} `;
}
if (formatType in FORMATS) {
@@ -138,20 +123,9 @@ class HuiTimestampDisplay extends LitElement {
: format.style && format.style in FORMATS[formatType]
? format.style
: "default";
const formatted = FORMATS[formatType][style](
this.ts,
this._locale,
this._config
);
if (this.tooltip) {
return html`
<ha-tooltip for=${this._uid} placement="right">
${formatDateTimeWithSeconds(this.ts, this._locale, this._config)}
</ha-tooltip>
<span id=${this._uid}> ${formatted} </span>
`;
}
return html` ${formatted} `;
return html`
${FORMATS[formatType][style](this.ts, this._locale, this._config)}
`;
}
return html`${this._localize(
"ui.panel.lovelace.components.timestamp-display.invalid_format"
@@ -4,8 +4,12 @@ import memoizeOne from "memoize-one";
import { assert } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDomain } from "../../../../common/entity/compute_domain";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import { SENSOR_TIMESTAMP_DEVICE_CLASSES } from "../../../../data/sensor";
import type { EntitiesCardEntityConfig } from "../../cards/types";
@@ -13,7 +17,19 @@ import type { LovelaceRowEditor } from "../../types";
import { entitiesConfigStruct } from "../structs/entities-struct";
import { DOMAIN_TO_ELEMENT_TYPE } from "../../create-element/create-row-element";
import { TIMESTAMP_STATE_DOMAINS } from "../../../../common/const";
import { stateContentHasTimestamp } from "../../../../state-display/state-display";
const SECONDARY_INFO_VALUES = {
none: {},
"entity-id": {},
"last-changed": {},
"last-updated": {},
"last-triggered": { domains: ["automation", "script"] },
area: {},
position: { domains: ["cover"] },
state: {},
"tilt-position": { domains: ["cover"] },
brightness: { domains: ["light"] },
};
@customElement("hui-generic-entity-row-editor")
export class HuiGenericEntityRowEditor
@@ -31,87 +47,96 @@ export class HuiGenericEntityRowEditor
this._config = config;
}
private _schema = memoizeOne((showTimeFormat?: boolean) => {
return [
{ name: "entity", required: true, selector: { entity: {} } },
{
name: "name",
selector: { entity_name: {} },
context: { entity: "entity" },
},
{
name: "",
type: "grid",
schema: [
{
name: "icon",
selector: {
icon: {},
},
context: {
icon_entity: "entity",
},
},
{
name: "color",
selector: {
ui_color: {
include_state: true,
include_none: true,
private _schema = memoizeOne(
(entity: string, localize: LocalizeFunc, showTimeFormat?: boolean) => {
const domain = computeDomain(entity);
return [
{ name: "entity", required: true, selector: { entity: {} } },
{
name: "name",
selector: { entity_name: {} },
context: { entity: "entity" },
},
{
name: "",
type: "grid",
schema: [
{
name: "icon",
selector: {
icon: {},
},
context: {
icon_entity: "entity",
},
},
},
],
},
{
name: "secondary_info",
selector: {
ui_state_content: {
allow_context: true,
{
name: "color",
selector: {
ui_color: {
include_state: true,
include_none: true,
},
},
},
],
},
{
name: "secondary_info",
selector: {
select: {
options: (
Object.keys(SECONDARY_INFO_VALUES).filter(
(info) =>
!("domains" in SECONDARY_INFO_VALUES[info]) ||
("domains" in SECONDARY_INFO_VALUES[info] &&
SECONDARY_INFO_VALUES[info].domains!.includes(domain))
) as (keyof typeof SECONDARY_INFO_VALUES)[]
).map((info) => ({
value: info,
label: localize(
`ui.panel.lovelace.editor.card.entities.secondary_info_values.${info}`
),
})),
},
},
},
context: {
filter_entity: "entity",
},
},
{
name: "time_format",
visible: showTimeFormat,
selector: {
ui_time_format: {},
},
},
] as const;
});
...(showTimeFormat
? ([
{
name: "time_format",
selector: {
ui_time_format: {},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
] as const;
}
);
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
let schema = this.schema;
if (!schema) {
const entity = this._config.entity;
const domain = entity ? computeDomain(entity) : "";
const simpleEntity =
(DOMAIN_TO_ELEMENT_TYPE[domain] ||
DOMAIN_TO_ELEMENT_TYPE["_domain_not_found"]) === "simple";
const showTimeFormat =
(this._config.secondary_info &&
stateContentHasTimestamp(
entity,
this.hass.states[entity],
this._config.secondary_info
)) ||
(simpleEntity
? TIMESTAMP_STATE_DOMAINS.has(domain)
: domain === "event" ||
(domain === "sensor" &&
SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(
this.hass.states[entity]?.attributes.device_class
)));
schema = this._schema(showTimeFormat);
}
const entity = this._config.entity;
const domain = entity ? computeDomain(entity) : "";
const simpleEntity =
(DOMAIN_TO_ELEMENT_TYPE[domain] ||
DOMAIN_TO_ELEMENT_TYPE["_domain_not_found"]) === "simple";
const showTimeFormat = simpleEntity
? TIMESTAMP_STATE_DOMAINS.has(domain)
: domain === "event" ||
(domain === "sensor" &&
SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(
this.hass.states[entity]?.attributes.device_class
));
const schema =
this.schema ||
this._schema(this._config.entity, this.hass.localize, showTimeFormat);
return html`
<ha-form
@@ -1,4 +1,4 @@
import { array, union, object, string, optional, boolean } from "superstruct";
import { union, object, string, optional, boolean } from "superstruct";
import { timeFormatConfigStruct } from "../../components/types";
import {
actionConfigStruct,
@@ -12,7 +12,7 @@ export const entitiesConfigStruct = union([
name: optional(entityNameStruct),
icon: optional(string()),
image: optional(string()),
secondary_info: optional(union([string(), array(string())])),
secondary_info: optional(string()),
time_format: optional(timeFormatConfigStruct),
color: optional(string()),
tap_action: optional(actionConfigStruct),
@@ -23,7 +23,6 @@ import { hasConfigOrEntityChanged } from "../common/has-changed";
import "../components/hui-generic-entity-row";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { LovelaceRow } from "./types";
import "../../../state-display/state-display";
@customElement("hui-weather-entity-row")
class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
@@ -163,15 +162,25 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
hasSecondary
? html`
<div class="secondary">
<state-display
.stateObj=${stateObj}
.hass=${this.hass}
.content=${this._config.secondary_info}
.timeFormat=${this._config.time_format}
.name=${name}
timestamp-tooltip
>
</state-display>
${
this._config.secondary_info === "entity-id"
? stateObj.entity_id
: this._config.secondary_info === "last-changed"
? html`
<ha-relative-time
.datetime=${stateObj.last_changed}
capitalize
></ha-relative-time>
`
: this._config.secondary_info === "last-updated"
? html`
<ha-relative-time
.datetime=${stateObj.last_updated}
capitalize
></ha-relative-time>
`
: ""
}
</div>
`
: ""
+3 -10
View File
@@ -76,7 +76,6 @@ import { showMoreInfoDialog } from "../../dialogs/more-info/show-ha-more-info-di
import { showQuickBar } from "../../dialogs/quick-bar/show-dialog-quick-bar";
import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
import { haStyle } from "../../resources/styles";
import { ChildPanelReady } from "../../layouts/panel-ready";
import type { HomeAssistant, PanelInfo } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import { isMac } from "../../util/is_mac";
@@ -165,8 +164,6 @@ class HUIRoot extends LitElement {
private _restoreScroll = false;
private _childPanelReady?: ChildPanelReady;
private _undoRedoController = new UndoRedoController<UndoStackItem>(this, {
apply: (config) => this._applyUndoRedo(config),
currentConfig: () => ({
@@ -780,7 +777,7 @@ class HUIRoot extends LitElement {
huiView.narrow = this.narrow;
}
let newSelectView: HUIRoot["_curView"];
let newSelectView;
let viewPath: string | undefined = this.route!.path.split("/")[1];
viewPath = viewPath ? decodeURI(viewPath) : undefined;
@@ -1261,7 +1258,7 @@ class HUIRoot extends LitElement {
return;
}
let view: HUIView;
let view;
const viewConfig = this.config.views[viewIndex];
if (!viewConfig) {
@@ -1272,16 +1269,12 @@ class HUIRoot extends LitElement {
if (this._viewCache[viewIndex]) {
view = this._viewCache[viewIndex];
} else {
if (!this._childPanelReady) {
this._childPanelReady = new ChildPanelReady(this);
this.requestUpdate();
}
view = document.createElement("hui-view");
view.index = viewIndex;
this._viewCache[viewIndex] = view;
}
view.lovelace = this.lovelace!;
view.lovelace = this.lovelace;
view.hass = this.hass;
view.narrow = this.narrow;
+1 -17
View File
@@ -58,12 +58,6 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
private _mqlListenerRef?: () => void;
private _resolveInitialRender?: () => void;
public initialRenderComplete = new Promise<void>((resolve) => {
this._resolveInitialRender = resolve;
});
public connectedCallback() {
super.connectedCallback();
this._initMqls();
@@ -172,13 +166,7 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
root.removeChild(root.lastChild);
}
columns.forEach((column) => {
root.appendChild(column);
});
if (this.cards.length === 0 || columns.some((column) => column.lastChild)) {
this._resolveInitialRender?.();
this._resolveInitialRender = undefined;
}
columns.forEach((column) => root.appendChild(column));
}
private async _createColumns() {
@@ -246,10 +234,6 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
index,
this.lovelace!.editMode
);
if (columnElements.some((column) => column.isConnected)) {
this._resolveInitialRender?.();
this._resolveInitialRender = undefined;
}
}
// Remove empty columns
-26
View File
@@ -1,5 +1,4 @@
import deepClone from "deep-clone-simple";
import { consume } from "@lit/context";
import type { PropertyValues } from "lit";
import { ReactiveElement } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -20,10 +19,6 @@ import type {
} from "../../../data/lovelace/config/view";
import { isStrategyView } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import {
childPanelReadyContext,
type RegisterChildPanelReady,
} from "../../../layouts/panel-ready";
import "../badges/hui-badge";
import type { HuiBadge } from "../badges/hui-badge";
import "../cards/hui-card";
@@ -100,15 +95,6 @@ export class HUIView extends ReactiveElement {
private _config?: LovelaceViewConfig;
private _resolveInitialRender?: () => void;
private _initialRenderComplete = new Promise<void>((resolve) => {
this._resolveInitialRender = resolve;
});
@consume({ context: childPanelReadyContext })
private _registerChildPanelReady?: RegisterChildPanelReady;
@storage({
key: "dashboardCardClipboard",
state: false,
@@ -166,11 +152,6 @@ export class HUIView extends ReactiveElement {
return this;
}
public connectedCallback(): void {
super.connectedCallback();
this._registerChildPanelReady?.(this._initialRenderComplete);
}
public willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
@@ -343,13 +324,6 @@ export class HUIView extends ReactiveElement {
const viewConfig = await this._generateConfig(rawConfig);
this._setConfig(viewConfig, isStrategy);
await customElements.whenDefined(this._layoutElement!.localName);
if (this._layoutElement instanceof ReactiveElement) {
await this._layoutElement.updateComplete;
}
await this._layoutElement!.initialRenderComplete;
this._resolveInitialRender?.();
this._resolveInitialRender = undefined;
}
private _createLayoutElement(config: LovelaceViewConfig): void {
@@ -5,7 +5,6 @@ import { debounce } from "../../common/util/debounce";
import { deepEqual } from "../../common/util/deep-equal";
import "../../components/ha-top-app-bar-fixed";
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
import { ChildPanelReady } from "../../layouts/panel-ready";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
@@ -32,8 +31,6 @@ class PanelMaintenance extends LitElement {
@state() private _searchParams = new URLSearchParams(window.location.search);
private _childPanelReady?: ChildPanelReady;
public willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
// Initial setup
@@ -130,7 +127,6 @@ class PanelMaintenance extends LitElement {
return;
}
this._childPanelReady ??= new ChildPanelReady(this);
this._lovelace = {
config: config,
rawConfig: rawConfig,
-4
View File
@@ -5,7 +5,6 @@ import { debounce } from "../../common/util/debounce";
import { deepEqual } from "../../common/util/deep-equal";
import "../../components/ha-top-app-bar-fixed";
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
import { ChildPanelReady } from "../../layouts/panel-ready";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
@@ -32,8 +31,6 @@ class PanelSecurity extends LitElement {
@state() private _searchParms = new URLSearchParams(window.location.search);
private _childPanelReady?: ChildPanelReady;
public willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
// Initial setup
@@ -130,7 +127,6 @@ class PanelSecurity extends LitElement {
return;
}
this._childPanelReady ??= new ChildPanelReady(this);
this._lovelace = {
config: config,
rawConfig: rawConfig,
+2 -7
View File
@@ -10,6 +10,7 @@ import {
STRINGS_SEPARATOR_DOT,
TIMESTAMP_STATE_DOMAINS,
} from "../common/const";
import "../components/ha-relative-time";
import { UNAVAILABLE, UNKNOWN } from "../data/entity/entity";
import {
SENSOR_TIMESTAMP_DEVICE_CLASSES,
@@ -117,9 +118,6 @@ class StateDisplay extends LitElement {
@property({ attribute: false }) public timeFormat?: string;
@property({ type: Boolean, attribute: "timestamp-tooltip" })
public timestampTooltip = false;
@property({ type: Boolean, attribute: "dash-unavailable" })
public dashUnavailable?: boolean;
@@ -187,9 +185,7 @@ class StateDisplay extends LitElement {
if (content === "name" && this.name) {
return html`${this.name}`;
}
if (content === "entity-id") {
return stateObj.entity_id;
}
if (
content === "device_name" ||
content === "area_name" ||
@@ -218,7 +214,6 @@ class StateDisplay extends LitElement {
.ts=${new Date(relativeDateTime)}
.format=${this.timeFormat}
capitalize
.tooltip=${this.timestampTooltip}
></hui-timestamp-display>`;
}
+9 -6
View File
@@ -1312,18 +1312,18 @@
"invalid_duration": "The duration of the event is not valid. Please check start and end date.",
"not_all_required_fields": "Not all required fields are filled in",
"end_auto_adjusted": "Event end was adjusted to prevent negative duration",
"recurrence_range": {
"this_event": "Only this event",
"this_and_future": "This and all future events"
},
"confirm_delete": {
"delete": "Delete event",
"delete_this": "Delete only this event",
"delete_future": "Delete all future events",
"prompt": "Do you want to delete this event?",
"recurring_prompt": "Which events do you want to delete?"
"recurring_prompt": "Do you want to delete only this event, or this and all future occurrences of the event?"
},
"confirm_update": {
"update": "Update event",
"recurring_prompt": "Which events do you want to update?"
"update_this": "Update only this event",
"update_future": "Update all future events",
"recurring_prompt": "Do you want to update only this event, or this and all future occurrences of the event?"
},
"repeat": {
"label": "Repeat",
@@ -7557,6 +7557,9 @@
"download_backup": "Download backup",
"download_backup_description": "Save your Zigbee network configuration to a file",
"download_backup_action": "Download",
"backup_failed": "Failed to create backup",
"backup_incomplete_title": "Backup is incomplete",
"backup_incomplete_text": "A backup has been created but it is incomplete and cannot be restored. This is a limitation of your adapter's firmware.",
"migrate_radio": "Migrate adapter",
"migrate_radio_description": "Move your Zigbee network to a different adapter",
"migrate_radio_action": "Migrate",
-4
View File
@@ -1,4 +0,0 @@
{
"hashedEntry": "/frontend_(?:latest|es5)/[^/?#]+\\.[0-9a-f]{8,}\\.js",
"moduleError": "dynamically imported module|Importing a module script failed|error loading dynamically imported|ChunkLoadError"
}
+1 -96
View File
@@ -4,7 +4,7 @@
* Run with:
* yarn test:e2e:app
*/
import { test, expect, type Page } from "@playwright/test";
import { test, expect } from "@playwright/test";
import {
appSidebar,
appSidebarConfig,
@@ -161,106 +161,11 @@ test.describe("Quick search", () => {
defineRouteSmokeTests(appRouteSmokeGroups);
const assertInitialReadiness = async (
page: Page,
options: {
path: string;
loadingSelector: string;
resolver:
"rejectMediaBrowse" | "resolveCalendarRegistry" | "resolveMediaBrowse";
readySelector: string;
}
) => {
await goToPanel(page, options.path);
const launchScreen = page.locator("#ha-launch-screen");
await expect(launchScreen).toBeAttached({ timeout: QUICK_TIMEOUT });
await expect(page.locator(options.loadingSelector)).toBeAttached({
timeout: QUICK_TIMEOUT,
});
await page.evaluate((resolver) => window[resolver]?.(), options.resolver);
await expect(page.locator(options.readySelector)).toBeAttached({
timeout: PANEL_TIMEOUT,
});
await expect(launchScreen).not.toBeAttached({ timeout: QUICK_TIMEOUT });
};
test.describe("Initial readiness", () => {
test("keeps the launch screen until calendar content renders", async ({
page,
}) => {
await assertInitialReadiness(page, {
path: "/?scenario=delayed-calendar#/calendar",
loadingSelector: "ha-panel-calendar ha-spinner",
resolver: "resolveCalendarRegistry",
readySelector: "ha-full-calendar",
});
});
test("keeps the launch screen until media content renders", async ({
page,
}) => {
await assertInitialReadiness(page, {
path: "/?scenario=delayed-media-browse#/media-browser/browser",
loadingSelector: "ha-media-player-browse > ha-spinner",
resolver: "resolveMediaBrowse",
readySelector: "ha-media-player-browse .no-items",
});
});
test("keeps the launch screen until a media error renders", async ({
page,
}) => {
await assertInitialReadiness(page, {
path: "/?scenario=delayed-media-browse-error#/media-browser/browser",
loadingSelector: "ha-media-player-browse > ha-spinner",
resolver: "rejectMediaBrowse",
readySelector: "ha-media-player-browse ha-alert",
});
});
});
// ---------------------------------------------------------------------------
// Lovelace
// ---------------------------------------------------------------------------
test.describe("Lovelace dashboard", () => {
test("keeps the launch screen until generated content renders", async ({
page,
}) => {
await goToPanel(page, "/?scenario=delayed-generated-dashboard#/climate");
const launchScreen = page.locator("#ha-launch-screen");
await expect(launchScreen).toBeAttached({ timeout: QUICK_TIMEOUT });
await expect(page.locator("hui-view")).not.toBeAttached();
await page.evaluate(() => window.resolveGeneratedDashboard?.());
await expect(page.locator("hui-view")).toBeAttached({
timeout: PANEL_TIMEOUT,
});
await expect(launchScreen).not.toBeAttached({ timeout: QUICK_TIMEOUT });
});
test("keeps the launch screen until initial content renders", async ({
page,
}) => {
await goToPanel(page, "/?scenario=delayed-lovelace#/lovelace");
const launchScreen = page.locator("#ha-launch-screen");
await expect(launchScreen).toBeAttached({ timeout: QUICK_TIMEOUT });
await expect(page.locator("hui-card")).not.toBeAttached();
await page.evaluate(() => window.resolveLovelaceConfig?.());
await expect(page.locator("hui-card").first()).toBeAttached({
timeout: PANEL_TIMEOUT,
});
await expect(launchScreen).not.toBeAttached({ timeout: QUICK_TIMEOUT });
});
test("renders cards", async ({ page }) => {
await goToPanel(page, "/lovelace");
// At least one card should appear
-5
View File
@@ -92,11 +92,6 @@ declare global {
interface Window {
__assistRun?: unknown;
__mockHass: MockHomeAssistant;
rejectMediaBrowse?: () => void;
resolveCalendarRegistry?: () => void;
resolveGeneratedDashboard?: () => void;
resolveLovelaceConfig?: () => void;
resolveMediaBrowse?: () => void;
}
}
+1 -103
View File
@@ -1,10 +1,5 @@
import type { ExtEntityRegistryEntry } from "../../../../../src/data/entity/entity_registry";
import type { AssistPipeline } from "../../../../../src/data/assist_pipeline";
import type {
EntityRegistryEntry,
ExtEntityRegistryEntry,
} from "../../../../../src/data/entity/entity_registry";
import type { LovelaceRawConfig } from "../../../../../src/data/lovelace/config/types";
import type { MediaPlayerItem } from "../../../../../src/data/media-player";
import type { MockHomeAssistant } from "../../../../../src/fake_data/provide_hass";
export type Scenario = (hass: MockHomeAssistant) => Promise<void> | void;
@@ -129,98 +124,6 @@ const quickSearchAssistScenario: Scenario = async (hass) => {
});
};
const addLaunchScreen = () => {
const launchScreen = document.createElement("div");
launchScreen.id = "ha-launch-screen";
document.body.prepend(launchScreen);
};
const delayedLovelaceScenario: Scenario = (hass) => {
addLaunchScreen();
const config: LovelaceRawConfig = {
views: [
{
title: "Home",
cards: [{ type: "markdown", content: "Dashboard ready" }],
},
],
};
let resolveConfig: ((config: LovelaceRawConfig) => void) | undefined;
const configPromise = new Promise<LovelaceRawConfig>((resolve) => {
resolveConfig = resolve;
});
window.resolveLovelaceConfig = () => resolveConfig?.(config);
hass.mockWS("lovelace/config", () => configPromise);
};
const delayedGeneratedDashboardScenario: Scenario = (hass) => {
addLaunchScreen();
const loadFragmentTranslation = hass.loadFragmentTranslation;
let resolveTranslation: (() => void) | undefined;
const translationReady = new Promise<void>((resolve) => {
resolveTranslation = resolve;
});
hass.loadFragmentTranslation = async (fragment) => {
if (fragment === "lovelace") {
await translationReady;
}
return loadFragmentTranslation(fragment);
};
window.resolveGeneratedDashboard = resolveTranslation;
};
const delayedCalendarScenario: Scenario = (hass) => {
addLaunchScreen();
let resolveRegistry: ((entries: EntityRegistryEntry[]) => void) | undefined;
const registryPromise = new Promise<EntityRegistryEntry[]>((resolve) => {
resolveRegistry = resolve;
});
window.resolveCalendarRegistry = () => resolveRegistry?.([]);
hass.mockWS("config/entity_registry/list", () => registryPromise);
};
const delayedMediaBrowseScenario: Scenario = (hass) => {
addLaunchScreen();
const root: MediaPlayerItem = {
title: "Media",
media_content_id: "media-source://media_source",
media_content_type: "app",
media_class: "directory",
can_play: false,
can_expand: true,
can_search: false,
children: [],
};
let resolveBrowse: ((item: MediaPlayerItem) => void) | undefined;
const browsePromise = new Promise<MediaPlayerItem>((resolve) => {
resolveBrowse = resolve;
});
window.resolveMediaBrowse = () => resolveBrowse?.(root);
hass.mockWS("media_source/browse_media", () => browsePromise);
};
const delayedMediaBrowseErrorScenario: Scenario = (hass) => {
addLaunchScreen();
let rejectBrowse:
((reason: { code: string; message: string }) => void) | undefined;
const browsePromise = new Promise<MediaPlayerItem>((_resolve, reject) => {
rejectBrowse = reject;
});
window.rejectMediaBrowse = () =>
rejectBrowse?.({ code: "unknown_error", message: "Browse failed" });
hass.mockWS("media_source/browse_media", () => browsePromise);
};
// ── Registry ──────────────────────────────────────────────────────────────
export const scenarios: Record<string, Scenario> = {
@@ -228,11 +131,6 @@ export const scenarios: Record<string, Scenario> = {
"non-admin": nonAdminScenario,
"dark-theme": darkThemeScenario,
"custom-theme": customThemeScenario,
"delayed-calendar": delayedCalendarScenario,
"delayed-generated-dashboard": delayedGeneratedDashboardScenario,
"delayed-media-browse": delayedMediaBrowseScenario,
"delayed-media-browse-error": delayedMediaBrowseErrorScenario,
"light-more-info": lightMoreInfoScenario,
"quick-search-assist": quickSearchAssistScenario,
"delayed-lovelace": delayedLovelaceScenario,
};
+69 -69
View File
@@ -40,7 +40,7 @@ __metadata:
languageName: node
linkType: hard
"@asamuzakjp/dom-selector@npm:^8.3.0":
"@asamuzakjp/dom-selector@npm:^8.2.5":
version: 8.3.0
resolution: "@asamuzakjp/dom-selector@npm:8.3.0"
dependencies:
@@ -2499,7 +2499,7 @@ __metadata:
languageName: node
linkType: hard
"@csstools/css-syntax-patches-for-csstree@npm:^1.1.7":
"@csstools/css-syntax-patches-for-csstree@npm:^1.1.6":
version: 1.1.7
resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.1.7"
peerDependencies:
@@ -4740,65 +4740,65 @@ __metadata:
languageName: node
linkType: hard
"@rspack/binding-darwin-arm64@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding-darwin-arm64@npm:2.1.7"
"@rspack/binding-darwin-arm64@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding-darwin-arm64@npm:2.1.6"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@rspack/binding-darwin-x64@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding-darwin-x64@npm:2.1.7"
"@rspack/binding-darwin-x64@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding-darwin-x64@npm:2.1.6"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@rspack/binding-linux-arm64-gnu@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding-linux-arm64-gnu@npm:2.1.7"
"@rspack/binding-linux-arm64-gnu@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding-linux-arm64-gnu@npm:2.1.6"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@rspack/binding-linux-arm64-musl@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding-linux-arm64-musl@npm:2.1.7"
"@rspack/binding-linux-arm64-musl@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding-linux-arm64-musl@npm:2.1.6"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@rspack/binding-linux-riscv64-gnu@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding-linux-riscv64-gnu@npm:2.1.7"
"@rspack/binding-linux-riscv64-gnu@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding-linux-riscv64-gnu@npm:2.1.6"
conditions: os=linux & cpu=riscv64 & libc=glibc
languageName: node
linkType: hard
"@rspack/binding-linux-riscv64-musl@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding-linux-riscv64-musl@npm:2.1.7"
"@rspack/binding-linux-riscv64-musl@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding-linux-riscv64-musl@npm:2.1.6"
conditions: os=linux & cpu=riscv64 & libc=musl
languageName: node
linkType: hard
"@rspack/binding-linux-x64-gnu@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding-linux-x64-gnu@npm:2.1.7"
"@rspack/binding-linux-x64-gnu@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding-linux-x64-gnu@npm:2.1.6"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@rspack/binding-linux-x64-musl@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding-linux-x64-musl@npm:2.1.7"
"@rspack/binding-linux-x64-musl@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding-linux-x64-musl@npm:2.1.6"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@rspack/binding-wasm32-wasi@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding-wasm32-wasi@npm:2.1.7"
"@rspack/binding-wasm32-wasi@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding-wasm32-wasi@npm:2.1.6"
dependencies:
"@emnapi/core": "npm:1.11.2"
"@emnapi/runtime": "npm:1.11.2"
@@ -4807,43 +4807,43 @@ __metadata:
languageName: node
linkType: hard
"@rspack/binding-win32-arm64-msvc@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding-win32-arm64-msvc@npm:2.1.7"
"@rspack/binding-win32-arm64-msvc@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding-win32-arm64-msvc@npm:2.1.6"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@rspack/binding-win32-ia32-msvc@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding-win32-ia32-msvc@npm:2.1.7"
"@rspack/binding-win32-ia32-msvc@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding-win32-ia32-msvc@npm:2.1.6"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@rspack/binding-win32-x64-msvc@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding-win32-x64-msvc@npm:2.1.7"
"@rspack/binding-win32-x64-msvc@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding-win32-x64-msvc@npm:2.1.6"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@rspack/binding@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/binding@npm:2.1.7"
"@rspack/binding@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/binding@npm:2.1.6"
dependencies:
"@rspack/binding-darwin-arm64": "npm:2.1.7"
"@rspack/binding-darwin-x64": "npm:2.1.7"
"@rspack/binding-linux-arm64-gnu": "npm:2.1.7"
"@rspack/binding-linux-arm64-musl": "npm:2.1.7"
"@rspack/binding-linux-riscv64-gnu": "npm:2.1.7"
"@rspack/binding-linux-riscv64-musl": "npm:2.1.7"
"@rspack/binding-linux-x64-gnu": "npm:2.1.7"
"@rspack/binding-linux-x64-musl": "npm:2.1.7"
"@rspack/binding-wasm32-wasi": "npm:2.1.7"
"@rspack/binding-win32-arm64-msvc": "npm:2.1.7"
"@rspack/binding-win32-ia32-msvc": "npm:2.1.7"
"@rspack/binding-win32-x64-msvc": "npm:2.1.7"
"@rspack/binding-darwin-arm64": "npm:2.1.6"
"@rspack/binding-darwin-x64": "npm:2.1.6"
"@rspack/binding-linux-arm64-gnu": "npm:2.1.6"
"@rspack/binding-linux-arm64-musl": "npm:2.1.6"
"@rspack/binding-linux-riscv64-gnu": "npm:2.1.6"
"@rspack/binding-linux-riscv64-musl": "npm:2.1.6"
"@rspack/binding-linux-x64-gnu": "npm:2.1.6"
"@rspack/binding-linux-x64-musl": "npm:2.1.6"
"@rspack/binding-wasm32-wasi": "npm:2.1.6"
"@rspack/binding-win32-arm64-msvc": "npm:2.1.6"
"@rspack/binding-win32-ia32-msvc": "npm:2.1.6"
"@rspack/binding-win32-x64-msvc": "npm:2.1.6"
dependenciesMeta:
"@rspack/binding-darwin-arm64":
optional: true
@@ -4869,15 +4869,15 @@ __metadata:
optional: true
"@rspack/binding-win32-x64-msvc":
optional: true
checksum: 10/1400d8553d2122850fc43a45b69828bcdcbccf7715381a627802e160da6f93e90ca41d0280a69d175894bc29a428a374c80157b874883363b998090dc0aff33c
checksum: 10/0d164b7425f448ed6b8b81e687f4a82377289476cee41553ced9532e211d216fa13429b9125f3e3f025e391709bf752983c16eb4549341a21cbd0c61510afbeb
languageName: node
linkType: hard
"@rspack/core@npm:2.1.7":
version: 2.1.7
resolution: "@rspack/core@npm:2.1.7"
"@rspack/core@npm:2.1.6":
version: 2.1.6
resolution: "@rspack/core@npm:2.1.6"
dependencies:
"@rspack/binding": "npm:2.1.7"
"@rspack/binding": "npm:2.1.6"
peerDependencies:
"@module-federation/runtime-tools": ^0.24.1 || ^2.0.0
"@swc/helpers": ^0.5.23
@@ -4886,7 +4886,7 @@ __metadata:
optional: true
"@swc/helpers":
optional: true
checksum: 10/84526df889fa2813cfa7dfbed71b713c47c2010b1f55da26db698a87ea11f3f433a931ec29616ff300cb63d0ea27cbe7d2b40df64abf45f9b1a484b59c94ca1a
checksum: 10/334f8e13aa3540b7320d3186250fc4e2f128711f765d2073274188f23c692d4262e7cbb88c10571626b55e480186c474bdbcb564e7fa97df803479554de855b4
languageName: node
linkType: hard
@@ -9940,7 +9940,7 @@ __metadata:
"@playwright/test": "npm:1.62.0"
"@replit/codemirror-indentation-markers": "npm:6.5.3"
"@rsdoctor/rspack-plugin": "npm:1.6.1"
"@rspack/core": "npm:2.1.7"
"@rspack/core": "npm:2.1.6"
"@rspack/dev-server": "npm:2.1.0"
"@swc/helpers": "npm:0.5.23"
"@thomasloven/round-slider": "npm:0.6.0"
@@ -10009,7 +10009,7 @@ __metadata:
idb-keyval: "npm:6.3.0"
intl-messageformat: "npm:11.2.12"
js-yaml: "npm:5.2.2"
jsdom: "npm:30.0.1"
jsdom: "npm:30.0.0"
jszip: "npm:3.10.1"
leaflet: "npm:1.9.4"
leaflet-draw: "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
@@ -10964,14 +10964,14 @@ __metadata:
languageName: node
linkType: hard
"jsdom@npm:30.0.1":
version: 30.0.1
resolution: "jsdom@npm:30.0.1"
"jsdom@npm:30.0.0":
version: 30.0.0
resolution: "jsdom@npm:30.0.0"
dependencies:
"@asamuzakjp/css-color": "npm:^6.0.5"
"@asamuzakjp/dom-selector": "npm:^8.3.0"
"@asamuzakjp/dom-selector": "npm:^8.2.5"
"@bramus/specificity": "npm:^2.4.2"
"@csstools/css-syntax-patches-for-csstree": "npm:^1.1.7"
"@csstools/css-syntax-patches-for-csstree": "npm:^1.1.6"
"@exodus/bytes": "npm:^1.15.1"
css-tree: "npm:^3.2.1"
data-urls: "npm:^7.0.0"
@@ -10983,7 +10983,7 @@ __metadata:
saxes: "npm:^6.0.0"
symbol-tree: "npm:^3.2.4"
tough-cookie: "npm:^6.0.2"
undici: "npm:^8.9.0"
undici: "npm:^8.7.0"
w3c-xmlserializer: "npm:^5.0.0"
webidl-conversions: "npm:^8.0.1"
whatwg-mimetype: "npm:^5.0.0"
@@ -10994,7 +10994,7 @@ __metadata:
peerDependenciesMeta:
canvas:
optional: true
checksum: 10/5089ceca7d340b3beec451b7a3286f2bf38d707482e0bdcf046e63b4de3ea2e679372fbb733c5c24c1ce1cc9e70272d10e49fd1b4b146beb77b12c17c069cb36
checksum: 10/dc73c071e67224465018733525047d05772667bd8e00a3703a6f6515238c027b6ca768bf86874a53d512fd0bb962d72cc6a6c31c1bc38e52318f9d412ad0bb90
languageName: node
linkType: hard
@@ -15056,7 +15056,7 @@ __metadata:
languageName: node
linkType: hard
"undici@npm:^8.4.1, undici@npm:^8.9.0":
"undici@npm:^8.4.1, undici@npm:^8.7.0":
version: 8.9.0
resolution: "undici@npm:8.9.0"
checksum: 10/dfad3e233087eafdf1d361acd17c4d45a9590d5db9400458f1c03f47d8f1ef68647e2109ebb982c862e50c227093abd2d5f44b154904fc0b3d22576b6e95cfe7