mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-28 09:56:50 +00:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f73718a35 | ||
|
|
4bf2acdb43 | ||
|
|
f622e5eeb9 | ||
|
|
595068597f | ||
|
|
c708f35ab6 | ||
|
|
205874c800 | ||
|
|
47ddcbe2e5 | ||
|
|
c43b8bfbe0 | ||
|
|
e349b00b15 | ||
|
|
0bb8bdc98a | ||
|
|
bbc5385843 | ||
|
|
43dc5651d3 | ||
|
|
18dcf7abc0 |
+5
-5
@@ -109,7 +109,7 @@
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.3.0",
|
||||
"intl-messageformat": "11.2.14",
|
||||
"js-yaml": "5.4.0",
|
||||
"js-yaml": "5.3.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
@@ -117,7 +117,7 @@
|
||||
"lit-html": "3.3.3",
|
||||
"luxon": "3.7.2",
|
||||
"maplibre-gl": "5.24.0",
|
||||
"marked": "18.0.11",
|
||||
"marked": "18.0.10",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.4",
|
||||
"object-hash": "3.0.0",
|
||||
@@ -179,7 +179,7 @@
|
||||
"browserslist": "4.28.8",
|
||||
"browserslist-useragent-regexp": "4.1.4",
|
||||
"del": "8.0.1",
|
||||
"eslint": "10.9.1",
|
||||
"eslint": "10.9.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.11",
|
||||
"eslint-plugin-import-x": "4.17.1",
|
||||
@@ -206,7 +206,7 @@
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.18.1",
|
||||
"map-stream": "0.0.7",
|
||||
"minify-literals": "2.2.0",
|
||||
"minify-literals": "2.1.0",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.9.6",
|
||||
"rspack-manifest-plugin": "5.2.2",
|
||||
@@ -216,7 +216,7 @@
|
||||
"terser-webpack-plugin": "5.6.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.68.0",
|
||||
"typescript-eslint": "8.67.0",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.11",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20260826.0"
|
||||
version = "20260826.1"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
||||
+2
-85
@@ -80,67 +80,6 @@ const ensureDialogsClosed = async (timestamp: number): Promise<boolean> => {
|
||||
return ensureDialogsClosed(timestamp);
|
||||
};
|
||||
|
||||
/**
|
||||
* Lets a page with unsaved changes (e.g. the automation editor) veto
|
||||
* navigation. `isDirty` is read live at navigation time; `prompt` resolves
|
||||
* true when navigation may proceed.
|
||||
*/
|
||||
export interface UnsavedChangesGuard {
|
||||
isDirty(): boolean;
|
||||
prompt(): Promise<boolean>;
|
||||
}
|
||||
|
||||
const unsavedChangesGuards = new Set<UnsavedChangesGuard>();
|
||||
|
||||
export const registerUnsavedChangesGuard = (guard: UnsavedChangesGuard) => {
|
||||
unsavedChangesGuards.add(guard);
|
||||
};
|
||||
|
||||
export const unregisterUnsavedChangesGuard = (guard: UnsavedChangesGuard) => {
|
||||
unsavedChangesGuards.delete(guard);
|
||||
};
|
||||
|
||||
let pendingUnsavedPrompt: Promise<boolean> | undefined;
|
||||
|
||||
/**
|
||||
* Counts navigations that changed the history entry, so a navigation held up
|
||||
* by an unsaved-changes prompt can tell whether a newer one has moved the app
|
||||
* on in the meantime.
|
||||
*/
|
||||
let committedNavigations = 0;
|
||||
|
||||
/**
|
||||
* Asks each dirty guard whether navigation may proceed. Returns true when
|
||||
* nothing is dirty or every prompt was confirmed. Concurrent navigations
|
||||
* share one pending prompt instead of stacking dialogs; the dirty check runs
|
||||
* before joining it, so a navigation triggered from inside a prompt (e.g. by
|
||||
* its save action) cannot deadlock on its own promise.
|
||||
*/
|
||||
const ensureUnsavedChangesConfirmed = (): Promise<boolean> => {
|
||||
const dirtyGuards = [...unsavedChangesGuards].filter((guard) =>
|
||||
guard.isDirty()
|
||||
);
|
||||
if (!dirtyGuards.length) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
if (!pendingUnsavedPrompt) {
|
||||
pendingUnsavedPrompt = (async () => {
|
||||
try {
|
||||
for (const guard of dirtyGuards) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
if (!(await guard.prompt())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
pendingUnsavedPrompt = undefined;
|
||||
}
|
||||
})();
|
||||
}
|
||||
return pendingUnsavedPrompt;
|
||||
};
|
||||
|
||||
const buildHistoryState = (
|
||||
data: Record<string, unknown> | undefined,
|
||||
from?: string
|
||||
@@ -152,7 +91,7 @@ const buildHistoryState = (
|
||||
return { ...state, from };
|
||||
};
|
||||
|
||||
const performNavigation = async (path: string, options?: NavigateOptions) => {
|
||||
export const navigate = async (path: string, options?: NavigateOptions) => {
|
||||
const canProceed = await ensureDialogsClosed(Date.now());
|
||||
if (!canProceed) {
|
||||
return false;
|
||||
@@ -188,28 +127,9 @@ const performNavigation = async (path: string, options?: NavigateOptions) => {
|
||||
fireEvent(mainWindow, "location-changed", {
|
||||
replace,
|
||||
});
|
||||
committedNavigations += 1;
|
||||
return true;
|
||||
};
|
||||
|
||||
export const navigate = async (path: string, options?: NavigateOptions) => {
|
||||
// Only guard actual departures: navigating to the current path keeps the
|
||||
// page, and any unsaved state on it, mounted.
|
||||
if (path !== currentPath()) {
|
||||
const navigationsBeforePrompt = committedNavigations;
|
||||
if (!(await ensureUnsavedChangesConfirmed())) {
|
||||
return false;
|
||||
}
|
||||
if (committedNavigations !== navigationsBeforePrompt) {
|
||||
// Another navigation landed while the prompt was waiting for an answer,
|
||||
// so this destination is stale. Dropping it keeps a late answer from
|
||||
// pulling the user back off the page they are on now.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return performNavigation(path, options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether the previous history entry is a page this app navigated away from.
|
||||
* `history.length` cannot answer this: a login redirect goes through
|
||||
@@ -222,9 +142,6 @@ export const canGoBack = (): boolean =>
|
||||
/**
|
||||
* Navigate back to the page we came from, falling back to a path when the
|
||||
* previous entry is not ours (deep link, login redirect, fresh tab).
|
||||
* Deliberately not guarded against unsaved changes: pages with such a guard
|
||||
* confirm in their own back handlers, and delete flows leave through here
|
||||
* after the edited item is already gone.
|
||||
*/
|
||||
export const goBack = async (fallbackPath?: string): Promise<void> => {
|
||||
const canProceed = await ensureDialogsClosed(Date.now());
|
||||
@@ -239,5 +156,5 @@ export const goBack = async (fallbackPath?: string): Promise<void> => {
|
||||
return;
|
||||
}
|
||||
|
||||
await performNavigation(fallbackPath || "/", { replace: true });
|
||||
await navigate(fallbackPath || "/", { replace: true });
|
||||
};
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import deepFreeze from "deep-freeze";
|
||||
|
||||
const inFlightRequests = new WeakMap<object, Map<string, Promise<unknown>>>();
|
||||
|
||||
export const shareInFlightRequest = <T>(
|
||||
owner: object,
|
||||
key: string,
|
||||
fetcher: () => Promise<T>
|
||||
): Promise<T> => {
|
||||
let requests = inFlightRequests.get(owner);
|
||||
if (!requests) {
|
||||
requests = new Map();
|
||||
inFlightRequests.set(owner, requests);
|
||||
}
|
||||
|
||||
const ownerRequests = requests;
|
||||
const existing = ownerRequests.get(key);
|
||||
if (existing) {
|
||||
return existing as Promise<T>;
|
||||
}
|
||||
|
||||
const request = fetcher()
|
||||
.then((result) => deepFreeze(result) as T)
|
||||
.finally(() => {
|
||||
if (ownerRequests.get(key) !== request) {
|
||||
return;
|
||||
}
|
||||
|
||||
ownerRequests.delete(key);
|
||||
if (ownerRequests.size === 0) {
|
||||
inFlightRequests.delete(owner);
|
||||
}
|
||||
});
|
||||
|
||||
ownerRequests.set(key, request);
|
||||
return request;
|
||||
};
|
||||
@@ -3,27 +3,6 @@ import type { TooltipPositionCallback } from "echarts/types/dist/shared";
|
||||
export const TOOLTIP_GAP_PX = 12;
|
||||
export const TOOLTIP_TOP_OFFSET_PX = 10;
|
||||
|
||||
const offsetFromCursor = (
|
||||
cursorX: number,
|
||||
dom: unknown,
|
||||
viewW: number,
|
||||
tipW: number
|
||||
) => {
|
||||
const rtl =
|
||||
dom instanceof HTMLElement && getComputedStyle(dom).direction === "rtl";
|
||||
|
||||
const rightOfCursor = cursorX + TOOLTIP_GAP_PX;
|
||||
const leftOfCursor = cursorX - TOOLTIP_GAP_PX - tipW;
|
||||
|
||||
let x = rtl ? leftOfCursor : rightOfCursor;
|
||||
const overflowsRight = x + tipW > viewW;
|
||||
const overflowsLeft = x < 0;
|
||||
if (overflowsRight || overflowsLeft) {
|
||||
x = rtl ? rightOfCursor : leftOfCursor;
|
||||
}
|
||||
return Math.max(0, Math.min(x, viewW - tipW));
|
||||
};
|
||||
|
||||
/**
|
||||
* Pins the tooltip near the top of the chart and offsets it horizontally
|
||||
* from the cursor so it never covers the data point being inspected.
|
||||
@@ -41,29 +20,21 @@ export const sideTooltipPosition: TooltipPositionCallback = (
|
||||
const [viewW, viewH] = size.viewSize;
|
||||
const [tipW, tipH] = size.contentSize;
|
||||
|
||||
const x = offsetFromCursor(cursorX, dom, viewW, tipW);
|
||||
const rtl =
|
||||
dom instanceof HTMLElement && getComputedStyle(dom).direction === "rtl";
|
||||
|
||||
const rightOfCursor = cursorX + TOOLTIP_GAP_PX;
|
||||
const leftOfCursor = cursorX - TOOLTIP_GAP_PX - tipW;
|
||||
|
||||
let x = rtl ? leftOfCursor : rightOfCursor;
|
||||
const overflowsRight = x + tipW > viewW;
|
||||
const overflowsLeft = x < 0;
|
||||
if (overflowsRight || overflowsLeft) {
|
||||
x = rtl ? rightOfCursor : leftOfCursor;
|
||||
}
|
||||
x = Math.max(0, Math.min(x, viewW - tipW));
|
||||
|
||||
const y = Math.max(0, Math.min(TOOLTIP_TOP_OFFSET_PX, viewH - tipH));
|
||||
|
||||
return [x, y];
|
||||
};
|
||||
|
||||
/**
|
||||
* Offsets the tooltip horizontally from the cursor and keeps it level with it.
|
||||
* For item-trigger tooltips where the cursor's row is what the tooltip shows.
|
||||
*/
|
||||
export const itemTooltipPosition: TooltipPositionCallback = (
|
||||
point,
|
||||
_params,
|
||||
dom,
|
||||
_rect,
|
||||
size
|
||||
) => {
|
||||
const [cursorX, cursorY] = point;
|
||||
const [viewW, viewH] = size.viewSize;
|
||||
const [tipW, tipH] = size.contentSize;
|
||||
|
||||
const x = offsetFromCursor(cursorX, dom, viewW, tipW);
|
||||
const y = Math.max(0, Math.min(cursorY - tipH / 2, viewH - tipH));
|
||||
|
||||
return [x, y];
|
||||
};
|
||||
|
||||
@@ -328,7 +328,6 @@ export class StateHistoryChartLine extends LitElement {
|
||||
...createYAxisPrecisionBounds({
|
||||
min: this._clampYAxis(minYAxis),
|
||||
max: this._clampYAxis(maxYAxis),
|
||||
unit: this.unit,
|
||||
onFractionDigits: (digits) => {
|
||||
if (digits !== this._yAxisFractionDigits) {
|
||||
this._yAxisFractionDigits = digits;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -12,7 +13,7 @@ import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type { TimelineEntity } from "../../data/history";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import { itemTooltipPosition } from "./chart-tooltip-position";
|
||||
import { sideTooltipPosition } from "./chart-tooltip-position";
|
||||
import "./ha-chart-tooltip-marker";
|
||||
import { computeTimelineColor } from "./timeline-color";
|
||||
import type { HaECOption, HaECSeries } from "../../resources/echarts/echarts";
|
||||
@@ -23,6 +24,7 @@ import { measureTextWidth } from "../../util/text";
|
||||
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
|
||||
const ROW_HEIGHT = 30;
|
||||
const ROW_HEIGHT_INSIDE_LABELS = 64;
|
||||
const GRID_BOTTOM = 30;
|
||||
|
||||
@customElement("state-history-chart-timeline")
|
||||
@@ -41,6 +43,10 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
@property({ attribute: "show-names", type: Boolean }) public showNames = true;
|
||||
|
||||
/** Draw each row's name above its bar instead of in a label column. */
|
||||
@property({ attribute: "inside-labels", type: Boolean })
|
||||
public insideLabels = false;
|
||||
|
||||
@property({ attribute: "click-for-more-info", type: Boolean })
|
||||
public clickForMoreInfo = true;
|
||||
|
||||
@@ -63,6 +69,13 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
@state() private _yWidth = 0;
|
||||
|
||||
private _width = 0;
|
||||
|
||||
private _resize = new ResizeController(this, {
|
||||
skipInitial: true,
|
||||
callback: (entries) => entries[0]?.contentRect.width,
|
||||
});
|
||||
|
||||
private _chartTime: Date = new Date();
|
||||
|
||||
protected render() {
|
||||
@@ -70,7 +83,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
<ha-chart-base
|
||||
.hass=${this.hass}
|
||||
.options=${this._chartOptions}
|
||||
.height=${`${this.data.length * ROW_HEIGHT + GRID_BOTTOM}px`}
|
||||
.height=${`${this.data.length * (this.insideLabels ? ROW_HEIGHT_INSIDE_LABELS : ROW_HEIGHT) + GRID_BOTTOM}px`}
|
||||
.data=${this._chartData as HaECSeries}
|
||||
small-controls
|
||||
@chart-click=${this._handleChartClick}
|
||||
@@ -180,13 +193,19 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
this._generateData();
|
||||
}
|
||||
|
||||
const width = this.insideLabels ? Math.round(this._resize.value ?? 0) : 0;
|
||||
const widthChanged = width !== this._width;
|
||||
this._width = width;
|
||||
|
||||
if (
|
||||
!this.hasUpdated ||
|
||||
changedProps.has("startTime") ||
|
||||
changedProps.has("endTime") ||
|
||||
changedProps.has("showNames") ||
|
||||
changedProps.has("insideLabels") ||
|
||||
changedProps.has("paddingYAxis") ||
|
||||
changedProps.has("_yWidth")
|
||||
changedProps.has("_yWidth") ||
|
||||
widthChanged
|
||||
) {
|
||||
this._createOptions();
|
||||
}
|
||||
@@ -196,14 +215,22 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
const narrow = this.narrow;
|
||||
const showNames = this.chunked || this.showNames;
|
||||
const maxInternalLabelWidth = narrow ? 105 : 185;
|
||||
const labelWidth = showNames
|
||||
? Math.max(this.paddingYAxis, this._yWidth)
|
||||
: 0;
|
||||
const insideLabels = this.insideLabels;
|
||||
const labelWidth =
|
||||
showNames && !insideLabels
|
||||
? Math.max(this.paddingYAxis, this._yWidth)
|
||||
: 0;
|
||||
const labelMargin = 5;
|
||||
const rtl = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
// Keeps the plot aligned with the line charts sharing the y-axis padding.
|
||||
const plotPadding = insideLabels ? this.paddingYAxis : labelWidth;
|
||||
// A zero width hides the labels instead of truncating them.
|
||||
const insideLabelWidth = this._width
|
||||
? Math.max(0, this._width - plotPadding - labelMargin)
|
||||
: undefined;
|
||||
this._chartOptions = {
|
||||
xAxis: {
|
||||
type: "time",
|
||||
@@ -227,41 +254,56 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
show: showNames,
|
||||
width: labelWidth,
|
||||
overflow: "truncate",
|
||||
margin: labelMargin,
|
||||
formatter: (id: string) => {
|
||||
const label = this._chartData.find((d) => d.id === id)
|
||||
?.name as string;
|
||||
const width = label
|
||||
? Math.min(
|
||||
measureTextWidth(label, 12) + labelMargin,
|
||||
maxInternalLabelWidth
|
||||
)
|
||||
: 0;
|
||||
if (width > this._yWidth) {
|
||||
this._yWidth = width;
|
||||
fireEvent(this, "y-width-changed", {
|
||||
value: this._yWidth,
|
||||
chartIndex: this.chartIndex,
|
||||
});
|
||||
axisLabel: insideLabels
|
||||
? {
|
||||
show: showNames,
|
||||
inside: true,
|
||||
margin: 0,
|
||||
padding: [0, rtl ? 2 : 0, 14, rtl ? 0 : 2],
|
||||
align: rtl ? "right" : "left",
|
||||
verticalAlign: "bottom",
|
||||
width: insideLabelWidth,
|
||||
overflow: "truncate",
|
||||
formatter: (id: string) =>
|
||||
(this._chartData.find((d) => d.id === id)?.name as string) ??
|
||||
"",
|
||||
hideOverlap: true,
|
||||
}
|
||||
return label;
|
||||
},
|
||||
hideOverlap: true,
|
||||
},
|
||||
: {
|
||||
show: showNames,
|
||||
width: labelWidth,
|
||||
overflow: "truncate",
|
||||
margin: labelMargin,
|
||||
formatter: (id: string) => {
|
||||
const label = this._chartData.find((d) => d.id === id)
|
||||
?.name as string;
|
||||
const width = label
|
||||
? Math.min(
|
||||
measureTextWidth(label, 12) + labelMargin,
|
||||
maxInternalLabelWidth
|
||||
)
|
||||
: 0;
|
||||
if (width > this._yWidth) {
|
||||
this._yWidth = width;
|
||||
fireEvent(this, "y-width-changed", {
|
||||
value: this._yWidth,
|
||||
chartIndex: this.chartIndex,
|
||||
});
|
||||
}
|
||||
return label;
|
||||
},
|
||||
hideOverlap: true,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
top: 10,
|
||||
top: insideLabels ? 20 : 10,
|
||||
bottom: GRID_BOTTOM,
|
||||
left: rtl ? 1 : labelWidth,
|
||||
right: rtl ? labelWidth : 1,
|
||||
left: rtl ? 1 : plotPadding,
|
||||
right: rtl ? plotPadding : 1,
|
||||
},
|
||||
tooltip: {
|
||||
renderMode: "html",
|
||||
position: itemTooltipPosition,
|
||||
position: sideTooltipPosition,
|
||||
confine: true,
|
||||
formatter: this._renderTooltip,
|
||||
},
|
||||
@@ -401,6 +443,10 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ha-chart-base {
|
||||
--chart-max-height: none;
|
||||
}
|
||||
|
||||
@@ -79,6 +79,10 @@ export class StateHistoryCharts extends LitElement {
|
||||
|
||||
@property({ attribute: "show-names", type: Boolean }) public showNames = true;
|
||||
|
||||
/** Draw timeline row names above their bar instead of in a label column. */
|
||||
@property({ attribute: "inside-labels", type: Boolean, reflect: true })
|
||||
public insideLabels = false;
|
||||
|
||||
@property({ attribute: "click-for-more-info", type: Boolean })
|
||||
public clickForMoreInfo = true;
|
||||
|
||||
@@ -227,6 +231,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
.startTime=${this._computedStartTime}
|
||||
.endTime=${this._computedEndTime}
|
||||
.showNames=${this.showNames}
|
||||
.insideLabels=${this.insideLabels}
|
||||
.names=${this.names}
|
||||
.narrow=${this.narrow}
|
||||
.chunked=${this.virtualize}
|
||||
@@ -424,6 +429,12 @@ export class StateHistoryCharts extends LitElement {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
/* Names inside the plot sit close to the chart above them, so the groups
|
||||
need more room between them to stay apart. */
|
||||
:host([inside-labels]) .entry-container.timeline:not(:first-child) {
|
||||
margin-top: var(--ha-space-8);
|
||||
}
|
||||
|
||||
.entry-container:hover {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -446,7 +446,6 @@ export class StatisticsChart extends LitElement {
|
||||
...createYAxisPrecisionBounds({
|
||||
min: this._clampYAxis(minYAxis),
|
||||
max: this._clampYAxis(maxYAxis),
|
||||
unit: this.unit,
|
||||
// Bar charts stay anchored at 0, so precision must reflect the
|
||||
// 0-based range that is actually rendered.
|
||||
includeZero: !yAxisScale,
|
||||
|
||||
@@ -1,32 +1,13 @@
|
||||
import { intervalScaleEnsureValidExtent } from "echarts/lib/scale/helper";
|
||||
import { getPrecision, nice, round } from "echarts/lib/util/number";
|
||||
|
||||
// A range smaller than this fraction of the axis magnitude is floating-point
|
||||
// noise (e.g. from summed statistics), not real precision.
|
||||
const NEGLIGIBLE_RANGE_RATIO = 1e-10;
|
||||
|
||||
// Intervals the axis aims for. Passed to ECharts rather than assumed, so the
|
||||
// precision derived here cannot drift from the ticks it renders.
|
||||
const SPLIT_NUMBER = 5;
|
||||
|
||||
// How thin a gap between the data and the plot edge counts as no gap at all,
|
||||
// as a fraction of the data span. ECharts floors the axis minimum and ceils the
|
||||
// maximum to a tick multiple, which usually leaves headroom, but quantized
|
||||
// states often land exactly on a tick and get none — collapsing area-filled
|
||||
// series, which are drawn from their value down to the axis minimum. Widening
|
||||
// the extent by this much before that rounding bumps any axis with less
|
||||
// headroom out to a full tick, and leaves the rest where they are.
|
||||
const GAP_FRACTION_OF_SPAN = 0.02;
|
||||
|
||||
// A percentage has a real ceiling the way zero is a real floor, so the gap must
|
||||
// not push the axis past it. Not every `%` sensor is bounded — power factor is
|
||||
// signed and can read over 100 — so this only applies while the data stays under.
|
||||
const PERCENT_MAX = 100;
|
||||
|
||||
// Derive the number of decimal digits to use for Y-axis labels from the
|
||||
// observed data range, by asking ECharts for the same tick interval it will
|
||||
// render. This matches the precision it actually draws, so labels are neither
|
||||
// truncated to identical values nor padded with extra zeros.
|
||||
// observed data range. We mirror how ECharts sizes its ticks: it splits the
|
||||
// range into ~5 intervals (its default `splitNumber`) and rounds that raw
|
||||
// interval to a "nice" 1/2/3/5×10ⁿ value, then reports the decimals that nice
|
||||
// interval needs. This matches the precision ECharts actually renders, so
|
||||
// labels are neither truncated to identical values nor padded with extra zeros.
|
||||
export function computeYAxisFractionDigits(
|
||||
min: number,
|
||||
max: number,
|
||||
@@ -41,7 +22,13 @@ export function computeYAxisFractionDigits(
|
||||
// with a tail of zeros (e.g. "0.20000000000000"), so treat it as flat.
|
||||
const magnitude = Math.max(Math.abs(lo), Math.abs(hi));
|
||||
if (range <= magnitude * NEGLIGIBLE_RANGE_RATIO) return 1;
|
||||
return getPrecision(nice(range / SPLIT_NUMBER, true));
|
||||
const rawInterval = range / 5;
|
||||
const exponent = Math.floor(Math.log10(rawInterval));
|
||||
const mantissa = rawInterval / 10 ** exponent; // in [1, 10)
|
||||
// Rounding the mantissa to a nice value only ever carries to the next power
|
||||
// of ten (mantissa ≥ 7 → 10), which needs one fewer decimal.
|
||||
const niceExponent = mantissa >= 7 ? exponent + 1 : exponent;
|
||||
return Math.max(0, -niceExponent);
|
||||
}
|
||||
|
||||
interface YAxisExtentValues {
|
||||
@@ -57,116 +44,34 @@ const resolveYAxisBound = (
|
||||
values: YAxisExtentValues
|
||||
): number | undefined => (typeof bound === "function" ? bound(values) : bound);
|
||||
|
||||
// A constant series has no span for the gap to be a fraction of, so ECharts
|
||||
// falls back to `Math.abs(min)` when sizing it. That makes the extent unequal,
|
||||
// which in turn stops `intervalScaleEnsureValidExtent` from applying the ±|v|/2
|
||||
// expansion a flat series relies on for its window. Run that expansion here and
|
||||
// return fixed bounds, which also suppresses the gap.
|
||||
const flatSeriesExtent = (value: number, fixed: [boolean, boolean]) => {
|
||||
const [lo, hi] = intervalScaleEnsureValidExtent([value, value], fixed);
|
||||
const interval = nice((hi - lo) / SPLIT_NUMBER, true);
|
||||
const precision = getPrecision(interval);
|
||||
return {
|
||||
min: round(Math.floor(lo / interval) * interval, precision),
|
||||
max: round(Math.ceil(hi / interval) * interval, precision),
|
||||
};
|
||||
};
|
||||
|
||||
// Build the `yAxis` options that keep tick-label precision and the plot-edge gap
|
||||
// in agreement with the extent ECharts renders. It re-invokes the `min`/`max`
|
||||
// callbacks with the extent of the visible (zoom-filtered) data on every
|
||||
// dataZoom, and always before the label formatter runs, so the fraction digits
|
||||
// recomputed here track the zoomed range. A callback returns `undefined`
|
||||
// wherever auto-scaling should stand, and a number only where the axis has to be
|
||||
// pinned: an explicit bound, the zero anchor, or a constant series.
|
||||
// Wrap the Y-axis `min`/`max` options in callbacks so the tick-label precision
|
||||
// tracks the currently visible axis extent. ECharts re-invokes these callbacks
|
||||
// with the extent of the visible (zoom-filtered) data on every dataZoom, and
|
||||
// always before the label formatter runs, so recomputing the fraction digits
|
||||
// here keeps zoomed-in labels distinct. The callbacks return the original
|
||||
// bounds unchanged, so auto-scaling still applies when a bound is not set.
|
||||
export function createYAxisPrecisionBounds(options: {
|
||||
min?: YAxisBound;
|
||||
max?: YAxisBound;
|
||||
// Set for bar axes anchored at 0, so precision reflects the 0-based range.
|
||||
// Such an axis also gets no gap: pushing it below zero would defeat the zero
|
||||
// anchoring and leave the bars floating above the axis.
|
||||
includeZero?: boolean;
|
||||
// Used to recognise a bounded quantity, so the gap cannot widen the axis past
|
||||
// a limit the data itself never crosses.
|
||||
unit?: string;
|
||||
onFractionDigits: (digits: number) => void;
|
||||
}): {
|
||||
min: (values: YAxisExtentValues) => number | undefined;
|
||||
max: (values: YAxisExtentValues) => number | undefined;
|
||||
boundaryGap: [number, number];
|
||||
splitNumber: number;
|
||||
} {
|
||||
const { min, max, includeZero, unit, onFractionDigits } = options;
|
||||
const naturalMax = unit === "%" ? PERCENT_MAX : undefined;
|
||||
|
||||
const resolveBounds = (values: YAxisExtentValues) => {
|
||||
const resolvedMin = resolveYAxisBound(min, values);
|
||||
const resolvedMax = resolveYAxisBound(max, values);
|
||||
if (
|
||||
includeZero ||
|
||||
!Number.isFinite(values.min) ||
|
||||
!Number.isFinite(values.max)
|
||||
) {
|
||||
return { min: resolvedMin, max: resolvedMax, gap: 0 };
|
||||
}
|
||||
if (values.min === values.max) {
|
||||
const flat = flatSeriesExtent(values.min, [
|
||||
resolvedMin !== undefined,
|
||||
resolvedMax !== undefined,
|
||||
]);
|
||||
// The expansion is a fraction of the magnitude, so a constant series near
|
||||
// the ceiling would otherwise overshoot it too.
|
||||
const flatMax =
|
||||
naturalMax !== undefined && values.max <= naturalMax
|
||||
? Math.min(flat.max, naturalMax)
|
||||
: flat.max;
|
||||
return {
|
||||
min: resolvedMin ?? flat.min,
|
||||
max: resolvedMax ?? flatMax,
|
||||
gap: 0,
|
||||
};
|
||||
}
|
||||
const gap = (values.max - values.min) * GAP_FRACTION_OF_SPAN;
|
||||
// Never let the gap carry a series past a boundary it does not itself cross.
|
||||
const floor = values.min >= 0 ? 0 : undefined;
|
||||
const ceiling =
|
||||
naturalMax !== undefined && values.max <= naturalMax
|
||||
? naturalMax
|
||||
: values.max <= 0
|
||||
? 0
|
||||
: undefined;
|
||||
return {
|
||||
min:
|
||||
resolvedMin ??
|
||||
(floor !== undefined && values.min - floor < gap ? floor : undefined),
|
||||
max:
|
||||
resolvedMax ??
|
||||
(ceiling !== undefined && ceiling - values.max < gap
|
||||
? ceiling
|
||||
: undefined),
|
||||
gap,
|
||||
};
|
||||
};
|
||||
|
||||
const { min, max, includeZero, onFractionDigits } = options;
|
||||
return {
|
||||
// Always emit the key. `setOption` merges the Y axis rather than replacing
|
||||
// it, so a conditionally spread gap would survive a chart switching to a
|
||||
// zero-anchored type and leave its bars floating.
|
||||
boundaryGap: includeZero
|
||||
? [0, 0]
|
||||
: [GAP_FRACTION_OF_SPAN, GAP_FRACTION_OF_SPAN],
|
||||
splitNumber: SPLIT_NUMBER,
|
||||
min: (values) => {
|
||||
const bounds = resolveBounds(values);
|
||||
const resolvedMin = resolveYAxisBound(min, values);
|
||||
const resolvedMax = resolveYAxisBound(max, values);
|
||||
const extentMin = resolvedMin ?? values.min;
|
||||
const extentMax = resolvedMax ?? values.max;
|
||||
onFractionDigits(
|
||||
computeYAxisFractionDigits(
|
||||
bounds.min ?? values.min - bounds.gap,
|
||||
bounds.max ?? values.max + bounds.gap,
|
||||
includeZero
|
||||
)
|
||||
computeYAxisFractionDigits(extentMin, extentMax, includeZero)
|
||||
);
|
||||
return bounds.min;
|
||||
return resolvedMin;
|
||||
},
|
||||
max: (values) => resolveBounds(values).max,
|
||||
max: (values) => resolveYAxisBound(max, values),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,10 +16,10 @@ import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import {
|
||||
getStatisticIds,
|
||||
getStatisticLabel,
|
||||
type StatisticsMetaData,
|
||||
} from "../../data/recorder";
|
||||
import { getStatisticIds } from "../../data/recorder_statistic_ids";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import "../ha-combo-box-item";
|
||||
|
||||
@@ -90,7 +90,7 @@ class HaAlert extends LitElement {
|
||||
static styles = css`
|
||||
.issue-type {
|
||||
position: relative;
|
||||
padding: var(--ha-alert-padding, 8px);
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
}
|
||||
.icon {
|
||||
|
||||
@@ -95,10 +95,6 @@ export class HaDrawer extends LitElement {
|
||||
}
|
||||
|
||||
private _handleAfterHide(ev: Event) {
|
||||
// Ignore wa-after-hide from nested Web Awesome components (e.g. tooltips)
|
||||
if (ev.target !== ev.currentTarget) {
|
||||
return;
|
||||
}
|
||||
ev.stopPropagation();
|
||||
this.open = false;
|
||||
fireEvent(this, "hass-drawer-closed");
|
||||
|
||||
@@ -564,73 +564,75 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
this.primaryEntitiesOnly
|
||||
);
|
||||
|
||||
let referencedAreas = entries.referenced_areas;
|
||||
const hiddenAreaIds: string[] = [];
|
||||
if (this.type === "floor" || this.type === "label") {
|
||||
referencedAreas = referencedAreas.filter((area_id) => {
|
||||
const area = this.hass.areas[area_id];
|
||||
// Absent from the registry is not a filter decision: drop the id
|
||||
// without marking it hidden, so entities targeted through their
|
||||
// own area or label are not dropped along with it.
|
||||
if (!area) {
|
||||
entries.referenced_areas = entries.referenced_areas.filter(
|
||||
(area_id) => {
|
||||
const area = this.hass.areas[area_id];
|
||||
// Absent from the registry is not a filter decision: drop the id
|
||||
// without marking it hidden, so entities targeted through their
|
||||
// own area or label are not dropped along with it.
|
||||
if (!area) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
(this.type === "floor" || area.labels.includes(this.itemId)) &&
|
||||
areaMeetsFilter(
|
||||
area,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this.deviceFilter,
|
||||
this.includeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.hass.states,
|
||||
this.entityFilter,
|
||||
!this.primaryEntitiesOnly
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
hiddenAreaIds.push(area_id);
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
(this.type === "floor" || area.labels.includes(this.itemId)) &&
|
||||
areaMeetsFilter(
|
||||
area,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this.deviceFilter,
|
||||
this.includeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.hass.states,
|
||||
this.entityFilter,
|
||||
!this.primaryEntitiesOnly
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
hiddenAreaIds.push(area_id);
|
||||
return false;
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
let referencedDevices = entries.referenced_devices;
|
||||
const hiddenDeviceIds: string[] = [];
|
||||
if (
|
||||
this.type === "floor" ||
|
||||
this.type === "area" ||
|
||||
this.type === "label"
|
||||
) {
|
||||
referencedDevices = referencedDevices.filter((device_id) => {
|
||||
const device = this.hass.devices[device_id];
|
||||
if (!device) {
|
||||
entries.referenced_devices = entries.referenced_devices.filter(
|
||||
(device_id) => {
|
||||
const device = this.hass.devices[device_id];
|
||||
if (!device) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!hiddenAreaIds.includes(device.area_id || "") &&
|
||||
deviceMeetsFilter(
|
||||
device,
|
||||
this.hass.entities,
|
||||
this.deviceFilter,
|
||||
this.includeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.hass.states,
|
||||
this.entityFilter,
|
||||
!this.primaryEntitiesOnly
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
hiddenDeviceIds.push(device_id);
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!hiddenAreaIds.includes(device.area_id || "") &&
|
||||
deviceMeetsFilter(
|
||||
device,
|
||||
this.hass.entities,
|
||||
this.deviceFilter,
|
||||
this.includeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.hass.states,
|
||||
this.entityFilter,
|
||||
!this.primaryEntitiesOnly
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
hiddenDeviceIds.push(device_id);
|
||||
return false;
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
const referencedEntities = entries.referenced_entities.filter(
|
||||
entries.referenced_entities = entries.referenced_entities.filter(
|
||||
(entity_id) => {
|
||||
const entity = this.hass.entities[entity_id];
|
||||
// Core can reference entities that are absent from the display
|
||||
@@ -645,9 +647,9 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
(this.type === "area" && entity.area_id === this.itemId) ||
|
||||
(this.type === "floor" &&
|
||||
entity.area_id &&
|
||||
referencedAreas.includes(entity.area_id)) ||
|
||||
entries.referenced_areas.includes(entity.area_id)) ||
|
||||
(this.type === "label" && entity.labels.includes(this.itemId)) ||
|
||||
referencedDevices.includes(entity.device_id || "")
|
||||
entries.referenced_devices.includes(entity.device_id || "")
|
||||
) {
|
||||
return entityRegMeetsFilter(
|
||||
entity,
|
||||
@@ -662,12 +664,7 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
this._entries = {
|
||||
...entries,
|
||||
referenced_areas: referencedAreas,
|
||||
referenced_devices: referencedDevices,
|
||||
referenced_entities: referencedEntities,
|
||||
};
|
||||
this._entries = entries;
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to extract target", e);
|
||||
|
||||
+28
-35
@@ -1909,13 +1909,20 @@ export const downloadEnergyData = (
|
||||
const device_consumption_water = energyData.prefs.device_consumption_water;
|
||||
const stats = energyData.state.stats;
|
||||
|
||||
interface CsvRow {
|
||||
id: string;
|
||||
type: string;
|
||||
unit: string;
|
||||
data: StatisticValue[];
|
||||
}
|
||||
const rows: CsvRow[] = [];
|
||||
const timeSet = new Set<number>();
|
||||
Object.values(stats).forEach((stat) => {
|
||||
stat.forEach((datapoint) => {
|
||||
timeSet.add(datapoint.start);
|
||||
});
|
||||
});
|
||||
const times = Array.from(timeSet).sort();
|
||||
|
||||
const headers =
|
||||
"entity_id,type,unit," +
|
||||
times.map((t) => new Date(t).toISOString()).join(",") +
|
||||
"\n";
|
||||
const csv: string[] = [];
|
||||
csv[0] = headers;
|
||||
|
||||
const processCsvRow = function (
|
||||
id: string,
|
||||
@@ -1923,7 +1930,20 @@ export const downloadEnergyData = (
|
||||
unit: string,
|
||||
data: StatisticValue[]
|
||||
) {
|
||||
rows.push({ id, type, unit, data });
|
||||
let n = 0;
|
||||
const row: string[] = [];
|
||||
row.push(id);
|
||||
row.push(type);
|
||||
row.push(unit.normalize("NFKD"));
|
||||
times.forEach((t) => {
|
||||
if (n < data.length && data[n].start === t) {
|
||||
row.push((data[n].change ?? "").toString());
|
||||
n++;
|
||||
} else {
|
||||
row.push("");
|
||||
}
|
||||
});
|
||||
csv.push(row.join(",") + "\n");
|
||||
};
|
||||
|
||||
const processStat = function (stat: string, type: string, unit: string) {
|
||||
@@ -2170,33 +2190,6 @@ export const downloadEnergyData = (
|
||||
);
|
||||
}
|
||||
|
||||
const timeSet = new Set<number>();
|
||||
rows.forEach((row) => {
|
||||
row.data.forEach((datapoint) => {
|
||||
timeSet.add(datapoint.start);
|
||||
});
|
||||
});
|
||||
const times = Array.from(timeSet).sort();
|
||||
|
||||
const csv: string[] = [
|
||||
"entity_id,type,unit," +
|
||||
times.map((t) => new Date(t).toISOString()).join(",") +
|
||||
"\n",
|
||||
];
|
||||
rows.forEach(({ id, type, unit, data }) => {
|
||||
let n = 0;
|
||||
const row: string[] = [id, type, unit.normalize("NFKD")];
|
||||
times.forEach((t) => {
|
||||
if (n < data.length && data[n].start === t) {
|
||||
row.push((data[n].change ?? "").toString());
|
||||
n++;
|
||||
} else {
|
||||
row.push("");
|
||||
}
|
||||
});
|
||||
csv.push(row.join(",") + "\n");
|
||||
});
|
||||
|
||||
const blob = new Blob(csv, {
|
||||
type: "text/csv",
|
||||
});
|
||||
|
||||
@@ -50,6 +50,9 @@ export const DOMAIN_ATTRIBUTES_UNITS = {
|
||||
azimuth: "°",
|
||||
elevation: "°",
|
||||
},
|
||||
vacuum: {
|
||||
battery_level: "%",
|
||||
},
|
||||
valve: {
|
||||
current_position: "%",
|
||||
},
|
||||
|
||||
@@ -40,7 +40,8 @@ export const searchPlaces = (
|
||||
limit ? `&limit=${limit}` : ""
|
||||
}${addressdetails ? "&addressdetails=1" : ""}&accept-language=${
|
||||
hass.locale.language
|
||||
}&[email protected]`
|
||||
}&[email protected]`,
|
||||
{ headers: { "User-Agent": `HomeAssistant/${hass.config.version}` } }
|
||||
).then((res) => {
|
||||
if (res.ok) {
|
||||
return res.json();
|
||||
@@ -58,7 +59,8 @@ export const reverseGeocode = (
|
||||
location[1]
|
||||
}&accept-language=${hass.locale.language}&zoom=${
|
||||
zoom ?? 18
|
||||
}&format=jsonv2&[email protected]`
|
||||
}&format=jsonv2&[email protected]`,
|
||||
{ headers: { "User-Agent": `HomeAssistant/${hass.config.version}` } }
|
||||
).then((res) => {
|
||||
if (res.ok) {
|
||||
return res.json();
|
||||
|
||||
@@ -160,6 +160,15 @@ export const getRecorderEntityOptions = (
|
||||
entity_id,
|
||||
});
|
||||
|
||||
export const getStatisticIds = (
|
||||
hass: Pick<HomeAssistant, "callWS">,
|
||||
statistic_type?: "mean" | "sum"
|
||||
) =>
|
||||
hass.callWS<StatisticsMetaData[]>({
|
||||
type: "recorder/list_statistic_ids",
|
||||
statistic_type,
|
||||
});
|
||||
|
||||
export const getStatisticMetadata = (
|
||||
hass: HomeAssistant,
|
||||
statistic_ids?: string[]
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { shareInFlightRequest } from "../common/util/share-in-flight-request";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { StatisticsMetaData } from "./recorder";
|
||||
|
||||
type StatisticIdsType = "mean" | "sum";
|
||||
|
||||
type StatisticIdsApi = Pick<HomeAssistant, "callWS">;
|
||||
|
||||
export const getStatisticIds = (
|
||||
hass: StatisticIdsApi,
|
||||
statistic_type?: StatisticIdsType
|
||||
) =>
|
||||
shareInFlightRequest(
|
||||
hass.callWS,
|
||||
`recorder/list_statistic_ids:${statistic_type ?? "all"}`,
|
||||
() =>
|
||||
hass.callWS<StatisticsMetaData[]>({
|
||||
type: "recorder/list_statistic_ids",
|
||||
statistic_type,
|
||||
})
|
||||
);
|
||||
+4
-10
@@ -14,7 +14,6 @@ import {
|
||||
import type { HaEntityPickerEntityFilterFunc } from "./entity/entity";
|
||||
import type { EntityComboBoxItem } from "./entity/entity_picker";
|
||||
import type { EntityRegistryDisplayEntry } from "./entity/entity_registry";
|
||||
import { shareInFlightRequest } from "../common/util/share-in-flight-request";
|
||||
|
||||
export const TARGET_SEPARATOR = "________";
|
||||
|
||||
@@ -55,18 +54,13 @@ export const extractFromTarget = async (
|
||||
target: HassServiceTarget,
|
||||
expandGroup = false,
|
||||
primaryEntitiesOnly = true
|
||||
) => {
|
||||
const request = {
|
||||
type: "extract_from_target" as const,
|
||||
) =>
|
||||
callWS<ExtractFromTargetResult>({
|
||||
type: "extract_from_target",
|
||||
target,
|
||||
expand_group: expandGroup,
|
||||
primary_entities_only: primaryEntitiesOnly,
|
||||
};
|
||||
|
||||
return shareInFlightRequest(callWS, JSON.stringify(request), () =>
|
||||
callWS<ExtractFromTargetResult>(request)
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export const getTargetEntityCount = (target?: HassServiceTarget): number => {
|
||||
const tempTarget = {
|
||||
|
||||
@@ -526,16 +526,6 @@ export const fetchZwaveNetworkStatus = (
|
||||
});
|
||||
};
|
||||
|
||||
/** Node IDs of the nodes each node can reach directly, keyed by node ID. */
|
||||
export const fetchZwaveNetworkNeighbors = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string
|
||||
): Promise<Record<number, number[]>> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/network_neighbors",
|
||||
entry_id,
|
||||
});
|
||||
|
||||
export const fetchZwaveDataCollectionStatus = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import type { LitElement, PropertyValues } from "lit";
|
||||
import type { UnsavedChangesGuard } from "../common/navigate";
|
||||
import {
|
||||
registerUnsavedChangesGuard,
|
||||
unregisterUnsavedChangesGuard,
|
||||
} from "../common/navigate";
|
||||
import { isNavigationClick } from "../common/dom/is-navigation-click";
|
||||
import type { Constructor } from "../types";
|
||||
|
||||
export const PreventUnsavedMixin = <T extends Constructor<LitElement>>(
|
||||
@@ -13,34 +9,45 @@ export const PreventUnsavedMixin = <T extends Constructor<LitElement>>(
|
||||
/** Provided by `DirtyStateProviderMixin`. */
|
||||
declare isDirtyState: boolean;
|
||||
|
||||
private _handleUnload = (e: BeforeUnloadEvent) => e.preventDefault();
|
||||
private _handleClick = async (e: MouseEvent) => {
|
||||
// get the right target, otherwise the composedPath would return <home-assistant> in the new event
|
||||
const target = e.composedPath()[0];
|
||||
if (!isNavigationClick(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
private _unsavedChangesGuard: UnsavedChangesGuard = {
|
||||
isDirty: () => this.isDirtyState,
|
||||
prompt: () => this.promptDiscardChanges(),
|
||||
const result = await this.promptDiscardChanges();
|
||||
if (result) {
|
||||
this._removeListeners();
|
||||
if (target) {
|
||||
const newEvent = new MouseEvent(e.type, e);
|
||||
target.dispatchEvent(newEvent);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
private _handleUnload = (e: BeforeUnloadEvent) => e.preventDefault();
|
||||
|
||||
registerUnsavedChangesGuard(this._unsavedChangesGuard);
|
||||
private _removeListeners() {
|
||||
window.removeEventListener("click", this._handleClick, true);
|
||||
window.removeEventListener("beforeunload", this._handleUnload);
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
if (this.isDirtyState && this.isConnected) {
|
||||
window.addEventListener("click", this._handleClick, true);
|
||||
window.addEventListener("beforeunload", this._handleUnload);
|
||||
} else {
|
||||
window.removeEventListener("beforeunload", this._handleUnload);
|
||||
this._removeListeners();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
|
||||
unregisterUnsavedChangesGuard(this._unsavedChangesGuard);
|
||||
window.removeEventListener("beforeunload", this._handleUnload);
|
||||
this._removeListeners();
|
||||
}
|
||||
|
||||
protected async promptDiscardChanges(): Promise<boolean> {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import "@home-assistant/webawesome/dist/components/tree-item/tree-item";
|
||||
import type WaTreeItem from "@home-assistant/webawesome/dist/components/tree-item/tree-item";
|
||||
import "@home-assistant/webawesome/dist/components/tree/tree";
|
||||
import type { WaSelectionChangeEvent } from "@home-assistant/webawesome/dist/events/selection-change";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
@@ -361,7 +360,6 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
? html`<ha-list-base>${floorAreas}</ha-list-base>`
|
||||
: html`<wa-tree
|
||||
@wa-selection-change=${this._handleSelectionChange}
|
||||
@dblclick=${this._handleDoubleClick}
|
||||
>${floorAreas}</wa-tree
|
||||
>`
|
||||
}`
|
||||
@@ -1446,26 +1444,6 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
this._toggleItem(targetId, false);
|
||||
}
|
||||
|
||||
private _handleDoubleClick(ev: MouseEvent) {
|
||||
// the expand button and non-selectable items already toggle on single click
|
||||
if (
|
||||
ev
|
||||
.composedPath()
|
||||
.some((el) => (el as HTMLElement).classList?.contains("expand-button"))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const item = (ev.target as HTMLElement).closest<
|
||||
WaTreeItem & { target: string }
|
||||
>("wa-tree-item");
|
||||
if (!item || item.isLeaf || item.preventSelection) {
|
||||
return;
|
||||
}
|
||||
// avoid leaving the label text selected by the double click
|
||||
window.getSelection()?.removeAllRanges();
|
||||
this._toggleItem(item.target, !item.expanded);
|
||||
}
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
const configEntries = await getConfigEntries(this.hass);
|
||||
this._configEntryLookup = Object.fromEntries(
|
||||
|
||||
@@ -896,15 +896,10 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
return;
|
||||
}
|
||||
|
||||
this.yamlErrors = undefined;
|
||||
resolve(true);
|
||||
},
|
||||
onClose: () => resolve(false),
|
||||
onDiscard: () => {
|
||||
this.yamlErrors = undefined;
|
||||
this._markDirtyStateClean();
|
||||
resolve(true);
|
||||
},
|
||||
onDiscard: () => resolve(true),
|
||||
entityRegistryUpdate: this.entityRegistryUpdate,
|
||||
entityRegistryEntry: this.registryEntry,
|
||||
title: this.hass.localize(
|
||||
|
||||
@@ -57,7 +57,7 @@ export class CloudAccountOverview extends LitElement {
|
||||
|
||||
private _renderTopCard(): TemplateResult {
|
||||
return html`
|
||||
<ha-card outlined class="summary-card">
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
<div
|
||||
class="thank-you-header"
|
||||
@@ -121,8 +121,8 @@ export class CloudAccountOverview extends LitElement {
|
||||
<p class="muted">
|
||||
${this.hass.localize("ui.panel.config.cloud.account.funding_note")}
|
||||
</p>
|
||||
${this._renderSubscriptionState()}
|
||||
</div>
|
||||
${this._renderSubscriptionState()}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
@@ -132,7 +132,6 @@ export class CloudAccountOverview extends LitElement {
|
||||
case "trial":
|
||||
return html`
|
||||
<ha-alert
|
||||
class="subscription-alert"
|
||||
alert-type="warning"
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.overview.trial_title"
|
||||
@@ -157,7 +156,6 @@ export class CloudAccountOverview extends LitElement {
|
||||
case "canceled":
|
||||
return html`
|
||||
<ha-alert
|
||||
class="subscription-alert"
|
||||
alert-type="warning"
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.overview.canceled_title"
|
||||
@@ -183,7 +181,6 @@ export class CloudAccountOverview extends LitElement {
|
||||
case "expired":
|
||||
return html`
|
||||
<ha-alert
|
||||
class="subscription-alert"
|
||||
alert-type="error"
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.overview.expired_title"
|
||||
@@ -639,15 +636,11 @@ export class CloudAccountOverview extends LitElement {
|
||||
width: auto;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
/* Prevent the embedded .subscription-alert ha-alert from bleeding outside the card */
|
||||
.summary-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
.subscription-alert {
|
||||
ha-alert {
|
||||
display: block;
|
||||
--ha-alert-padding: var(--ha-space-3);
|
||||
margin-top: var(--ha-space-3);
|
||||
}
|
||||
.subscription-alert ha-button[slot="action"] {
|
||||
ha-alert ha-button[slot="action"] {
|
||||
width: max-content;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
+3
-112
@@ -1,4 +1,3 @@
|
||||
import { mdiSpiderWeb } from "@mdi/js";
|
||||
import type {
|
||||
CallbackDataParams,
|
||||
TopLevelFormatterParams,
|
||||
@@ -17,7 +16,6 @@ import type {
|
||||
NetworkLink,
|
||||
NetworkNode,
|
||||
} from "../../../../../components/chart/ha-network-graph";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/input/ha-input-search";
|
||||
import type { HaInputSearch } from "../../../../../components/input/ha-input-search";
|
||||
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
|
||||
@@ -27,7 +25,6 @@ import type {
|
||||
ZWaveJSNodeStatus,
|
||||
} from "../../../../../data/zwave_js";
|
||||
import {
|
||||
fetchZwaveNetworkNeighbors,
|
||||
fetchZwaveNetworkStatus,
|
||||
getNodeIdFromDevice,
|
||||
NodeStatus,
|
||||
@@ -36,7 +33,6 @@ import {
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import { showToast } from "../../../../../util/toast";
|
||||
|
||||
@customElement("zwave_js-network-visualization")
|
||||
export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
|
||||
@@ -59,19 +55,11 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _devices: Record<string, DeviceRegistryEntry> = {};
|
||||
|
||||
@state() private _neighbors?: Record<number, number[]>;
|
||||
|
||||
@state() private _showNeighbors = false;
|
||||
|
||||
@state() private _searchFilter = "";
|
||||
|
||||
// Route statistics reference repeaters by device registry ID
|
||||
private _nodeIdsByDeviceId: Record<string, number> = {};
|
||||
|
||||
private _neighborLinks = new Set<string>();
|
||||
|
||||
private _loadingNeighbors = false;
|
||||
|
||||
public hassSubscribe() {
|
||||
const subscriptions: Promise<UnsubscribeFunc>[] = [];
|
||||
const devices: Record<number, DeviceRegistryEntry> = {};
|
||||
@@ -101,32 +89,6 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
|
||||
return subscriptions;
|
||||
}
|
||||
|
||||
private async _toggleNeighbors() {
|
||||
this._showNeighbors = !this._showNeighbors;
|
||||
if (!this._showNeighbors || this._neighbors || this._loadingNeighbors) {
|
||||
return;
|
||||
}
|
||||
// fetched on demand: reading neighbors turns the radio off briefly
|
||||
this._loadingNeighbors = true;
|
||||
try {
|
||||
this._neighbors = await fetchZwaveNetworkNeighbors(
|
||||
this.hass,
|
||||
this.configEntryId
|
||||
);
|
||||
} catch (err: unknown) {
|
||||
this._showNeighbors = false;
|
||||
showToast(this, {
|
||||
message:
|
||||
(err as { message?: string }).message ??
|
||||
this.hass.localize(
|
||||
"ui.panel.config.zwave_js.visualization.neighbors_error"
|
||||
),
|
||||
});
|
||||
} finally {
|
||||
this._loadingNeighbors = false;
|
||||
}
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._fetchNetworkStatus();
|
||||
@@ -154,23 +116,13 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
|
||||
.searchFilter=${this._searchFilter}
|
||||
.data=${this._getNetworkData(
|
||||
this._nodeStatuses,
|
||||
this._nodeStatistics,
|
||||
this._showNeighbors ? this._neighbors : undefined
|
||||
this._nodeStatistics
|
||||
)}
|
||||
.searchableAttributes=${this._getSearchableAttributes}
|
||||
.tooltipFormatter=${this._tooltipFormatter}
|
||||
@chart-click=${this._handleChartClick}
|
||||
>
|
||||
${!this.narrow ? this._renderInputSearch("search") : nothing}
|
||||
<ha-icon-button
|
||||
slot="button"
|
||||
class=${this._showNeighbors ? "active" : "inactive"}
|
||||
.path=${mdiSpiderWeb}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.visualization.toggle_neighbors"
|
||||
)}
|
||||
@click=${this._toggleNeighbors}
|
||||
></ha-icon-button>
|
||||
</ha-network-graph>
|
||||
</hass-subpage>
|
||||
`;
|
||||
@@ -232,13 +184,6 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
|
||||
sourceDevice?.name_by_user ?? sourceDevice?.name ?? source;
|
||||
const targetName =
|
||||
targetDevice?.name_by_user ?? targetDevice?.name ?? target;
|
||||
if (this._neighborLinks.has(`${source}>${target}`)) {
|
||||
return html`${sourceName} ↔ ${targetName}<br /><b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.visualization.neighbor"
|
||||
)}</b
|
||||
>`;
|
||||
}
|
||||
// links point away from the controller, so the route belongs to the target
|
||||
const stats =
|
||||
this._nodeStatistics[target] ?? this._nodeStatistics[source];
|
||||
@@ -318,8 +263,7 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
|
||||
private _getNetworkData = memoizeOne(
|
||||
(
|
||||
nodeStatuses: Record<number, ZWaveJSNodeStatus>,
|
||||
nodeStatistics: Record<number, ZWaveJSNodeStatisticsUpdatedMessage>,
|
||||
neighbors: Record<number, number[]> | undefined
|
||||
nodeStatistics: Record<number, ZWaveJSNodeStatisticsUpdatedMessage>
|
||||
): NetworkData => {
|
||||
const style = getComputedStyle(this);
|
||||
const nodes: NetworkNode[] = [];
|
||||
@@ -476,49 +420,7 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
});
|
||||
|
||||
// Neighbors are the nodes a node can reach directly. They are symmetric
|
||||
// and carry no signal information, so they fill in the mesh underneath
|
||||
// the measured routes without overriding them.
|
||||
const neighborLinks: NetworkLink[] = [];
|
||||
const neighborKeys = new Set<string>();
|
||||
Object.entries(neighbors ?? {}).forEach(([nodeId, neighborIds]) => {
|
||||
neighborIds.forEach((neighborId) => {
|
||||
const target = String(neighborId);
|
||||
if (!nodeStatuses[neighborId] || target === nodeId) {
|
||||
return;
|
||||
}
|
||||
const [a, b] = [nodeId, target].sort();
|
||||
const key = `${a}>${b}`;
|
||||
if (
|
||||
neighborKeys.has(key) ||
|
||||
links.some(
|
||||
(link) =>
|
||||
(link.source === nodeId && link.target === target) ||
|
||||
(link.source === target && link.target === nodeId)
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
neighborKeys.add(key);
|
||||
neighborLinks.push({
|
||||
source: a,
|
||||
target: b,
|
||||
// equal values in both directions render the link without an arrow
|
||||
value: 1,
|
||||
reverseValue: 1,
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
color: style.getPropertyValue("--disabled-color"),
|
||||
type: "dashed",
|
||||
},
|
||||
// neighbors are plentiful, let the routes shape the layout
|
||||
ignoreForceLayout: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
this._neighborLinks = neighborKeys;
|
||||
|
||||
return { nodes, links: [...neighborLinks, ...links], categories };
|
||||
return { nodes, links, categories };
|
||||
}
|
||||
);
|
||||
|
||||
@@ -558,17 +460,6 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
|
||||
ha-input-search {
|
||||
flex: 1;
|
||||
}
|
||||
/* ha-chart-base can't style re-slotted buttons, so mirror its look */
|
||||
ha-icon-button[slot="button"] {
|
||||
background: var(--card-background-color);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
--ha-icon-button-size: 32px;
|
||||
color: var(--primary-color);
|
||||
border: 1px solid var(--divider-color);
|
||||
}
|
||||
ha-icon-button[slot="button"].inactive {
|
||||
color: var(--state-inactive-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1126,24 +1126,20 @@ export class HaSceneEditor extends DirtyStateProviderMixin<number>()(
|
||||
}
|
||||
|
||||
private async _confirmUnsavedChanged(): Promise<boolean> {
|
||||
if (!this.isDirtyState) {
|
||||
return true;
|
||||
if (this.isDirtyState) {
|
||||
return showConfirmationDialog(this, {
|
||||
title: this.hass!.localize(
|
||||
"ui.panel.config.scene.editor.unsaved_confirm_title"
|
||||
),
|
||||
text: this.hass!.localize(
|
||||
"ui.panel.config.scene.editor.unsaved_confirm_text"
|
||||
),
|
||||
confirmText: this.hass!.localize("ui.common.leave"),
|
||||
dismissText: this.hass!.localize("ui.common.stay"),
|
||||
destructive: true,
|
||||
});
|
||||
}
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass!.localize(
|
||||
"ui.panel.config.scene.editor.unsaved_confirm_title"
|
||||
),
|
||||
text: this.hass!.localize(
|
||||
"ui.panel.config.scene.editor.unsaved_confirm_text"
|
||||
),
|
||||
confirmText: this.hass!.localize("ui.common.leave"),
|
||||
dismissText: this.hass!.localize("ui.common.stay"),
|
||||
destructive: true,
|
||||
});
|
||||
if (confirmed) {
|
||||
this._markDirtyStateClean();
|
||||
}
|
||||
return confirmed;
|
||||
return true;
|
||||
}
|
||||
|
||||
private async _duplicate() {
|
||||
|
||||
@@ -804,15 +804,10 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
return;
|
||||
}
|
||||
|
||||
this.yamlErrors = undefined;
|
||||
resolve(true);
|
||||
},
|
||||
onClose: () => resolve(false),
|
||||
onDiscard: () => {
|
||||
this.yamlErrors = undefined;
|
||||
this._markDirtyStateClean();
|
||||
resolve(true);
|
||||
},
|
||||
onDiscard: () => resolve(true),
|
||||
entityRegistryUpdate: this.entityRegistryUpdate,
|
||||
entityRegistryEntry: this.registryEntry,
|
||||
title: this.hass.localize(
|
||||
|
||||
@@ -52,11 +52,11 @@ import type {
|
||||
} from "../../../../data/recorder";
|
||||
import {
|
||||
clearStatistics,
|
||||
getStatisticIds,
|
||||
StatisticMeanType,
|
||||
updateStatisticsIssues,
|
||||
validateStatistics,
|
||||
} from "../../../../data/recorder";
|
||||
import { getStatisticIds } from "../../../../data/recorder_statistic_ids";
|
||||
import {
|
||||
apiContext,
|
||||
internationalizationContext,
|
||||
|
||||
@@ -395,11 +395,9 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
aliases: entry?.aliases || [],
|
||||
};
|
||||
}
|
||||
if (result[entityId]) {
|
||||
result[entityId].assistants_sortable_key = getAssistantsSortableKey(
|
||||
result[entityId].assistants
|
||||
);
|
||||
}
|
||||
result[entityId].assistants_sortable_key = getAssistantsSortableKey(
|
||||
result[entityId].assistants
|
||||
);
|
||||
});
|
||||
}
|
||||
return Object.values(result);
|
||||
|
||||
@@ -266,6 +266,7 @@ class HaPanelHistory extends LitElement {
|
||||
.startTime=${this._startDate}
|
||||
.endTime=${this._endDate}
|
||||
.narrow=${this.narrow}
|
||||
inside-labels
|
||||
sync-charts
|
||||
>
|
||||
</state-history-charts>
|
||||
@@ -809,7 +810,12 @@ class HaPanelHistory extends LitElement {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden auto;
|
||||
padding: 16px;
|
||||
padding: 16px 8px;
|
||||
}
|
||||
|
||||
/* Line the charts up with the toolbar when there are no axis labels. */
|
||||
:host([narrow]) .results {
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
.progress-wrapper {
|
||||
|
||||
@@ -188,7 +188,7 @@ class HuiHistoryChartCardFeature
|
||||
}
|
||||
if (this._coordinates && !this._coordinates.length) {
|
||||
return html`
|
||||
<div class="container no-history-found">
|
||||
<div class="container">
|
||||
<div class="info">
|
||||
${this.hass!.localize(
|
||||
"ui.components.history_charts.no_history_found"
|
||||
@@ -280,14 +280,6 @@ class HuiHistoryChartCardFeature
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.no-history-found {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
hui-graph-base {
|
||||
width: 100%;
|
||||
--accent-color: var(--feature-color);
|
||||
|
||||
@@ -217,34 +217,22 @@ export class HuiClockCardDigital extends LitElement {
|
||||
"hour minute second"
|
||||
"hour minute am-pm";
|
||||
|
||||
font-size: var(
|
||||
--ha-clock-card-digital-font-size-small,
|
||||
var(--ha-font-size-2xl)
|
||||
);
|
||||
font-size: 1.5rem;
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: 0.8;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.time-title + .time-parts {
|
||||
font-size: var(
|
||||
--ha-clock-card-digital-font-size-small,
|
||||
var(--ha-font-size-2xl)
|
||||
);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.time-parts.size-medium {
|
||||
font-size: var(
|
||||
--ha-clock-card-digital-font-size-medium,
|
||||
calc(48px * var(--ha-font-size-scale))
|
||||
);
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.time-parts.size-large {
|
||||
font-size: var(
|
||||
--ha-clock-card-digital-font-size-large,
|
||||
calc(64px * var(--ha-font-size-scale))
|
||||
);
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
.time-parts.size-medium .time-part.second,
|
||||
|
||||
@@ -165,7 +165,7 @@ export class HuiViewBackgroundEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _currentBackgroundImage(): string | undefined {
|
||||
const background = this._backgroundData(this.config);
|
||||
const background = this._backgroundData(this._config);
|
||||
return typeof background.image === "object"
|
||||
? background.image.media_content_id
|
||||
: background.image;
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type {
|
||||
PictureEntityCardConfig,
|
||||
TileCardConfig,
|
||||
} from "../../cards/types";
|
||||
|
||||
export const computeFavoriteCardConfig = (
|
||||
entityId: string
|
||||
): LovelaceCardConfig => {
|
||||
// A camera tile would only show a thumbnail, so give the picture the room.
|
||||
if (computeDomain(entityId) === "camera") {
|
||||
return {
|
||||
type: "picture-entity",
|
||||
entity: entityId,
|
||||
show_name: false,
|
||||
show_state: false,
|
||||
grid_options: {
|
||||
columns: 6,
|
||||
rows: 2,
|
||||
},
|
||||
} satisfies PictureEntityCardConfig;
|
||||
}
|
||||
|
||||
return {
|
||||
type: "tile",
|
||||
entity: entityId,
|
||||
// Favorites come from all over the home, so name the area.
|
||||
state_content: ["state", "area_name"],
|
||||
show_entity_picture: true,
|
||||
} satisfies TileCardConfig;
|
||||
};
|
||||
@@ -34,7 +34,6 @@ import type {
|
||||
TileCardConfig,
|
||||
UpdatesCardConfig,
|
||||
} from "../../cards/types";
|
||||
import { computeFavoriteCardConfig } from "../helpers/favorite-cards";
|
||||
import {
|
||||
LARGE_SCREEN_CONDITION,
|
||||
SMALL_SCREEN_CONDITION,
|
||||
@@ -272,7 +271,15 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
column_span: maxColumns,
|
||||
cards: [
|
||||
favoritesHeadingCard,
|
||||
...favoriteEntities.map(computeFavoriteCardConfig),
|
||||
...favoriteEntities.map(
|
||||
(entityId) =>
|
||||
({
|
||||
type: "tile",
|
||||
entity: entityId,
|
||||
state_content: ["state", "area_name"],
|
||||
show_entity_picture: true,
|
||||
}) satisfies TileCardConfig
|
||||
),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
+10
-6
@@ -4,9 +4,8 @@ import { isComponentLoaded } from "../../../../common/config/is_component_loaded
|
||||
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||
import { getCommonControlsUsagePrediction } from "../../../../data/usage_prediction";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { HeadingCardConfig } from "../../cards/types";
|
||||
import type { HeadingCardConfig, TileCardConfig } from "../../cards/types";
|
||||
import type { Condition } from "../../common/validate-condition";
|
||||
import { computeFavoriteCardConfig } from "../helpers/favorite-cards";
|
||||
import type { LovelaceStrategyDependency } from "../types";
|
||||
|
||||
const DEFAULT_LIMIT = 8;
|
||||
@@ -26,6 +25,13 @@ export interface CommonControlsSectionStrategyConfig {
|
||||
title_visibilty?: Condition[];
|
||||
}
|
||||
|
||||
const toTileCard = (entity: string): TileCardConfig => ({
|
||||
type: "tile",
|
||||
entity,
|
||||
state_content: ["state", "area_name"],
|
||||
show_entity_picture: true,
|
||||
});
|
||||
|
||||
@customElement("common-controls-section-strategy")
|
||||
export class CommonControlsSectionStrategy extends ReactiveElement {
|
||||
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
|
||||
@@ -57,9 +63,7 @@ export class CommonControlsSectionStrategy extends ReactiveElement {
|
||||
|
||||
// Pinned entities already fill the section, skip the prediction call.
|
||||
if (includedEntities.length >= limit) {
|
||||
section.cards!.push(
|
||||
...includedEntities.slice(0, limit).map(computeFavoriteCardConfig)
|
||||
);
|
||||
section.cards!.push(...includedEntities.slice(0, limit).map(toTileCard));
|
||||
return section;
|
||||
}
|
||||
|
||||
@@ -106,7 +110,7 @@ export class CommonControlsSectionStrategy extends ReactiveElement {
|
||||
return section;
|
||||
}
|
||||
|
||||
section.cards!.push(...entities.map(computeFavoriteCardConfig));
|
||||
section.cards!.push(...entities.map(toTileCard));
|
||||
return section;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,11 @@ import type {
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { SecurityAlertEntityConfig } from "../../../data/frontend";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LogbookCardConfig } from "../../lovelace/cards/types";
|
||||
import type {
|
||||
LogbookCardConfig,
|
||||
TileCardConfig,
|
||||
} from "../../lovelace/cards/types";
|
||||
import { computeAreaTileCardConfig } from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import { computeFavoriteCardConfig } from "../../lovelace/strategies/helpers/favorite-cards";
|
||||
import { computeSecurityAlertCardConfig } from "./security-alerts";
|
||||
|
||||
export interface SecurityViewStrategyConfig {
|
||||
@@ -184,7 +186,15 @@ export class SecurityViewStrategy extends ReactiveElement {
|
||||
),
|
||||
heading_style: "title",
|
||||
},
|
||||
...favoriteEntities.map(computeFavoriteCardConfig),
|
||||
...favoriteEntities.map(
|
||||
(entityId) =>
|
||||
({
|
||||
type: "tile",
|
||||
entity: entityId,
|
||||
state_content: ["state", "area_name"],
|
||||
show_entity_picture: true,
|
||||
}) satisfies TileCardConfig
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8403,10 +8403,7 @@
|
||||
"status": "Status",
|
||||
"version": "Version",
|
||||
"data_rate": "Data rate",
|
||||
"area": "Area",
|
||||
"neighbor": "In range of each other",
|
||||
"toggle_neighbors": "Toggle neighbor connections. Loading neighbor data temporarily turns off the Z-Wave adapter.",
|
||||
"neighbors_error": "Failed to load neighbor connections"
|
||||
"area": "Area"
|
||||
},
|
||||
"node_status": {
|
||||
"0": "Unknown",
|
||||
|
||||
Vendored
-8
@@ -13,11 +13,3 @@ declare module "echarts/lib/util/states" {
|
||||
declare module "echarts/lib/chart/sankey/SankeyView" {
|
||||
export { default } from "echarts/types/src/chart/sankey/SankeyView";
|
||||
}
|
||||
|
||||
declare module "echarts/lib/util/number" {
|
||||
export * from "echarts/types/src/util/number";
|
||||
}
|
||||
|
||||
declare module "echarts/lib/scale/helper" {
|
||||
export * from "echarts/types/src/scale/helper";
|
||||
}
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type {
|
||||
NavigateOptions,
|
||||
UnsavedChangesGuard,
|
||||
} from "../../src/common/navigate";
|
||||
import {
|
||||
canGoBack,
|
||||
goBack,
|
||||
navigate,
|
||||
registerUnsavedChangesGuard,
|
||||
unregisterUnsavedChangesGuard,
|
||||
} from "../../src/common/navigate";
|
||||
import type { NavigateOptions } from "../../src/common/navigate";
|
||||
import { canGoBack, goBack, navigate } from "../../src/common/navigate";
|
||||
|
||||
// navigate() closes open dialogs before touching history.
|
||||
vi.mock("../../src/dialogs/make-dialog-manager", () => ({
|
||||
@@ -63,145 +54,6 @@ describe("navigate", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("unsaved changes guard", () => {
|
||||
const registeredGuards: UnsavedChangesGuard[] = [];
|
||||
|
||||
// Registering through this keeps afterEach able to clean up the module-level
|
||||
// registry, which outlives the test that filled it.
|
||||
const trackGuard = <T extends UnsavedChangesGuard>(guard: T): T => {
|
||||
registerUnsavedChangesGuard(guard);
|
||||
registeredGuards.push(guard);
|
||||
return guard;
|
||||
};
|
||||
|
||||
const registerGuard = (isDirty: boolean, promptResult = true) =>
|
||||
trackGuard({
|
||||
isDirty: vi.fn(() => isDirty),
|
||||
prompt: vi.fn(async () => promptResult),
|
||||
});
|
||||
|
||||
// A guard whose prompt stays open until the test answers it.
|
||||
const trackDeferredGuard = (isDirty: () => boolean) => {
|
||||
let answer!: (value: boolean) => void;
|
||||
const guard = trackGuard({
|
||||
isDirty,
|
||||
prompt: vi.fn(
|
||||
() =>
|
||||
new Promise<boolean>((resolve) => {
|
||||
answer = resolve;
|
||||
})
|
||||
),
|
||||
});
|
||||
return { guard, answerPrompt: (value: boolean) => answer(value) };
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setEntry("/config");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
registeredGuards.splice(0).forEach(unregisterUnsavedChangesGuard);
|
||||
});
|
||||
|
||||
it("navigates without prompting when no guard is dirty", async () => {
|
||||
const guard = registerGuard(false);
|
||||
|
||||
expect(await navigate("/config/areas")).toBe(true);
|
||||
|
||||
expect(window.location.pathname).toEqual("/config/areas");
|
||||
expect(guard.prompt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("prompts a dirty guard and navigates when confirmed", async () => {
|
||||
const guard = registerGuard(true, true);
|
||||
|
||||
expect(await navigate("/config/areas")).toBe(true);
|
||||
|
||||
expect(guard.prompt).toHaveBeenCalledOnce();
|
||||
expect(window.location.pathname).toEqual("/config/areas");
|
||||
});
|
||||
|
||||
it("leaves history untouched when the prompt is declined", async () => {
|
||||
const guard = registerGuard(true, false);
|
||||
const listener = vi.fn();
|
||||
window.addEventListener("location-changed", listener);
|
||||
|
||||
expect(await navigate("/config/areas")).toBe(false);
|
||||
|
||||
window.removeEventListener("location-changed", listener);
|
||||
expect(guard.prompt).toHaveBeenCalledOnce();
|
||||
expect(window.location.pathname).toEqual("/config");
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not prompt when navigating to the current path", async () => {
|
||||
const guard = registerGuard(true, false);
|
||||
|
||||
expect(await navigate("/config")).toBe(true);
|
||||
|
||||
expect(guard.prompt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips a pending prompt once no guard is dirty anymore", async () => {
|
||||
let dirty = true;
|
||||
const { guard, answerPrompt } = trackDeferredGuard(() => dirty);
|
||||
|
||||
const first = navigate("/config/areas");
|
||||
dirty = false;
|
||||
|
||||
expect(await navigate("/config/devices/dashboard")).toBe(true);
|
||||
expect(window.location.pathname).toEqual("/config/devices/dashboard");
|
||||
expect(guard.prompt).toHaveBeenCalledOnce();
|
||||
|
||||
answerPrompt(true);
|
||||
await first;
|
||||
});
|
||||
|
||||
it("drops a navigation superseded while its prompt was open", async () => {
|
||||
let dirty = true;
|
||||
const { answerPrompt } = trackDeferredGuard(() => dirty);
|
||||
|
||||
const superseded = navigate("/config/areas");
|
||||
dirty = false;
|
||||
await navigate("/config/devices/dashboard");
|
||||
|
||||
answerPrompt(true);
|
||||
|
||||
expect(await superseded).toBe(false);
|
||||
expect(window.location.pathname).toEqual("/config/devices/dashboard");
|
||||
});
|
||||
|
||||
it("shares one pending prompt between concurrent navigations", async () => {
|
||||
const { guard, answerPrompt } = trackDeferredGuard(() => true);
|
||||
|
||||
const first = navigate("/config/areas");
|
||||
const second = navigate("/config/devices/dashboard");
|
||||
answerPrompt(true);
|
||||
|
||||
await Promise.all([first, second]);
|
||||
|
||||
expect(guard.prompt).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("stops prompting once the guard is unregistered", async () => {
|
||||
const guard = registerGuard(true, false);
|
||||
unregisterUnsavedChangesGuard(guard);
|
||||
|
||||
expect(await navigate("/config/areas")).toBe(true);
|
||||
|
||||
expect(guard.prompt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not prompt for goBack's fallback navigation", async () => {
|
||||
const guard = registerGuard(true, false);
|
||||
|
||||
await goBack("/config/cloud/account");
|
||||
|
||||
expect(window.location.pathname).toEqual("/config/cloud/account");
|
||||
expect(guard.prompt).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("goBack", () => {
|
||||
beforeEach(() => {
|
||||
setEntry("/config/cloud/remote");
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { shareInFlightRequest } from "../../../src/common/util/share-in-flight-request";
|
||||
|
||||
const deferred = <T>() => {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (reason?: unknown) => void;
|
||||
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
|
||||
return { promise, resolve, reject };
|
||||
};
|
||||
|
||||
describe("shareInFlightRequest", () => {
|
||||
it("shares an in-flight request for the same owner and key", async () => {
|
||||
const owner = {};
|
||||
const request = deferred<number>();
|
||||
const fetcher = vi.fn(() => request.promise);
|
||||
|
||||
const first = shareInFlightRequest(owner, "resource:a", fetcher);
|
||||
const second = shareInFlightRequest(owner, "resource:a", fetcher);
|
||||
|
||||
expect(first).toBe(second);
|
||||
expect(fetcher).toHaveBeenCalledTimes(1);
|
||||
|
||||
request.resolve(42);
|
||||
|
||||
await expect(first).resolves.toBe(42);
|
||||
await expect(second).resolves.toBe(42);
|
||||
});
|
||||
|
||||
it("does not share requests with different keys", async () => {
|
||||
const owner = {};
|
||||
const fetcherA = vi.fn(async () => "a");
|
||||
const fetcherB = vi.fn(async () => "b");
|
||||
|
||||
await Promise.all([
|
||||
shareInFlightRequest(owner, "resource:a", fetcherA),
|
||||
shareInFlightRequest(owner, "resource:b", fetcherB),
|
||||
]);
|
||||
|
||||
expect(fetcherA).toHaveBeenCalledTimes(1);
|
||||
expect(fetcherB).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not share requests between different owners", async () => {
|
||||
const firstOwner = {};
|
||||
const secondOwner = {};
|
||||
const firstFetcher = vi.fn(async () => "first");
|
||||
const secondFetcher = vi.fn(async () => "second");
|
||||
|
||||
await Promise.all([
|
||||
shareInFlightRequest(firstOwner, "resource:a", firstFetcher),
|
||||
shareInFlightRequest(secondOwner, "resource:a", secondFetcher),
|
||||
]);
|
||||
|
||||
expect(firstFetcher).toHaveBeenCalledTimes(1);
|
||||
expect(secondFetcher).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("forgets a request after it resolves", async () => {
|
||||
const owner = {};
|
||||
const fetcher = vi.fn(async () => 42);
|
||||
|
||||
await shareInFlightRequest(owner, "resource:a", fetcher);
|
||||
await shareInFlightRequest(owner, "resource:a", fetcher);
|
||||
|
||||
expect(fetcher).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("forgets a request after it rejects", async () => {
|
||||
const owner = {};
|
||||
const fetcher = vi
|
||||
.fn<() => Promise<number>>()
|
||||
.mockRejectedValueOnce(new Error("failed"))
|
||||
.mockResolvedValueOnce(42);
|
||||
|
||||
await expect(
|
||||
shareInFlightRequest(owner, "resource:a", fetcher)
|
||||
).rejects.toThrow("failed");
|
||||
|
||||
await expect(
|
||||
shareInFlightRequest(owner, "resource:a", fetcher)
|
||||
).resolves.toBe(42);
|
||||
|
||||
expect(fetcher).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("deep-freezes resolved results", async () => {
|
||||
const owner = {};
|
||||
const result = { items: ["a"] };
|
||||
const fetcher = vi.fn(async () => result);
|
||||
|
||||
const shared = await shareInFlightRequest(owner, "resource:a", fetcher);
|
||||
|
||||
expect(shared).toBe(result);
|
||||
expect(Object.isFrozen(shared)).toBe(true);
|
||||
expect(Object.isFrozen((shared as typeof result).items)).toBe(true);
|
||||
expect(() => {
|
||||
(shared as typeof result).items.push("b");
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -1,103 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { SVGRenderer } from "echarts/renderers";
|
||||
import echarts from "../../../src/resources/echarts/echarts";
|
||||
import { createYAxisPrecisionBounds } from "../../../src/components/chart/y-axis-fraction-digits";
|
||||
|
||||
// jsdom has no canvas, and zrender reaches for one to measure label text.
|
||||
HTMLCanvasElement.prototype.getContext = (() => ({
|
||||
measureText: () => ({ width: 10 }),
|
||||
})) as any;
|
||||
|
||||
// The app registers the canvas renderer; jsdom needs the SVG one.
|
||||
echarts.use([SVGRenderer]);
|
||||
|
||||
// The gap is opened by ECharts' own tick rounding, not by our helper, so these
|
||||
// assert the extent ECharts actually renders rather than what we hand it.
|
||||
const renderExtent = (
|
||||
data: number[],
|
||||
yAxis: Record<string, unknown>,
|
||||
nextYAxis?: Record<string, unknown>
|
||||
): [number, number] => {
|
||||
const chart = echarts.init(null, null, {
|
||||
ssr: true,
|
||||
renderer: "svg",
|
||||
width: 400,
|
||||
height: 300,
|
||||
});
|
||||
const option = (axis: Record<string, unknown>) => ({
|
||||
xAxis: { type: "category", data: data.map((_, index) => String(index)) },
|
||||
yAxis: { type: "value", ...axis },
|
||||
series: [{ type: "line", data }],
|
||||
});
|
||||
chart.setOption(option(yAxis));
|
||||
if (nextYAxis) {
|
||||
chart.setOption(option(nextYAxis), { replaceMerge: ["series"] });
|
||||
}
|
||||
// getModel() is internal, but it is the only way to read the rendered extent.
|
||||
const extent = (chart as any)
|
||||
.getModel()
|
||||
.getComponent("yAxis")
|
||||
.axis.scale.getExtent();
|
||||
chart.dispose();
|
||||
return extent;
|
||||
};
|
||||
|
||||
const withGap = (includeZero = false, unit?: string) => ({
|
||||
scale: !includeZero,
|
||||
...createYAxisPrecisionBounds({
|
||||
includeZero,
|
||||
unit,
|
||||
onFractionDigits: () => undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
describe("Y-axis tick nudge", () => {
|
||||
it("opens a gap under data that lands exactly on a tick", () => {
|
||||
// The reported bug: a thermostat's states are quantized, so the minimum is
|
||||
// an exact tick and the HVAC action band has nothing to fill.
|
||||
expect(renderExtent([18, 21.2, 19], { scale: true })).toEqual([18, 21.5]);
|
||||
expect(renderExtent([18, 21.2, 19], withGap())).toEqual([17.5, 21.5]);
|
||||
});
|
||||
|
||||
it("leaves an axis with real headroom untouched", () => {
|
||||
// Its minimum sits well clear of the tick below it, so the rounding it
|
||||
// already gets is enough and the widened extent floors to the same place.
|
||||
const data = [18.3, 21.2, 19.5];
|
||||
expect(renderExtent(data, withGap())).toEqual(
|
||||
renderExtent(data, { scale: true })
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps a non-negative series anchored at zero", () => {
|
||||
expect(renderExtent([0, 3500, 1200], withGap())[0]).toBe(0);
|
||||
// Without the clamp the nudged minimum floors a whole interval below zero.
|
||||
expect(
|
||||
renderExtent([0, 3500, 1200], {
|
||||
scale: true,
|
||||
boundaryGap: [0.02, 0.02],
|
||||
})[0]
|
||||
).toBe(-1000);
|
||||
});
|
||||
|
||||
it("keeps the window ECharts gives a constant series", () => {
|
||||
expect(renderExtent([21, 21, 21], withGap())).toEqual(
|
||||
renderExtent([21, 21, 21], { scale: true })
|
||||
);
|
||||
// A series flat at zero has no magnitude to expand by.
|
||||
expect(renderExtent([0, 0, 0], withGap())).toEqual(
|
||||
renderExtent([0, 0, 0], { scale: true })
|
||||
);
|
||||
});
|
||||
|
||||
it("does not round a percentage axis past 100", () => {
|
||||
const battery = [20, 100, 60, 20, 80];
|
||||
expect(renderExtent(battery, withGap())).toEqual([0, 120]);
|
||||
expect(renderExtent(battery, withGap(false, "%"))).toEqual([0, 100]);
|
||||
});
|
||||
|
||||
it("re-anchors at zero when a chart switches to a zero-anchored type", () => {
|
||||
// setOption merges the Y axis, so a gap that is only emitted conditionally
|
||||
// survives the switch and leaves the bars floating.
|
||||
expect(renderExtent([20, 500, 300], withGap(), withGap(true))[0]).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -72,25 +72,13 @@ describe("computeYAxisFractionDigits", () => {
|
||||
});
|
||||
|
||||
describe("createYAxisPrecisionBounds", () => {
|
||||
const makeBounds = (
|
||||
options: Omit<
|
||||
Parameters<typeof createYAxisPrecisionBounds>[0],
|
||||
"onFractionDigits"
|
||||
> = {}
|
||||
) => {
|
||||
const onFractionDigits = vi.fn();
|
||||
return {
|
||||
...createYAxisPrecisionBounds({ ...options, onFractionDigits }),
|
||||
onFractionDigits,
|
||||
};
|
||||
};
|
||||
|
||||
it("computes digits from the visible extent when no bounds are set", () => {
|
||||
const { min, max, onFractionDigits } = makeBounds();
|
||||
const onFractionDigits = vi.fn();
|
||||
const { min, max } = createYAxisPrecisionBounds({ onFractionDigits });
|
||||
|
||||
// Zoomed-out extent -> coarse precision, callbacks leave scaling to ECharts
|
||||
expect(min({ min: 10, max: 100 })).toBeUndefined();
|
||||
expect(max({ min: 10, max: 100 })).toBeUndefined();
|
||||
expect(min({ min: 0, max: 100 })).toBeUndefined();
|
||||
expect(max({ min: 0, max: 100 })).toBeUndefined();
|
||||
expect(onFractionDigits).toHaveBeenLastCalledWith(0);
|
||||
|
||||
// Zoomed-in narrow extent -> more decimals so ticks stay distinct
|
||||
@@ -99,7 +87,12 @@ describe("createYAxisPrecisionBounds", () => {
|
||||
});
|
||||
|
||||
it("computes digits from numeric bounds and returns them unchanged", () => {
|
||||
const { min, max, onFractionDigits } = makeBounds({ min: 1.85, max: 2 });
|
||||
const onFractionDigits = vi.fn();
|
||||
const { min, max } = createYAxisPrecisionBounds({
|
||||
min: 1.85,
|
||||
max: 2,
|
||||
onFractionDigits,
|
||||
});
|
||||
|
||||
// Fixed bounds pin the range, so the visible extent is ignored
|
||||
expect(min({ min: 1.9, max: 1.95 })).toBe(1.85);
|
||||
@@ -108,9 +101,11 @@ describe("createYAxisPrecisionBounds", () => {
|
||||
});
|
||||
|
||||
it("resolves function bounds and passes their result through", () => {
|
||||
const { min, max, onFractionDigits } = makeBounds({
|
||||
const onFractionDigits = vi.fn();
|
||||
const { min, max } = createYAxisPrecisionBounds({
|
||||
min: ({ min: dataMin }) => dataMin - 1,
|
||||
max: ({ max: dataMax }) => dataMax + 1,
|
||||
onFractionDigits,
|
||||
});
|
||||
|
||||
expect(min({ min: 10, max: 11 })).toBe(9);
|
||||
@@ -120,7 +115,11 @@ describe("createYAxisPrecisionBounds", () => {
|
||||
});
|
||||
|
||||
it("unions the extent with zero for anchored axes", () => {
|
||||
const { min, onFractionDigits } = makeBounds({ includeZero: true });
|
||||
const onFractionDigits = vi.fn();
|
||||
const { min } = createYAxisPrecisionBounds({
|
||||
includeZero: true,
|
||||
onFractionDigits,
|
||||
});
|
||||
|
||||
// Data sits at 20..25, but a bar axis renders from 0 -> coarse precision
|
||||
min({ min: 20, max: 25 });
|
||||
@@ -132,102 +131,10 @@ describe("createYAxisPrecisionBounds", () => {
|
||||
});
|
||||
|
||||
it("does not over-pad when the visible extent collapses to noise", () => {
|
||||
const { min, onFractionDigits } = makeBounds();
|
||||
const onFractionDigits = vi.fn();
|
||||
const { min } = createYAxisPrecisionBounds({ onFractionDigits });
|
||||
|
||||
min({ min: 0.3, max: 0.3 + 1e-15 });
|
||||
expect(onFractionDigits).toHaveBeenLastCalledWith(1);
|
||||
});
|
||||
|
||||
it("widens the extent so ECharts always rounds out a gap", () => {
|
||||
const { min, max, boundaryGap, splitNumber } = makeBounds();
|
||||
|
||||
// The gap is applied by ECharts, so the bounds stay auto-scaled.
|
||||
expect(boundaryGap).toEqual([0.02, 0.02]);
|
||||
// Pinned rather than assumed, since the precision here is derived from it.
|
||||
expect(splitNumber).toBe(5);
|
||||
expect(min({ min: 18, max: 21.2 })).toBeUndefined();
|
||||
expect(max({ min: 18, max: 21.2 })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("anchors at zero instead of widening a non-negative series below it", () => {
|
||||
const { min, max } = makeBounds();
|
||||
|
||||
// Without this, ECharts floors the widened -0.0035 to a full interval below
|
||||
// zero, inventing a negative region on a power chart.
|
||||
expect(min({ min: 0, max: 3500 })).toBe(0);
|
||||
expect(max({ min: 0, max: 3500 })).toBeUndefined();
|
||||
|
||||
// A minimum clear of the gap is left to ECharts.
|
||||
expect(min({ min: 100, max: 3500 })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("anchors an all-negative series at zero from above", () => {
|
||||
const { min, max } = makeBounds();
|
||||
|
||||
expect(max({ min: -3500, max: 0 })).toBe(0);
|
||||
expect(min({ min: -3500, max: 0 })).toBeUndefined();
|
||||
|
||||
// Mixed-sign data is widened at both ends.
|
||||
expect(min({ min: -20, max: 20 })).toBeUndefined();
|
||||
expect(max({ min: -20, max: 20 })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps the expanded window ECharts gives a constant series", () => {
|
||||
const { min, max, onFractionDigits } = makeBounds();
|
||||
|
||||
// ECharts only expands a flat extent while its bounds are equal, so the gap
|
||||
// would otherwise collapse this to 20.99998..21.00002 at five decimals.
|
||||
expect(min({ min: 21, max: 21 })).toBe(10);
|
||||
expect(max({ min: 21, max: 21 })).toBe(35);
|
||||
expect(onFractionDigits).toHaveBeenLastCalledWith(0);
|
||||
|
||||
// A series flat at zero has no magnitude to expand by.
|
||||
expect(min({ min: 0, max: 0 })).toBe(0);
|
||||
expect(max({ min: 0, max: 0 })).toBe(1);
|
||||
});
|
||||
|
||||
it("stops a percentage axis at 100 the way it stops at zero", () => {
|
||||
const { max } = makeBounds({ unit: "%" });
|
||||
|
||||
// A battery that reaches full would otherwise round out to an axis
|
||||
// labelled up to 120%.
|
||||
expect(max({ min: 20, max: 100 })).toBe(100);
|
||||
expect(max({ min: 20, max: 99 })).toBe(100);
|
||||
|
||||
// Well clear of the ceiling, so it still gets its gap.
|
||||
expect(max({ min: 40, max: 60 })).toBeUndefined();
|
||||
|
||||
// Not every % sensor is bounded: power factor is signed and can read over
|
||||
// 100, and a series already past the ceiling must be left alone.
|
||||
expect(max({ min: -80, max: 140 })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("holds a constant percentage under the ceiling too", () => {
|
||||
const { max } = makeBounds({ unit: "%" });
|
||||
|
||||
// A device left on the charger reads a flat 100%, and the expansion a flat
|
||||
// series gets is a fraction of its magnitude, so it overshoots on its own.
|
||||
expect(max({ min: 100, max: 100 })).toBe(100);
|
||||
expect(max({ min: 80, max: 80 })).toBe(100);
|
||||
|
||||
// Far enough below that the expansion never reaches the ceiling.
|
||||
expect(max({ min: 54, max: 54 })).toBe(90);
|
||||
});
|
||||
|
||||
it("only treats a percentage as bounded", () => {
|
||||
// The same extent without the unit is widened as usual.
|
||||
expect(makeBounds().max({ min: 20, max: 100 })).toBeUndefined();
|
||||
expect(
|
||||
makeBounds({ unit: "W" }).max({ min: 20, max: 100 })
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("never widens a zero-anchored axis", () => {
|
||||
const { min, max, boundaryGap } = makeBounds({ includeZero: true });
|
||||
|
||||
// Widening below zero would defeat ECharts' anchoring and float the bars.
|
||||
expect(boundaryGap).toEqual([0, 0]);
|
||||
expect(min({ min: 0, max: 3500 })).toBeUndefined();
|
||||
expect(max({ min: 20, max: 20 })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -173,31 +173,4 @@ describe("ha-target-picker-item-row target extraction", () => {
|
||||
expect(entries!.referenced_devices).toEqual(["dev_1"]);
|
||||
expect(entries!.referenced_entities).toEqual(["light.on_dev1"]);
|
||||
});
|
||||
|
||||
it("does not mutate the extracted target result", async () => {
|
||||
const result = extractResult({
|
||||
referenced_areas: ["area_missing"],
|
||||
referenced_devices: ["dev_missing"],
|
||||
referenced_entities: ["light.missing"],
|
||||
});
|
||||
|
||||
const entries = await extractedBy(
|
||||
result,
|
||||
{
|
||||
areas: {},
|
||||
devices: {},
|
||||
entities: {},
|
||||
},
|
||||
{ type: "floor", itemId: "floor_1" }
|
||||
);
|
||||
|
||||
expect(entries).toBeDefined();
|
||||
expect(entries).not.toBe(result);
|
||||
expect(entries!.referenced_areas).toEqual([]);
|
||||
expect(entries!.referenced_devices).toEqual([]);
|
||||
expect(entries!.referenced_entities).toEqual([]);
|
||||
expect(result.referenced_areas).toEqual(["area_missing"]);
|
||||
expect(result.referenced_devices).toEqual(["dev_missing"]);
|
||||
expect(result.referenced_entities).toEqual(["light.missing"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
import { getStatisticIds } from "../../src/data/recorder_statistic_ids";
|
||||
import type { StatisticsMetaData } from "../../src/data/recorder";
|
||||
|
||||
const createHass = (callWS = vi.fn()) =>
|
||||
({
|
||||
callWS,
|
||||
}) as unknown as Pick<HomeAssistant, "callWS">;
|
||||
|
||||
const deferred = <T>() => {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (reason?: unknown) => void;
|
||||
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
|
||||
return { promise, resolve, reject };
|
||||
};
|
||||
|
||||
describe("getStatisticIds", () => {
|
||||
it("shares concurrent requests for the same statistic type", async () => {
|
||||
const request = deferred<StatisticsMetaData[]>();
|
||||
const callWS = vi.fn().mockReturnValue(request.promise);
|
||||
const hass = createHass(callWS);
|
||||
|
||||
const first = getStatisticIds(hass);
|
||||
const second = getStatisticIds(hass);
|
||||
|
||||
expect(callWS).toHaveBeenCalledTimes(1);
|
||||
expect(second).toBe(first);
|
||||
|
||||
request.resolve([]);
|
||||
await Promise.all([first, second]);
|
||||
});
|
||||
|
||||
it("works with callers that only expose callWS", async () => {
|
||||
const request = deferred<StatisticsMetaData[]>();
|
||||
const callWS = vi.fn().mockReturnValue(request.promise);
|
||||
const api = { callWS } as Pick<HomeAssistant, "callWS">;
|
||||
|
||||
const first = getStatisticIds(api);
|
||||
const second = getStatisticIds(api);
|
||||
|
||||
expect(callWS).toHaveBeenCalledTimes(1);
|
||||
expect(second).toBe(first);
|
||||
|
||||
request.resolve([]);
|
||||
await Promise.all([first, second]);
|
||||
});
|
||||
|
||||
it("shares requests across callers with the same callWS", async () => {
|
||||
const request = deferred<StatisticsMetaData[]>();
|
||||
const callWS = vi.fn().mockReturnValue(request.promise);
|
||||
const firstHass = createHass(callWS);
|
||||
const secondHass = createHass(callWS);
|
||||
|
||||
const first = getStatisticIds(firstHass);
|
||||
const second = getStatisticIds(secondHass);
|
||||
|
||||
expect(callWS).toHaveBeenCalledTimes(1);
|
||||
expect(second).toBe(first);
|
||||
|
||||
request.resolve([]);
|
||||
await Promise.all([first, second]);
|
||||
});
|
||||
|
||||
it("does not share requests for different statistic types", async () => {
|
||||
const callWS = vi.fn().mockResolvedValue([]);
|
||||
const hass = createHass(callWS);
|
||||
|
||||
await Promise.all([
|
||||
getStatisticIds(hass),
|
||||
getStatisticIds(hass, "mean"),
|
||||
getStatisticIds(hass, "sum"),
|
||||
]);
|
||||
|
||||
expect(callWS).toHaveBeenCalledTimes(3);
|
||||
expect(callWS).toHaveBeenNthCalledWith(1, {
|
||||
type: "recorder/list_statistic_ids",
|
||||
statistic_type: undefined,
|
||||
});
|
||||
expect(callWS).toHaveBeenNthCalledWith(2, {
|
||||
type: "recorder/list_statistic_ids",
|
||||
statistic_type: "mean",
|
||||
});
|
||||
expect(callWS).toHaveBeenNthCalledWith(3, {
|
||||
type: "recorder/list_statistic_ids",
|
||||
statistic_type: "sum",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not share requests across different callWS owners", async () => {
|
||||
const firstRequest = deferred<StatisticsMetaData[]>();
|
||||
const secondRequest = deferred<StatisticsMetaData[]>();
|
||||
|
||||
const firstCallWS = vi.fn().mockReturnValue(firstRequest.promise);
|
||||
const secondCallWS = vi.fn().mockReturnValue(secondRequest.promise);
|
||||
|
||||
const first = getStatisticIds(createHass(firstCallWS));
|
||||
const second = getStatisticIds(createHass(secondCallWS));
|
||||
|
||||
expect(firstCallWS).toHaveBeenCalledTimes(1);
|
||||
expect(secondCallWS).toHaveBeenCalledTimes(1);
|
||||
expect(second).not.toBe(first);
|
||||
|
||||
firstRequest.resolve([]);
|
||||
secondRequest.resolve([]);
|
||||
await Promise.all([first, second]);
|
||||
});
|
||||
|
||||
it("fetches again after the previous request settles", async () => {
|
||||
const callWS = vi.fn().mockResolvedValue([]);
|
||||
const hass = createHass(callWS);
|
||||
|
||||
await getStatisticIds(hass, "sum");
|
||||
await getStatisticIds(hass, "sum");
|
||||
|
||||
expect(callWS).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("retries after a failed request", async () => {
|
||||
const callWS = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error("failed"))
|
||||
.mockResolvedValueOnce([]);
|
||||
const hass = createHass(callWS);
|
||||
|
||||
await expect(getStatisticIds(hass)).rejects.toThrow("failed");
|
||||
await getStatisticIds(hass);
|
||||
|
||||
expect(callWS).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { extractFromTarget } from "../../src/data/target";
|
||||
|
||||
type CallWS = Parameters<typeof extractFromTarget>[0];
|
||||
|
||||
const callWS = () => vi.fn().mockResolvedValue({}) as unknown as CallWS;
|
||||
|
||||
describe("extractFromTarget in-flight sharing", () => {
|
||||
it("shares identical concurrent requests", async () => {
|
||||
const ws = callWS();
|
||||
const target = { area_id: ["bathroom"] };
|
||||
|
||||
await Promise.all([
|
||||
extractFromTarget(ws, target),
|
||||
extractFromTarget(ws, target),
|
||||
]);
|
||||
|
||||
expect(ws).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not share different targets", async () => {
|
||||
const ws = callWS();
|
||||
|
||||
await Promise.all([
|
||||
extractFromTarget(ws, { area_id: ["bathroom"] }),
|
||||
extractFromTarget(ws, { area_id: ["kitchen"] }),
|
||||
]);
|
||||
|
||||
expect(ws).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("does not share different expandGroup values", async () => {
|
||||
const ws = callWS();
|
||||
const target = { area_id: ["bathroom"] };
|
||||
|
||||
await Promise.all([
|
||||
extractFromTarget(ws, target, false),
|
||||
extractFromTarget(ws, target, true),
|
||||
]);
|
||||
|
||||
expect(ws).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("does not share different primaryEntitiesOnly values", async () => {
|
||||
const ws = callWS();
|
||||
const target = { area_id: ["bathroom"] };
|
||||
|
||||
await Promise.all([
|
||||
extractFromTarget(ws, target, false, true),
|
||||
extractFromTarget(ws, target, false, false),
|
||||
]);
|
||||
|
||||
expect(ws).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("does not share between different callWS owners", async () => {
|
||||
const first = callWS();
|
||||
const second = callWS();
|
||||
const target = { area_id: ["bathroom"] };
|
||||
|
||||
await Promise.all([
|
||||
extractFromTarget(first, target),
|
||||
extractFromTarget(second, target),
|
||||
]);
|
||||
|
||||
expect(first).toHaveBeenCalledTimes(1);
|
||||
expect(second).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("fetches again after the request settles", async () => {
|
||||
const ws = callWS();
|
||||
const target = { area_id: ["bathroom"] };
|
||||
|
||||
await extractFromTarget(ws, target);
|
||||
await extractFromTarget(ws, target);
|
||||
|
||||
expect(ws).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -1,143 +0,0 @@
|
||||
import { LitElement } from "lit";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { navigate } from "../../src/common/navigate";
|
||||
import { DirtyStateProviderMixin } from "../../src/mixins/dirty-state-provider-mixin";
|
||||
import { PreventUnsavedMixin } from "../../src/mixins/prevent-unsaved-mixin";
|
||||
|
||||
// navigate() closes open dialogs before touching history.
|
||||
vi.mock("../../src/dialogs/make-dialog-manager", () => ({
|
||||
closeAllDialogs: vi.fn(async () => true),
|
||||
}));
|
||||
|
||||
interface TestState {
|
||||
name: string;
|
||||
}
|
||||
|
||||
// Mirrors the editors: DirtyStateProviderMixin wraps PreventUnsavedMixin.
|
||||
class TestPreventUnsaved extends DirtyStateProviderMixin<TestState>()(
|
||||
PreventUnsavedMixin(LitElement)
|
||||
) {
|
||||
public promptResponse = true;
|
||||
|
||||
public promptCalls = 0;
|
||||
|
||||
public initialize(value: TestState) {
|
||||
this._initDirtyTracking({ type: "shallow" }, value);
|
||||
}
|
||||
|
||||
public setValue(value: TestState) {
|
||||
this._updateDirtyState(value);
|
||||
}
|
||||
|
||||
public markClean() {
|
||||
this._markDirtyStateClean();
|
||||
}
|
||||
|
||||
protected async promptDiscardChanges(): Promise<boolean> {
|
||||
this.promptCalls++;
|
||||
return this.promptResponse;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("test-prevent-unsaved", TestPreventUnsaved);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"test-prevent-unsaved": TestPreventUnsaved;
|
||||
}
|
||||
}
|
||||
|
||||
const setEntry = (path: string) => {
|
||||
window.history.replaceState(null, "", path);
|
||||
};
|
||||
|
||||
const mountClean = async () => {
|
||||
const element = document.createElement("test-prevent-unsaved");
|
||||
document.body.append(element);
|
||||
element.initialize({ name: "Kitchen" });
|
||||
await element.updateComplete;
|
||||
return element;
|
||||
};
|
||||
|
||||
const mountDirty = async () => {
|
||||
const element = await mountClean();
|
||||
element.setValue({ name: "Bedroom" });
|
||||
await element.updateComplete;
|
||||
return element;
|
||||
};
|
||||
|
||||
describe("PreventUnsavedMixin", () => {
|
||||
beforeEach(() => {
|
||||
setEntry("/config/automation/edit/1234");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.querySelectorAll("test-prevent-unsaved").forEach((element) => {
|
||||
element.remove();
|
||||
});
|
||||
});
|
||||
|
||||
it("blocks navigation while dirty when the prompt is declined", async () => {
|
||||
const element = await mountDirty();
|
||||
element.promptResponse = false;
|
||||
|
||||
expect(await navigate("/config/scene/dashboard")).toBe(false);
|
||||
|
||||
expect(element.promptCalls).toBe(1);
|
||||
expect(window.location.pathname).toEqual("/config/automation/edit/1234");
|
||||
});
|
||||
|
||||
it("navigates while dirty when the prompt is confirmed", async () => {
|
||||
const element = await mountDirty();
|
||||
|
||||
expect(await navigate("/config/scene/dashboard")).toBe(true);
|
||||
|
||||
expect(element.promptCalls).toBe(1);
|
||||
expect(window.location.pathname).toEqual("/config/scene/dashboard");
|
||||
});
|
||||
|
||||
it("does not prompt while clean", async () => {
|
||||
const element = await mountClean();
|
||||
|
||||
expect(await navigate("/config/scene/dashboard")).toBe(true);
|
||||
|
||||
expect(element.promptCalls).toBe(0);
|
||||
expect(window.location.pathname).toEqual("/config/scene/dashboard");
|
||||
});
|
||||
|
||||
it("does not prompt after changes are marked clean, before the next render", async () => {
|
||||
const element = await mountDirty();
|
||||
element.markClean();
|
||||
|
||||
expect(await navigate("/config/scene/dashboard")).toBe(true);
|
||||
|
||||
expect(element.promptCalls).toBe(0);
|
||||
expect(window.location.pathname).toEqual("/config/scene/dashboard");
|
||||
});
|
||||
|
||||
it("stops guarding after being disconnected", async () => {
|
||||
const element = await mountDirty();
|
||||
element.promptResponse = false;
|
||||
element.remove();
|
||||
|
||||
expect(await navigate("/config/scene/dashboard")).toBe(true);
|
||||
|
||||
expect(element.promptCalls).toBe(0);
|
||||
expect(window.location.pathname).toEqual("/config/scene/dashboard");
|
||||
});
|
||||
|
||||
it("arms beforeunload only while dirty", async () => {
|
||||
const element = await mountDirty();
|
||||
|
||||
let event = new Event("beforeunload", { cancelable: true });
|
||||
window.dispatchEvent(event);
|
||||
expect(event.defaultPrevented).toBe(true);
|
||||
|
||||
element.setValue({ name: "Kitchen" });
|
||||
await element.updateComplete;
|
||||
|
||||
event = new Event("beforeunload", { cancelable: true });
|
||||
window.dispatchEvent(event);
|
||||
expect(event.defaultPrevented).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -5368,7 +5368,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sveltejs/acorn-typescript@npm:^1.0.13":
|
||||
"@sveltejs/acorn-typescript@npm:^1.0.10":
|
||||
version: 1.0.13
|
||||
resolution: "@sveltejs/acorn-typescript@npm:1.0.13"
|
||||
peerDependencies:
|
||||
@@ -5951,105 +5951,105 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/eslint-plugin@npm:8.68.0":
|
||||
version: 8.68.0
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.68.0"
|
||||
"@typescript-eslint/eslint-plugin@npm:8.67.0":
|
||||
version: 8.67.0
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.67.0"
|
||||
dependencies:
|
||||
"@eslint-community/regexpp": "npm:^4.12.2"
|
||||
"@typescript-eslint/scope-manager": "npm:8.68.0"
|
||||
"@typescript-eslint/type-utils": "npm:8.68.0"
|
||||
"@typescript-eslint/utils": "npm:8.68.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.68.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.67.0"
|
||||
"@typescript-eslint/type-utils": "npm:8.67.0"
|
||||
"@typescript-eslint/utils": "npm:8.67.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.67.0"
|
||||
ignore: "npm:^7.0.5"
|
||||
natural-compare: "npm:^1.4.0"
|
||||
ts-api-utils: "npm:^2.5.0"
|
||||
peerDependencies:
|
||||
"@typescript-eslint/parser": ^8.68.0
|
||||
"@typescript-eslint/parser": ^8.67.0
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/8776fd8ffde8eceae3b1aeca57e528e8ee61a683223c3bef455ca3c5e28e52275e6cd6ebf8ee284a46883e6220bbf297312084e1ee85e00cef017a78c169a674
|
||||
checksum: 10/d53f9e51c6be98ff4cdcb94de24e2951f5e2b7a1dfc56c4f8a2d81e61346c511c0478117626d9cfd5d5d63f5e7a0e0653dc1abe1e7d926a3f9806aaf1fa11bd2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/parser@npm:8.68.0":
|
||||
version: 8.68.0
|
||||
resolution: "@typescript-eslint/parser@npm:8.68.0"
|
||||
"@typescript-eslint/parser@npm:8.67.0":
|
||||
version: 8.67.0
|
||||
resolution: "@typescript-eslint/parser@npm:8.67.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager": "npm:8.68.0"
|
||||
"@typescript-eslint/types": "npm:8.68.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.68.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.68.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.67.0"
|
||||
"@typescript-eslint/types": "npm:8.67.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.67.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.67.0"
|
||||
debug: "npm:^4.4.3"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/979ae0d2c6a391fd5b9b0218189fda5112ff0b5ab2538eef1da4cd77ac8ae46f9b590db9779f2614bd193105b40cf12635b565c840f4cdbbb4a683e24cf5ec4b
|
||||
checksum: 10/fa156ade066d0886ea0daa0f872940cdb450a2b5dbd1687e0df64ec19aac86711dde51e17e6b20e72387aa38daec811736821e9db950e8557affa7faea63d65c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/project-service@npm:8.68.0":
|
||||
version: 8.68.0
|
||||
resolution: "@typescript-eslint/project-service@npm:8.68.0"
|
||||
"@typescript-eslint/project-service@npm:8.67.0":
|
||||
version: 8.67.0
|
||||
resolution: "@typescript-eslint/project-service@npm:8.67.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils": "npm:^8.68.0"
|
||||
"@typescript-eslint/types": "npm:^8.68.0"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:^8.67.0"
|
||||
"@typescript-eslint/types": "npm:^8.67.0"
|
||||
debug: "npm:^4.4.3"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/37521df3731f9069c7df55362342dd0027f43947241de33c2a1c029b4553fe092e2ac69a534153aaf08c862e6b2390d864cb6c88a0714fc276395390df276fb6
|
||||
checksum: 10/3beccdee1e74060eac4e6c5e7edd14a800c8c98e3313dbfe5838a357e83babc22c41c3ebe637c331c4b93db2da50f5479ccf4677db7b6b1a760f3cd27795b36f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/scope-manager@npm:8.68.0":
|
||||
version: 8.68.0
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.68.0"
|
||||
"@typescript-eslint/scope-manager@npm:8.67.0":
|
||||
version: 8.67.0
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.67.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.68.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.68.0"
|
||||
checksum: 10/494d499315b4f3ab8eaa114aa53a4b2b9f533b82eb007cd5b5617cececfd8ad6938edb74f734479ae6e40a4086f510b6b984de8e66d06c518cb47817549db780
|
||||
"@typescript-eslint/types": "npm:8.67.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.67.0"
|
||||
checksum: 10/a0ca8d52e6c5b2538f7df860acd5a36437cce8e6d5bcededdfde04c02489f4b523cb178b502c266d138cff6758ea375ebe311b2b6af373b127ded4876a697ffb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@npm:8.68.0, @typescript-eslint/tsconfig-utils@npm:^8.68.0":
|
||||
version: 8.68.0
|
||||
resolution: "@typescript-eslint/tsconfig-utils@npm:8.68.0"
|
||||
"@typescript-eslint/tsconfig-utils@npm:8.67.0, @typescript-eslint/tsconfig-utils@npm:^8.67.0":
|
||||
version: 8.67.0
|
||||
resolution: "@typescript-eslint/tsconfig-utils@npm:8.67.0"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/b527a7c5deaad40bb72f78eec1bfea5997a7e792e64666984605e9a167246cf856c8d40290a7b3d39cefb3a242538e22a3a4fc136132e4669a314887e2de5f08
|
||||
checksum: 10/736d0ab8a273b4e4e17d412b390386eec0171cd3f7ba451bacd92c4f931051bba2049e040fdae84603ea0e07430afb05b49c6d8b6d429f9012d3c6b03eb69f54
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/type-utils@npm:8.68.0":
|
||||
version: 8.68.0
|
||||
resolution: "@typescript-eslint/type-utils@npm:8.68.0"
|
||||
"@typescript-eslint/type-utils@npm:8.67.0":
|
||||
version: 8.67.0
|
||||
resolution: "@typescript-eslint/type-utils@npm:8.67.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.68.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.68.0"
|
||||
"@typescript-eslint/utils": "npm:8.68.0"
|
||||
"@typescript-eslint/types": "npm:8.67.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.67.0"
|
||||
"@typescript-eslint/utils": "npm:8.67.0"
|
||||
debug: "npm:^4.4.3"
|
||||
ts-api-utils: "npm:^2.5.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/ed6b6fe74eee017a79c9dbfa72b3a08748dac8de5702314494d8849feee516c20e2907ad730fd82e50be81c5984c575b29e35eb8b5c99091445b4bc5102ce836
|
||||
checksum: 10/a0b943e5a8f3c2e8490ec28599eb94561d7ad029fcf42f2e9a0e8d2685ce625898371cd0a7d3a906fe8b4077d72fee8329cad512a5b0521bc2167f7d88a984fb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/types@npm:8.68.0, @typescript-eslint/types@npm:^8.56.0, @typescript-eslint/types@npm:^8.68.0":
|
||||
version: 8.68.0
|
||||
resolution: "@typescript-eslint/types@npm:8.68.0"
|
||||
checksum: 10/f9337e0263d95e6edca6e364f9b1f70d5840b9648b5aa0d02065bcee5809a50c92d024ba17b458182ec4e7fffe5efd05584f1667b1e43ff3e7121b218283a207
|
||||
"@typescript-eslint/types@npm:8.67.0, @typescript-eslint/types@npm:^8.56.0, @typescript-eslint/types@npm:^8.67.0":
|
||||
version: 8.67.0
|
||||
resolution: "@typescript-eslint/types@npm:8.67.0"
|
||||
checksum: 10/8edc1a14a52c7566409c19b34fb72ba3a62a3b409c98b1d496de9fcd83179fb128c8df2fde391c199316a54cdc5d314ba70ce7b440161dd850b73a44f3e54e6d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/typescript-estree@npm:8.68.0":
|
||||
version: 8.68.0
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.68.0"
|
||||
"@typescript-eslint/typescript-estree@npm:8.67.0":
|
||||
version: 8.67.0
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.67.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service": "npm:8.68.0"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:8.68.0"
|
||||
"@typescript-eslint/types": "npm:8.68.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.68.0"
|
||||
"@typescript-eslint/project-service": "npm:8.67.0"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:8.67.0"
|
||||
"@typescript-eslint/types": "npm:8.67.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.67.0"
|
||||
debug: "npm:^4.4.3"
|
||||
minimatch: "npm:^10.2.2"
|
||||
semver: "npm:^7.7.3"
|
||||
@@ -6057,32 +6057,32 @@ __metadata:
|
||||
ts-api-utils: "npm:^2.5.0"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/47d75cbc9c10842aeb3c1cc8d0625bd578b3b43b75d3dc86d3543a739b2fc8d0b524fd86b5af1f1493a3f0ae1b9e65c3200bf01a6bde0162efe6414708eac553
|
||||
checksum: 10/31c5be812f67c997b9d1e9324c4289b6268a12e9948492403fdd9c9b5df12dcc00f845ae93ae37c78f717583519060175f2147f165b65112eef1e40acf44ad1d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/utils@npm:8.68.0":
|
||||
version: 8.68.0
|
||||
resolution: "@typescript-eslint/utils@npm:8.68.0"
|
||||
"@typescript-eslint/utils@npm:8.67.0":
|
||||
version: 8.67.0
|
||||
resolution: "@typescript-eslint/utils@npm:8.67.0"
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils": "npm:^4.9.1"
|
||||
"@typescript-eslint/scope-manager": "npm:8.68.0"
|
||||
"@typescript-eslint/types": "npm:8.68.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.68.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.67.0"
|
||||
"@typescript-eslint/types": "npm:8.67.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.67.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/6581a1e7ced23cce82279df001d643844c97408bc84826da77ec77d52b77047004b202ff8cfcfbc0b0fef15bf2cafb5ae21bafbda24c9b58102ce634f66c2af9
|
||||
checksum: 10/8703468cdea3630275c6faacc031e7e4bd7fb8f8988e80ab987811ae3c63a9a9677496cee2b4811f49cb9bc9e6f6c0bdb91cb213353019ce31df0a2b351cea51
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/visitor-keys@npm:8.68.0":
|
||||
version: 8.68.0
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.68.0"
|
||||
"@typescript-eslint/visitor-keys@npm:8.67.0":
|
||||
version: 8.67.0
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.67.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.68.0"
|
||||
"@typescript-eslint/types": "npm:8.67.0"
|
||||
eslint-visitor-keys: "npm:^5.0.0"
|
||||
checksum: 10/d802bc2c4569495f3d83f9bcf169e3a432ac1c6672e243399886409dbc55d7fa30375d9d0b2cf0d9466b81fc2b01b991483eb56e8cdb8141721009978d3b078b
|
||||
checksum: 10/c92c9b202105a8f398ab2ad4be58c8c8fd9af6b5ddd28cdcd57d75f7f04f69561a56a05aac9a008668c64faa7222ebb3fefe19097923a18eb2b9f7a90809a5be
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -6787,7 +6787,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"acorn@npm:^8.10.0, acorn@npm:^8.11.0, acorn@npm:^8.15.0, acorn@npm:^8.16.0, acorn@npm:^8.18.0":
|
||||
"acorn@npm:^8.10.0, acorn@npm:^8.11.0, acorn@npm:^8.15.0, acorn@npm:^8.16.0, acorn@npm:^8.17.0":
|
||||
version: 8.18.0
|
||||
resolution: "acorn@npm:8.18.0"
|
||||
bin:
|
||||
@@ -9097,9 +9097,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint@npm:10.9.1":
|
||||
version: 10.9.1
|
||||
resolution: "eslint@npm:10.9.1"
|
||||
"eslint@npm:10.9.0":
|
||||
version: 10.9.0
|
||||
resolution: "eslint@npm:10.9.0"
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils": "npm:^4.8.0"
|
||||
"@eslint-community/regexpp": "npm:^4.12.2"
|
||||
@@ -9138,7 +9138,7 @@ __metadata:
|
||||
optional: true
|
||||
bin:
|
||||
eslint: bin/eslint.js
|
||||
checksum: 10/4cee237c7d2c445a05330e595b18521d7452fd3f419241bf1ccf81c769a6761c10ca5a06988197482d4842ed1a26b1dcb51b9526408990cee84559e6c08cb3ab
|
||||
checksum: 10/1cd63bbf4f2c003ed759ab6ae9e2030be0abafdcd7b9ffb5be9e9c3aa3ea6142e6af0a337aac21044957750807fde06c9d052e2c9734cca6792dc2d095f9c180
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -10206,7 +10206,7 @@ __metadata:
|
||||
echarts: "npm:6.1.0"
|
||||
echarts-extension-chart2music: "npm:0.1.1"
|
||||
element-internals-polyfill: "npm:3.0.2"
|
||||
eslint: "npm:10.9.1"
|
||||
eslint: "npm:10.9.0"
|
||||
eslint-config-prettier: "npm:10.1.8"
|
||||
eslint-import-resolver-webpack: "npm:0.13.11"
|
||||
eslint-plugin-import-x: "npm:4.17.1"
|
||||
@@ -10229,7 +10229,7 @@ __metadata:
|
||||
husky: "npm:9.1.7"
|
||||
idb-keyval: "npm:6.3.0"
|
||||
intl-messageformat: "npm:11.2.14"
|
||||
js-yaml: "npm:5.4.0"
|
||||
js-yaml: "npm:5.3.0"
|
||||
jsdom: "npm:30.0.1"
|
||||
jszip: "npm:3.10.1"
|
||||
leaflet: "npm:1.9.4"
|
||||
@@ -10246,9 +10246,9 @@ __metadata:
|
||||
luxon: "npm:3.7.2"
|
||||
map-stream: "npm:0.0.7"
|
||||
maplibre-gl: "npm:5.24.0"
|
||||
marked: "npm:18.0.11"
|
||||
marked: "npm:18.0.10"
|
||||
memoize-one: "npm:6.0.0"
|
||||
minify-literals: "npm:2.2.0"
|
||||
minify-literals: "npm:2.1.0"
|
||||
node-vibrant: "npm:4.0.4"
|
||||
object-hash: "npm:3.0.0"
|
||||
pinst: "npm:3.0.0"
|
||||
@@ -10269,7 +10269,7 @@ __metadata:
|
||||
tinykeys: "patch:tinykeys@npm%3A4.0.0#~/.yarn/patches/tinykeys-npm-4.0.0-a6ca3fd771.patch"
|
||||
ts-lit-plugin: "npm:2.0.2"
|
||||
typescript: "npm:6.0.3"
|
||||
typescript-eslint: "npm:8.68.0"
|
||||
typescript-eslint: "npm:8.67.0"
|
||||
vite-tsconfig-paths: "npm:6.1.1"
|
||||
vitest: "npm:4.1.11"
|
||||
webpack-stats-plugin: "npm:1.1.3"
|
||||
@@ -10327,9 +10327,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"html-minifier-next@npm:^8.1.0":
|
||||
version: 8.1.0
|
||||
resolution: "html-minifier-next@npm:8.1.0"
|
||||
"html-minifier-next@npm:^7.0.0":
|
||||
version: 7.5.3
|
||||
resolution: "html-minifier-next@npm:7.5.3"
|
||||
dependencies:
|
||||
commander: "npm:^15.0.0"
|
||||
entities: "npm:^8.0.0"
|
||||
@@ -10344,7 +10344,7 @@ __metadata:
|
||||
bin:
|
||||
hmn: cli.js
|
||||
html-minifier-next: cli.js
|
||||
checksum: 10/5badf1abd2c6fb9ada2f7dd4929be74f43155510298f7af323b0e39a2c20e2a33ed8de507f105bcea389118f2813e7776b559cf4b2b6397537c5871aa27f2125
|
||||
checksum: 10/fe4ad84f5c577c2de936c7aad5c0a7619c5e7ce3763f3c2e15f1ecc9629d89ebc578a38c0e44883cc0388253de262962610f7dbc3101f8d16e363c477a6a9b07
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -11177,14 +11177,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-yaml@npm:5.4.0":
|
||||
version: 5.4.0
|
||||
resolution: "js-yaml@npm:5.4.0"
|
||||
"js-yaml@npm:5.3.0":
|
||||
version: 5.3.0
|
||||
resolution: "js-yaml@npm:5.3.0"
|
||||
dependencies:
|
||||
argparse: "npm:^2.0.1"
|
||||
bin:
|
||||
js-yaml: bin/js-yaml.mjs
|
||||
checksum: 10/d8216083212772b203a1ee16be2ea96a1638c6d7f0ff769ba4da274b5ad29e858bd26d96761feb8937fce1aa47e8acfa79b2cd21a8ced724cc5555c11b4d056c
|
||||
checksum: 10/6c7e8ea8dd5643d9df73f4aa796f3572537284e9b819b74ef919467452bfa4624f8f7477cfc775c91db7e2f32387133b1b6c4e95aef9702be6bb042ae554884a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -11600,7 +11600,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lightningcss@npm:1.33.0, lightningcss@npm:^1.33.0":
|
||||
"lightningcss@npm:1.33.0, lightningcss@npm:^1.32.0, lightningcss@npm:^1.33.0":
|
||||
version: 1.33.0
|
||||
resolution: "lightningcss@npm:1.33.0"
|
||||
dependencies:
|
||||
@@ -11854,15 +11854,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"magic-string@npm:^1.2.2":
|
||||
version: 1.2.2
|
||||
resolution: "magic-string@npm:1.2.2"
|
||||
dependencies:
|
||||
"@jridgewell/sourcemap-codec": "npm:^1.5.5"
|
||||
checksum: 10/23514d9fb744d30689037e41440125b6c4bc3fb5b8e6d992798e59f93d7452276f223d062ba9b1ce78fbee5f9cc365c1e0ae3c6babddb74cb3aa9074adb57b03
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"magicast@npm:^0.5.2":
|
||||
version: 0.5.4
|
||||
resolution: "magicast@npm:0.5.4"
|
||||
@@ -11944,12 +11935,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"marked@npm:18.0.11":
|
||||
version: 18.0.11
|
||||
resolution: "marked@npm:18.0.11"
|
||||
"marked@npm:18.0.10":
|
||||
version: 18.0.10
|
||||
resolution: "marked@npm:18.0.10"
|
||||
bin:
|
||||
marked: bin/marked.js
|
||||
checksum: 10/6da69f17820c7442358d751e902d3e0e06fef957e1178d634db938cf339c4504399c9f11a953b0a116b1e5bebc1c7fb52726777cc6c351f2c9f39e4a8276e21a
|
||||
checksum: 10/0d4b560e0773fd6ba30a4e7560ba7ae1f05d47b23926ad8337d9f80c598dfdbffdf2f81e773a1789e8bc9d9c2c2d3d0c9143a54d32581c336a3bbb1bad80461c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -12067,16 +12058,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minify-literals@npm:2.2.0":
|
||||
version: 2.2.0
|
||||
resolution: "minify-literals@npm:2.2.0"
|
||||
"minify-literals@npm:2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "minify-literals@npm:2.1.0"
|
||||
dependencies:
|
||||
"@sveltejs/acorn-typescript": "npm:^1.0.13"
|
||||
acorn: "npm:^8.18.0"
|
||||
html-minifier-next: "npm:^8.1.0"
|
||||
lightningcss: "npm:^1.33.0"
|
||||
magic-string: "npm:^1.2.2"
|
||||
checksum: 10/73f17955820675f405b95810f3bf861a05baf426300e8ba46d174987ee45366c885ef1281c40842025529343a4ca71b6fd8294ce674bccbd58685e4a917b9448
|
||||
"@sveltejs/acorn-typescript": "npm:^1.0.10"
|
||||
acorn: "npm:^8.17.0"
|
||||
html-minifier-next: "npm:^7.0.0"
|
||||
lightningcss: "npm:^1.32.0"
|
||||
magic-string: "npm:^0.30.21"
|
||||
checksum: 10/95bcd25afcf638c00bcd8981c2e6409be8c2feb4fd8f954bc9a547b2ade917810cef17bb075ad95e5ceaac6fffc8e625c161ab77b2363c36de53da6d139de872
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -15232,18 +15223,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript-eslint@npm:8.68.0":
|
||||
version: 8.68.0
|
||||
resolution: "typescript-eslint@npm:8.68.0"
|
||||
"typescript-eslint@npm:8.67.0":
|
||||
version: 8.67.0
|
||||
resolution: "typescript-eslint@npm:8.67.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin": "npm:8.68.0"
|
||||
"@typescript-eslint/parser": "npm:8.68.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.68.0"
|
||||
"@typescript-eslint/utils": "npm:8.68.0"
|
||||
"@typescript-eslint/eslint-plugin": "npm:8.67.0"
|
||||
"@typescript-eslint/parser": "npm:8.67.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.67.0"
|
||||
"@typescript-eslint/utils": "npm:8.67.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/451c64296ac5f48e4621a03324f5aaef4a26f1dd9d4df63bd3cb07f136e8379185a800731f83634eb5d909c696fad820ba74d31ab3d7cf263328504275d1305b
|
||||
checksum: 10/4efc2cea5b067f65d58e010fda3888ec94f2e67ebfa289be78c3a02458787b7125cf8f6095776b912005fb69e8748ad4018777b16436a0658957c163ef213bf9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user