mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-18 20:50:04 +00:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c4d6b50b0 | ||
|
|
f828c636a2 | ||
|
|
e91337b320 | ||
|
|
8ab7c118a9 | ||
|
|
0eafb1f4b4 | ||
|
|
9a7722a02e | ||
|
|
3e184eeb39 | ||
|
|
61be4e2952 | ||
|
|
315cf97e56 | ||
|
|
771a8aa4c9 | ||
|
|
d9aa649854 | ||
|
|
7a087cd6d0 | ||
|
|
32a30468bb | ||
|
|
0ace3cedfb | ||
|
|
aba7478e26 | ||
|
|
c8b2e5160a | ||
|
|
4c46ca0918 | ||
|
|
0483106f61 |
@@ -32,12 +32,12 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4
|
||||
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
|
||||
with:
|
||||
languages: javascript-typescript
|
||||
build-mode: none
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4
|
||||
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
|
||||
with:
|
||||
category: "/language:javascript-typescript"
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"_comment": "Initial JS budget (raw/uncompressed bytes) for the cold-load critical entrypoints. Enforced by build-scripts/check-bundle-size.cjs in CI. Re-seed after an intentional change with `--update --headroom=<percent>`.",
|
||||
"frontend-modern": {
|
||||
"app": 595204,
|
||||
"core": 57741,
|
||||
"authorize": 576928,
|
||||
"onboarding": 685964
|
||||
"app": 585518,
|
||||
"core": 57048,
|
||||
"authorize": 552423,
|
||||
"onboarding": 666818
|
||||
},
|
||||
"frontend-legacy": {
|
||||
"app": 861452,
|
||||
"core": 258557,
|
||||
"authorize": 834356,
|
||||
"onboarding": 1001360
|
||||
"app": 887384,
|
||||
"core": 244482,
|
||||
"authorize": 844995,
|
||||
"onboarding": 1008816
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,11 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
|
||||
cost_sensors: {},
|
||||
solar_forecast_domains: [],
|
||||
}));
|
||||
hass.mockWS("energy/validate", () => ({
|
||||
energy_sources: Array.from({ length: 6 }, () => []),
|
||||
device_consumption: Array.from({ length: 6 }, () => []),
|
||||
device_consumption_water: Array.from({ length: 2 }, () => []),
|
||||
}));
|
||||
hass.mockWS(
|
||||
"energy/fossil_energy_consumption",
|
||||
({ period }): FossilEnergyConsumption => ({
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockSensor = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS(
|
||||
"sensor/device_class_convertible_units",
|
||||
({ device_class }: { device_class: string }) => ({
|
||||
units: device_class === "energy" ? ["kWh"] : ["W"],
|
||||
})
|
||||
);
|
||||
hass.mockWS("sensor/numeric_device_classes", () => ({
|
||||
numeric_device_classes: [
|
||||
"volume_storage",
|
||||
|
||||
@@ -15,7 +15,6 @@ const ALL_FEATURES =
|
||||
VacuumEntityFeature.STOP +
|
||||
VacuumEntityFeature.RETURN_HOME +
|
||||
VacuumEntityFeature.FAN_SPEED +
|
||||
VacuumEntityFeature.BATTERY +
|
||||
VacuumEntityFeature.STATUS +
|
||||
VacuumEntityFeature.LOCATE +
|
||||
VacuumEntityFeature.CLEAN_SPOT +
|
||||
@@ -28,8 +27,6 @@ const ENTITIES = [
|
||||
attributes: {
|
||||
friendly_name: "Full featured vacuum",
|
||||
supported_features: ALL_FEATURES,
|
||||
battery_level: 85,
|
||||
battery_icon: "mdi:battery-80",
|
||||
fan_speed: "balanced",
|
||||
fan_speed_list: ["silent", "standard", "balanced", "turbo", "max"],
|
||||
status: "Charged",
|
||||
@@ -41,8 +38,6 @@ const ENTITIES = [
|
||||
attributes: {
|
||||
friendly_name: "Cleaning vacuum",
|
||||
supported_features: ALL_FEATURES,
|
||||
battery_level: 62,
|
||||
battery_icon: "mdi:battery-60",
|
||||
fan_speed: "turbo",
|
||||
fan_speed_list: ["silent", "standard", "balanced", "turbo", "max"],
|
||||
status: "Cleaning bedroom",
|
||||
@@ -58,10 +53,7 @@ const ENTITIES = [
|
||||
VacuumEntityFeature.START +
|
||||
VacuumEntityFeature.PAUSE +
|
||||
VacuumEntityFeature.STOP +
|
||||
VacuumEntityFeature.RETURN_HOME +
|
||||
VacuumEntityFeature.BATTERY,
|
||||
battery_level: 23,
|
||||
battery_icon: "mdi:battery-20",
|
||||
VacuumEntityFeature.RETURN_HOME,
|
||||
status: "Returning to dock",
|
||||
},
|
||||
},
|
||||
@@ -96,8 +88,6 @@ const ENTITIES = [
|
||||
attributes: {
|
||||
friendly_name: "Paused vacuum",
|
||||
supported_features: ALL_FEATURES,
|
||||
battery_level: 45,
|
||||
battery_icon: "mdi:battery-40",
|
||||
fan_speed: "standard",
|
||||
fan_speed_list: ["silent", "standard", "balanced", "turbo", "max"],
|
||||
status: "Paused",
|
||||
|
||||
+4
-5
@@ -103,7 +103,7 @@
|
||||
"echarts": "6.1.0",
|
||||
"element-internals-polyfill": "3.0.2",
|
||||
"fuse.js": "7.5.0",
|
||||
"hls.js": "1.6.17",
|
||||
"hls.js": "1.7.0",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.3.0",
|
||||
"intl-messageformat": "11.2.13",
|
||||
@@ -151,8 +151,8 @@
|
||||
"@octokit/plugin-retry": "8.1.1",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@playwright/test": "1.62.1",
|
||||
"@rsdoctor/rspack-plugin": "1.6.1",
|
||||
"@rspack/core": "2.1.9",
|
||||
"@rsdoctor/rspack-plugin": "1.6.2",
|
||||
"@rspack/core": "2.1.10",
|
||||
"@rspack/dev-server": "2.2.0",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.26",
|
||||
@@ -186,7 +186,7 @@
|
||||
"fs-extra": "11.4.0",
|
||||
"generate-license-file": "4.2.1",
|
||||
"glob": "13.0.6",
|
||||
"globals": "17.9.0",
|
||||
"globals": "17.11.0",
|
||||
"gulp": "5.0.1",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
"gulp-rename": "2.1.0",
|
||||
@@ -223,7 +223,6 @@
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "2.1.2",
|
||||
"@fullcalendar/daygrid": "6.1.21",
|
||||
"globals": "17.9.0",
|
||||
"tslib": "2.8.1",
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
|
||||
},
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
import type {
|
||||
ReactiveController,
|
||||
ReactiveControllerHost,
|
||||
} from "@lit/reactive-element/reactive-controller";
|
||||
import { css, type LitElement } from "lit";
|
||||
import type { Ref } from "lit/directives/ref";
|
||||
import { parseAnimationDuration } from "../util/parse-animation-duration";
|
||||
|
||||
type FilterPanelHost = ReactiveControllerHost &
|
||||
LitElement & { expanded: boolean };
|
||||
|
||||
const EASING = "cubic-bezier(0.4, 0, 0.2, 1)";
|
||||
|
||||
/**
|
||||
* Layout the controller relies on: the filter is a flex column made of its
|
||||
* header (`ha-expansion-panel`) and a `.content` wrapper. Collapsed, it is as
|
||||
* tall as its header; expanded, it fills what is left of the pane and hands
|
||||
* that space down to the list through the wrapper.
|
||||
*/
|
||||
export const filterPanelStyles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
:host([expanded]) {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
flex: none;
|
||||
--ha-card-border-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
ha-expansion-panel::part(summary) {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const panels = new Set<FilterPanelController>();
|
||||
|
||||
let pending: Set<FilterPanelController> | undefined;
|
||||
|
||||
const flush = () => {
|
||||
const batch = [...pending!].filter((panel) => panel.host.isConnected);
|
||||
pending = undefined;
|
||||
batch.forEach((panel) => panel.prepare());
|
||||
batch.forEach((panel) => panel.measure());
|
||||
batch.forEach((panel) => panel.play());
|
||||
};
|
||||
|
||||
// Filters that change in the same frame (one closing while another opens) are
|
||||
// animated as one batch: every filter is measured before any of them has
|
||||
// changed the DOM, and the closing ones are parked at their final height so
|
||||
// the opening one reads its own final height from the real layout.
|
||||
const enqueue = (panel: FilterPanelController) => {
|
||||
if (!pending) {
|
||||
pending = new Set();
|
||||
panels.forEach((other) => other.snapshot());
|
||||
requestAnimationFrame(flush);
|
||||
}
|
||||
pending.add(panel);
|
||||
};
|
||||
|
||||
const clearInlineStyles = (element?: HTMLElement) => {
|
||||
element?.style.removeProperty("height");
|
||||
element?.style.removeProperty("flex");
|
||||
element?.style.removeProperty("overflow");
|
||||
};
|
||||
|
||||
/**
|
||||
* Animates a filter of the filter pane between its collapsed and expanded
|
||||
* heights whenever `expanded` changes, and tells the host when to render its
|
||||
* content: from the moment it expands until its collapse animation has ended.
|
||||
*
|
||||
* During the animation the content keeps its final size and the host clips
|
||||
* it, so the list is revealed rather than resized.
|
||||
*/
|
||||
export class FilterPanelController implements ReactiveController {
|
||||
public showContent = false;
|
||||
|
||||
public host: FilterPanelHost;
|
||||
|
||||
private _content: Ref<HTMLElement>;
|
||||
|
||||
private _expanded?: boolean;
|
||||
|
||||
private _first = 0;
|
||||
|
||||
private _last = 0;
|
||||
|
||||
private _contentHeight = 0;
|
||||
|
||||
private _animation?: Animation;
|
||||
|
||||
constructor(host: FilterPanelHost, content: Ref<HTMLElement>) {
|
||||
this.host = host;
|
||||
this._content = content;
|
||||
host.addController(this);
|
||||
}
|
||||
|
||||
public hostConnected() {
|
||||
panels.add(this);
|
||||
}
|
||||
|
||||
public hostDisconnected() {
|
||||
panels.delete(this);
|
||||
this._animation?.cancel();
|
||||
this._animation = undefined;
|
||||
clearInlineStyles(this.host);
|
||||
clearInlineStyles(this._content.value);
|
||||
}
|
||||
|
||||
public hostUpdate() {
|
||||
const expanded = this.host.expanded;
|
||||
if (this._expanded === undefined) {
|
||||
this._expanded = expanded;
|
||||
this.showContent = expanded;
|
||||
return;
|
||||
}
|
||||
if (expanded === this._expanded) {
|
||||
return;
|
||||
}
|
||||
this._expanded = expanded;
|
||||
if (!this.host.isConnected) {
|
||||
this.showContent = expanded;
|
||||
return;
|
||||
}
|
||||
if (expanded) {
|
||||
this.showContent = true;
|
||||
}
|
||||
enqueue(this);
|
||||
}
|
||||
|
||||
public snapshot() {
|
||||
this._first = this.host.getBoundingClientRect().height;
|
||||
}
|
||||
|
||||
public prepare() {
|
||||
this._animation?.cancel();
|
||||
this._animation = undefined;
|
||||
const host = this.host;
|
||||
const content = this._content.value;
|
||||
clearInlineStyles(host);
|
||||
clearInlineStyles(content);
|
||||
if (host.expanded) {
|
||||
return;
|
||||
}
|
||||
this._last =
|
||||
host.getBoundingClientRect().height -
|
||||
(content?.getBoundingClientRect().height ?? 0);
|
||||
this._contentHeight = this._first - this._last;
|
||||
host.style.flex = "none";
|
||||
host.style.height = `${this._last}px`;
|
||||
}
|
||||
|
||||
public measure() {
|
||||
if (!this.host.expanded) {
|
||||
return;
|
||||
}
|
||||
this._last = this.host.getBoundingClientRect().height;
|
||||
this._contentHeight =
|
||||
this._content.value?.getBoundingClientRect().height ?? 0;
|
||||
}
|
||||
|
||||
public play() {
|
||||
const host = this.host;
|
||||
if (this._first === this._last) {
|
||||
this._finish();
|
||||
return;
|
||||
}
|
||||
const content = this._content.value;
|
||||
host.style.flex = "none";
|
||||
host.style.overflow = "hidden";
|
||||
if (content) {
|
||||
content.style.flex = "none";
|
||||
content.style.height = `${this._contentHeight}px`;
|
||||
}
|
||||
const animation = host.animate(
|
||||
[{ height: `${this._first}px` }, { height: `${this._last}px` }],
|
||||
{
|
||||
duration:
|
||||
parseAnimationDuration(
|
||||
getComputedStyle(host).getPropertyValue(
|
||||
"--ha-animation-duration-normal"
|
||||
)
|
||||
) || 250,
|
||||
easing: EASING,
|
||||
fill: "forwards",
|
||||
}
|
||||
);
|
||||
animation.onfinish = () => this._finish();
|
||||
this._animation = animation;
|
||||
}
|
||||
|
||||
private _finish() {
|
||||
if (!this.host.expanded) {
|
||||
this.showContent = false;
|
||||
this.host.requestUpdate();
|
||||
}
|
||||
clearInlineStyles(this.host);
|
||||
clearInlineStyles(this._content.value);
|
||||
this._animation?.cancel();
|
||||
this._animation = undefined;
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,8 @@ export class HaButtonToggleGroup extends LitElement {
|
||||
@property({ type: Boolean, reflect: true, attribute: "full-width" })
|
||||
public fullWidth = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property() public variant:
|
||||
"brand" | "neutral" | "success" | "warning" | "danger" = "brand";
|
||||
|
||||
@@ -57,6 +59,7 @@ export class HaButtonToggleGroup extends LitElement {
|
||||
.value=${button.value}
|
||||
@click=${this._handleClick}
|
||||
.title=${button.label}
|
||||
.disabled=${this.disabled}
|
||||
.appearance=${this.active === button.value ? "accent" : "filled"}
|
||||
>
|
||||
${
|
||||
|
||||
@@ -68,6 +68,7 @@ export class HaDurationInput extends LitElement {
|
||||
{ label: "-", iconPath: mdiMinusThick, value: "-" },
|
||||
]}
|
||||
.active=${this._negative ? "-" : "+"}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._negativeChanged}
|
||||
></ha-button-toggle-group>
|
||||
`
|
||||
@@ -235,8 +236,8 @@ export class HaDurationInput extends LitElement {
|
||||
ev.stopPropagation();
|
||||
const negative = (ev.detail?.value || ev.target.value) === "-";
|
||||
this._toggleNegative = negative;
|
||||
const value = this.data;
|
||||
if (value) {
|
||||
if (this.data) {
|
||||
const value = { ...this.data };
|
||||
FIELDS.forEach((t) => {
|
||||
if (value[t]) {
|
||||
value[t] = negative ? -Math.abs(value[t]) : Math.abs(value[t]);
|
||||
|
||||
@@ -2,12 +2,7 @@ import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import {
|
||||
FilterPanelController,
|
||||
filterPanelStyles,
|
||||
} from "../common/controllers/filter-panel-controller";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
@@ -39,11 +34,11 @@ export class HaFilterBlueprints extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
@state() private _blueprints?: Blueprints;
|
||||
|
||||
private _content = createRef<HTMLElement>();
|
||||
|
||||
private _panel = new FilterPanelController(this, this._content);
|
||||
@query("ha-list") private _list?: HTMLElement;
|
||||
|
||||
public willUpdate(properties: PropertyValues<this>) {
|
||||
super.willUpdate(properties);
|
||||
@@ -61,6 +56,7 @@ export class HaFilterBlueprints extends LitElement {
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
@@ -75,38 +71,29 @@ export class HaFilterBlueprints extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
${
|
||||
this._panel.showContent
|
||||
? html`
|
||||
<div class="content" ${ref(this._content)}>
|
||||
${
|
||||
this._blueprints
|
||||
? html`
|
||||
<ha-list
|
||||
@selected=${this._blueprintsSelected}
|
||||
multi
|
||||
class="ha-scrollbar"
|
||||
${
|
||||
this._blueprints && this._shouldRender
|
||||
? html`
|
||||
<ha-list
|
||||
@selected=${this._blueprintsSelected}
|
||||
multi
|
||||
class="ha-scrollbar"
|
||||
>
|
||||
${Object.entries(this._blueprints).map(([id, blueprint]) =>
|
||||
"error" in blueprint
|
||||
? nothing
|
||||
: html`<ha-check-list-item
|
||||
.value=${id}
|
||||
.selected=${(this.value || []).includes(id)}
|
||||
>
|
||||
${Object.entries(this._blueprints).map(
|
||||
([id, blueprint]) =>
|
||||
"error" in blueprint
|
||||
? nothing
|
||||
: html`<ha-check-list-item
|
||||
.value=${id}
|
||||
.selected=${(this.value || []).includes(id)}
|
||||
>
|
||||
${blueprint.metadata.name || id}
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</ha-list>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${blueprint.metadata.name || id}
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</ha-list>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -117,6 +104,19 @@ export class HaFilterBlueprints extends LitElement {
|
||||
this._blueprints = await fetchBlueprints(this.hass, this.type);
|
||||
}
|
||||
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (this.narrow || !this.expanded) return;
|
||||
this._list!.style.height = `${this.clientHeight - 49}px`;
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
@@ -191,11 +191,17 @@ export class HaFilterBlueprints extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
filterPanelStyles,
|
||||
css`
|
||||
ha-list {
|
||||
:host {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
:host([expanded]) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 0;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--ha-card-border-radius: var(--ha-border-radius-square);
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
|
||||
@@ -8,14 +8,9 @@ import {
|
||||
mdiTag,
|
||||
} from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import {
|
||||
FilterPanelController,
|
||||
filterPanelStyles,
|
||||
} from "../common/controllers/filter-panel-controller";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import type { CategoryRegistryEntry } from "../data/category_registry";
|
||||
@@ -52,9 +47,9 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _categories: CategoryRegistryEntry[] = [];
|
||||
|
||||
private _content = createRef<HTMLElement>();
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
private _panel = new FilterPanelController(this, this._content);
|
||||
@query("ha-list") private _list?: HTMLElement;
|
||||
|
||||
protected hassSubscribeRequiredHostProps = ["scope"];
|
||||
|
||||
@@ -75,6 +70,7 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
@@ -89,11 +85,9 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
${
|
||||
this._panel.showContent
|
||||
? html`
|
||||
<div class="content" ${ref(this._content)}>
|
||||
${
|
||||
this._shouldRender
|
||||
? html`
|
||||
<ha-list
|
||||
@selected=${this._categorySelected}
|
||||
class="ha-scrollbar"
|
||||
@@ -164,17 +158,34 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
</ha-list-item>`
|
||||
)}
|
||||
</ha-list>
|
||||
<ha-list-item graphic="icon" @click=${this._addCategory}>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.category.editor.add")}
|
||||
</ha-list-item>
|
||||
</div>
|
||||
`
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-expansion-panel>
|
||||
${
|
||||
this.expanded
|
||||
? html`<ha-list-item
|
||||
graphic="icon"
|
||||
@click=${this._addCategory}
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.category.editor.add")}
|
||||
</ha-list-item>`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this._list!.style.height = `${this.clientHeight - (49 + 48)}px`;
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleAction(ev: HaDropdownSelectEvent) {
|
||||
const categoryId = (ev.currentTarget as any).categoryId;
|
||||
const action = ev.detail.item.value;
|
||||
@@ -233,6 +244,10 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
@@ -272,8 +287,19 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
filterPanelStyles,
|
||||
css`
|
||||
:host {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
position: relative;
|
||||
}
|
||||
:host([expanded]) {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--ha-card-border-radius: var(--ha-border-radius-square);
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -299,8 +325,6 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
ha-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
--mdc-list-item-meta-size: auto;
|
||||
--mdc-list-side-padding-right: var(--ha-space-1);
|
||||
--mdc-list-side-padding-left: var(--ha-space-4);
|
||||
@@ -315,6 +339,12 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
.warning {
|
||||
color: var(--error-color);
|
||||
}
|
||||
.add {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -3,12 +3,7 @@ import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
FilterPanelController,
|
||||
filterPanelStyles,
|
||||
} from "../common/controllers/filter-panel-controller";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDeviceNameDisplay } from "../common/entity/compute_device_name";
|
||||
@@ -64,15 +59,13 @@ export class HaFilterDevices extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
@state() private _filter?: string;
|
||||
|
||||
@query("ha-list-selectable-virtualized")
|
||||
private _listElement?: HaListSelectableVirtualized;
|
||||
|
||||
private _content = createRef<HTMLElement>();
|
||||
|
||||
private _panel = new FilterPanelController(this, this._content);
|
||||
|
||||
public willUpdate(properties: PropertyValues<this>) {
|
||||
super.willUpdate(properties);
|
||||
|
||||
@@ -84,11 +77,26 @@ export class HaFilterDevices extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded || !this._listElement) {
|
||||
return;
|
||||
}
|
||||
this._listElement.style.height = `${this.clientHeight - 49 - 4 - 38}px`;
|
||||
// 49px - height of a header + 1px
|
||||
// 4px - padding-top of the search-input
|
||||
// 38px - height of the search input
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
@@ -104,33 +112,31 @@ export class HaFilterDevices extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
${
|
||||
this._shouldRender
|
||||
? html`<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleSearchChange}
|
||||
@keydown=${this._handleSearchKeydown}
|
||||
>
|
||||
</ha-input-search>
|
||||
<ha-list-selectable-virtualized
|
||||
multi
|
||||
.rows=${this._devices(
|
||||
this._devicesReg,
|
||||
this._filter || "",
|
||||
this._localize,
|
||||
this._states,
|
||||
this._i18n.locale.language
|
||||
)}
|
||||
.rowRenderer=${this._renderItem}
|
||||
@ha-list-item-selected=${this._handleAdded}
|
||||
@ha-list-item-deselected=${this._handleRemoved}
|
||||
></ha-list-selectable-virtualized>`
|
||||
: nothing
|
||||
}
|
||||
</ha-expansion-panel>
|
||||
${
|
||||
this._panel.showContent
|
||||
? html`<div class="content" ${ref(this._content)}>
|
||||
<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleSearchChange}
|
||||
@keydown=${this._handleSearchKeydown}
|
||||
>
|
||||
</ha-input-search>
|
||||
<ha-list-selectable-virtualized
|
||||
multi
|
||||
.rows=${this._devices(
|
||||
this._devicesReg,
|
||||
this._filter || "",
|
||||
this._localize,
|
||||
this._states,
|
||||
this._i18n.locale.language
|
||||
)}
|
||||
.rowRenderer=${this._renderItem}
|
||||
@ha-list-item-selected=${this._handleAdded}
|
||||
@ha-list-item-deselected=${this._handleRemoved}
|
||||
></ha-list-selectable-virtualized>
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -171,6 +177,10 @@ export class HaFilterDevices extends LitElement {
|
||||
this.value = (this.value ?? []).filter((deviceId) => deviceId !== id);
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
@@ -261,43 +271,58 @@ export class HaFilterDevices extends LitElement {
|
||||
this._listElement?.clearSelection();
|
||||
}
|
||||
|
||||
static styles = [
|
||||
filterPanelStyles,
|
||||
css`
|
||||
ha-list-selectable-virtualized {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.header ha-icon-button {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: 0;
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
background-color: var(--primary-color);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
ha-input-search {
|
||||
display: block;
|
||||
padding: var(--ha-space-1) var(--ha-space-2) 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
static styles = css`
|
||||
:host {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
:host([expanded]) {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
ha-expansion-panel {
|
||||
--ha-card-border-radius: var(--ha-border-radius-square);
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
:host([expanded]) ha-expansion-panel {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
ha-list-selectable-virtualized {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.header ha-icon-button {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: 0;
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
background-color: var(--primary-color);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
ha-input-search {
|
||||
display: block;
|
||||
padding: var(--ha-space-1) var(--ha-space-2) 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
FilterPanelController,
|
||||
filterPanelStyles,
|
||||
} from "../common/controllers/filter-panel-controller";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
@@ -46,17 +41,18 @@ export class HaFilterDomains extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
@state() private _filter?: string;
|
||||
|
||||
private _content = createRef<HTMLElement>();
|
||||
|
||||
private _panel = new FilterPanelController(this, this._content);
|
||||
@query("ha-list") private _list?: HTMLElement;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
@@ -71,48 +67,46 @@ export class HaFilterDomains extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
${
|
||||
this._shouldRender
|
||||
? html`<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</ha-input-search>
|
||||
<ha-list
|
||||
class="ha-scrollbar"
|
||||
@selected=${this._handleItemSelected}
|
||||
multi
|
||||
>
|
||||
${repeat(
|
||||
this._domains(
|
||||
this._states,
|
||||
this._localize,
|
||||
this._i18n.locale.language,
|
||||
this._filter,
|
||||
this.value
|
||||
),
|
||||
(i) => i,
|
||||
(domain) =>
|
||||
html`<ha-check-list-item
|
||||
.value=${domain}
|
||||
.selected=${(this.value || []).includes(domain)}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-domain-icon
|
||||
slot="graphic"
|
||||
.domain=${domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
${domainToName(this._localize, domain)}
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</ha-list> `
|
||||
: nothing
|
||||
}
|
||||
</ha-expansion-panel>
|
||||
${
|
||||
this._panel.showContent
|
||||
? html`<div class="content" ${ref(this._content)}>
|
||||
<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</ha-input-search>
|
||||
<ha-list
|
||||
class="ha-scrollbar"
|
||||
@selected=${this._handleItemSelected}
|
||||
multi
|
||||
>
|
||||
${repeat(
|
||||
this._domains(
|
||||
this._states,
|
||||
this._localize,
|
||||
this._i18n.locale.language,
|
||||
this._filter,
|
||||
this.value
|
||||
),
|
||||
(i) => i,
|
||||
(domain) =>
|
||||
html`<ha-check-list-item
|
||||
.value=${domain}
|
||||
.selected=${(this.value || []).includes(domain)}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-domain-icon
|
||||
slot="graphic"
|
||||
.domain=${domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
${domainToName(this._localize, domain)}
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</ha-list>
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -145,6 +139,22 @@ export class HaFilterDomains extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this._list!.style.height = `${this.clientHeight - 49 - 4 - 32}px`;
|
||||
// 49px - height of a header + 1px
|
||||
// 4px - padding-top of the search-input
|
||||
// 32px - height of the search input
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
@@ -189,11 +199,17 @@ export class HaFilterDomains extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
filterPanelStyles,
|
||||
css`
|
||||
ha-list {
|
||||
:host {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
:host([expanded]) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 0;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--ha-card-border-radius: var(--ha-border-radius-square);
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
|
||||
@@ -2,13 +2,8 @@ import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
FilterPanelController,
|
||||
filterPanelStyles,
|
||||
} from "../common/controllers/filter-panel-controller";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
@@ -57,11 +52,11 @@ export class HaFilterEntities extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
@state() private _filter?: string;
|
||||
|
||||
private _content = createRef<HTMLElement>();
|
||||
|
||||
private _panel = new FilterPanelController(this, this._content);
|
||||
@query("ha-list") private _list?: HTMLElement;
|
||||
|
||||
public willUpdate(properties: PropertyValues<this>) {
|
||||
super.willUpdate(properties);
|
||||
@@ -83,6 +78,7 @@ export class HaFilterEntities extends LitElement {
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
@@ -97,11 +93,9 @@ export class HaFilterEntities extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
${
|
||||
this._panel.showContent
|
||||
? html`
|
||||
<div class="content" ${ref(this._content)}>
|
||||
${
|
||||
this._shouldRender
|
||||
? html`
|
||||
<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@@ -124,13 +118,25 @@ export class HaFilterEntities extends LitElement {
|
||||
>
|
||||
</lit-virtualizer>
|
||||
</ha-list>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this._list!.style.height = `${this.clientHeight - 49 - 4 - 32}px`;
|
||||
// 49px - height of a header + 1px
|
||||
// 4px - padding-top of the search-input
|
||||
// 32px - height of the search input
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private _keyFunction = (entity) => entity?.entity_id;
|
||||
|
||||
private _renderItem = (entity) =>
|
||||
@@ -167,6 +173,10 @@ export class HaFilterEntities extends LitElement {
|
||||
listItem.selected = this.value?.includes(value);
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
@@ -245,11 +255,17 @@ export class HaFilterEntities extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
filterPanelStyles,
|
||||
css`
|
||||
ha-list {
|
||||
:host {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
:host([expanded]) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 0;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--ha-card-border-radius: var(--ha-border-radius-square);
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
|
||||
@@ -4,13 +4,8 @@ import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
FilterPanelController,
|
||||
filterPanelStyles,
|
||||
} from "../common/controllers/filter-panel-controller";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
@@ -69,12 +64,10 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
@query("ha-list-selectable") private _list?: HaListSelectable;
|
||||
|
||||
private _content = createRef<HTMLElement>();
|
||||
|
||||
private _panel = new FilterPanelController(this, this._content);
|
||||
|
||||
public willUpdate(properties: PropertyValues<this>) {
|
||||
super.willUpdate(properties);
|
||||
|
||||
@@ -93,6 +86,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
@@ -113,11 +107,9 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
${
|
||||
this._panel.showContent
|
||||
? html`
|
||||
<div class="content" ${ref(this._content)}>
|
||||
${
|
||||
this._shouldRender
|
||||
? html`
|
||||
<ha-list-selectable
|
||||
class="ha-scrollbar"
|
||||
multi
|
||||
@@ -162,10 +154,10 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
(area) => this._renderArea(area)
|
||||
)}
|
||||
</ha-list-selectable>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -251,6 +243,19 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this._list!.style.height = `${this.clientHeight - 49}px`;
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
@@ -342,11 +347,17 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
filterPanelStyles,
|
||||
css`
|
||||
ha-list-selectable {
|
||||
:host {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
:host([expanded]) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 0;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--ha-card-border-radius: var(--ha-border-radius-square);
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
FilterPanelController,
|
||||
filterPanelStyles,
|
||||
} from "../common/controllers/filter-panel-controller";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
@@ -49,11 +44,11 @@ export class HaFilterIntegrations extends LitElement {
|
||||
Object.values(manifests)
|
||||
);
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
@state() private _filter?: string;
|
||||
|
||||
private _content = createRef<HTMLElement>();
|
||||
|
||||
private _panel = new FilterPanelController(this, this._content);
|
||||
@query("ha-list") private _list?: HTMLElement;
|
||||
|
||||
protected render() {
|
||||
const manifests = this._manifests
|
||||
@@ -64,6 +59,7 @@ export class HaFilterIntegrations extends LitElement {
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
@@ -78,57 +74,67 @@ export class HaFilterIntegrations extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
${
|
||||
this._panel.showContent
|
||||
? html`<div class="content" ${ref(this._content)}>
|
||||
${
|
||||
manifests
|
||||
? html`<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</ha-input-search>
|
||||
<ha-list
|
||||
class="ha-scrollbar"
|
||||
@selected=${this._itemSelected}
|
||||
multi
|
||||
>
|
||||
${repeat(
|
||||
this._integrations(
|
||||
this._localize,
|
||||
manifests,
|
||||
this._filter,
|
||||
this.value,
|
||||
this._i18n.locale.language
|
||||
),
|
||||
(i) => i.domain,
|
||||
(integration) =>
|
||||
html`<ha-check-list-item
|
||||
.value=${integration.domain}
|
||||
.selected=${(this.value || []).includes(
|
||||
integration.domain
|
||||
)}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-domain-icon
|
||||
slot="graphic"
|
||||
.domain=${integration.domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
${integration.name}
|
||||
</ha-check-list-item>`
|
||||
${
|
||||
manifests && this._shouldRender
|
||||
? html`<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</ha-input-search>
|
||||
<ha-list
|
||||
class="ha-scrollbar"
|
||||
@selected=${this._itemSelected}
|
||||
multi
|
||||
>
|
||||
${repeat(
|
||||
this._integrations(
|
||||
this._localize,
|
||||
manifests,
|
||||
this._filter,
|
||||
this.value,
|
||||
this._i18n.locale.language
|
||||
),
|
||||
(i) => i.domain,
|
||||
(integration) =>
|
||||
html`<ha-check-list-item
|
||||
.value=${integration.domain}
|
||||
.selected=${(this.value || []).includes(
|
||||
integration.domain
|
||||
)}
|
||||
</ha-list>`
|
||||
: nothing
|
||||
}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-domain-icon
|
||||
slot="graphic"
|
||||
.domain=${integration.domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
${integration.name}
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</ha-list> `
|
||||
: nothing
|
||||
}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this._list!.style.height = `${this.clientHeight - 49 - 4 - 32}px`;
|
||||
// 49px - height of a header + 1px
|
||||
// 4px - padding-top of the search-input
|
||||
// 32px - height of the search input
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
@@ -207,11 +213,17 @@ export class HaFilterIntegrations extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
filterPanelStyles,
|
||||
css`
|
||||
ha-list {
|
||||
:host {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
:host([expanded]) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 0;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--ha-card-border-radius: var(--ha-border-radius-square);
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiCog, mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
FilterPanelController,
|
||||
filterPanelStyles,
|
||||
} from "../common/controllers/filter-panel-controller";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { navigate } from "../common/navigate";
|
||||
@@ -49,11 +44,11 @@ export class HaFilterLabels extends LitElement {
|
||||
@state()
|
||||
private _labels?: LabelRegistryEntry[];
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
@state() private _filter?: string;
|
||||
|
||||
private _content = createRef<HTMLElement>();
|
||||
|
||||
private _panel = new FilterPanelController(this, this._content);
|
||||
@query("ha-list") private _list?: HTMLElement;
|
||||
|
||||
private _filteredLabels = memoizeOne(
|
||||
// `_value` used to recalculate the memoization when the selection changes
|
||||
@@ -80,6 +75,7 @@ export class HaFilterLabels extends LitElement {
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
@@ -94,66 +90,89 @@ export class HaFilterLabels extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
${
|
||||
this._shouldRender
|
||||
? html`<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</ha-input-search>
|
||||
<ha-list
|
||||
@selected=${this._labelSelected}
|
||||
class="ha-scrollbar"
|
||||
multi
|
||||
>
|
||||
${repeat(
|
||||
this._filteredLabels(
|
||||
this._labels || [],
|
||||
this._filter,
|
||||
this._i18n.locale.language,
|
||||
this.value
|
||||
),
|
||||
(label) => label.label_id,
|
||||
(label) =>
|
||||
html`<ha-check-list-item
|
||||
.value=${label.label_id}
|
||||
.selected=${(this.value || []).includes(label.label_id)}
|
||||
hasMeta
|
||||
>
|
||||
<ha-label
|
||||
.color=${label.color}
|
||||
.description=${label.description}
|
||||
>
|
||||
${
|
||||
label.icon
|
||||
? html`<ha-icon
|
||||
slot="icon"
|
||||
.icon=${label.icon}
|
||||
></ha-icon>`
|
||||
: nothing
|
||||
}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</ha-list> `
|
||||
: nothing
|
||||
}
|
||||
</ha-expansion-panel>
|
||||
${
|
||||
this._panel.showContent
|
||||
? html`<div class="content" ${ref(this._content)}>
|
||||
<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</ha-input-search>
|
||||
<ha-list
|
||||
@selected=${this._labelSelected}
|
||||
class="ha-scrollbar"
|
||||
multi
|
||||
>
|
||||
${repeat(
|
||||
this._filteredLabels(
|
||||
this._labels || [],
|
||||
this._filter,
|
||||
this._i18n.locale.language,
|
||||
this.value
|
||||
),
|
||||
(label) => label.label_id,
|
||||
(label) =>
|
||||
html`<ha-check-list-item
|
||||
.value=${label.label_id}
|
||||
.selected=${(this.value || []).includes(label.label_id)}
|
||||
hasMeta
|
||||
>
|
||||
<ha-label
|
||||
.color=${label.color}
|
||||
.description=${label.description}
|
||||
>
|
||||
${
|
||||
label.icon
|
||||
? html`<ha-icon
|
||||
slot="icon"
|
||||
.icon=${label.icon}
|
||||
></ha-icon>`
|
||||
: nothing
|
||||
}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</ha-list>
|
||||
<ha-list-item graphic="icon" @click=${this._manageLabels}>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiCog}></ha-svg-icon>
|
||||
${this._localize("ui.panel.config.labels.manage_labels")}
|
||||
</ha-list-item>
|
||||
</div>`
|
||||
this.expanded
|
||||
? html`<ha-list-item
|
||||
graphic="icon"
|
||||
@click=${this._manageLabels}
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiCog}></ha-svg-icon>
|
||||
${this._localize("ui.panel.config.labels.manage_labels")}
|
||||
</ha-list-item>`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this._list!.style.height = `${this.clientHeight - (49 + 48 + 32 + 4)}px`;
|
||||
// 49px - height of a header + 1px
|
||||
// 4px - padding-top of the search-input
|
||||
// 32px - height of the search input
|
||||
// 48px - height of ha-list-item
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private _manageLabels() {
|
||||
navigate("/config/labels");
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
@@ -208,11 +227,18 @@ export class HaFilterLabels extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
filterPanelStyles,
|
||||
css`
|
||||
ha-list {
|
||||
:host {
|
||||
position: relative;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
:host([expanded]) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 0;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--ha-card-border-radius: var(--ha-border-radius-square);
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
@@ -241,6 +267,12 @@ export class HaFilterLabels extends LitElement {
|
||||
.warning {
|
||||
color: var(--error-color);
|
||||
}
|
||||
.add {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
ha-input-search {
|
||||
display: block;
|
||||
padding: var(--ha-space-1) var(--ha-space-2) 0;
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import type { List, SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import {
|
||||
FilterPanelController,
|
||||
filterPanelStyles,
|
||||
} from "../common/controllers/filter-panel-controller";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import "./ha-check-list-item";
|
||||
@@ -32,9 +27,9 @@ export class HaFilterStates extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
private _content = createRef<HTMLElement>();
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
private _panel = new FilterPanelController(this, this._content);
|
||||
@query("ha-list") private _list!: List;
|
||||
|
||||
protected render() {
|
||||
if (!this.states) {
|
||||
@@ -45,6 +40,7 @@ export class HaFilterStates extends LitElement {
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
@@ -59,11 +55,9 @@ export class HaFilterStates extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
${
|
||||
this._panel.showContent
|
||||
? html`
|
||||
<div class="content" ${ref(this._content)}>
|
||||
${
|
||||
this._shouldRender
|
||||
? html`
|
||||
<ha-list
|
||||
@selected=${this._statesSelected}
|
||||
multi
|
||||
@@ -88,13 +82,36 @@ export class HaFilterStates extends LitElement {
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</ha-list>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
protected willUpdate(changed: PropertyValues<this>) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
this._shouldRender = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
if ((changed.has("expanded") || changed.has("states")) && this.expanded) {
|
||||
setTimeout(async () => {
|
||||
if (!this.expanded) return;
|
||||
const list = this._list;
|
||||
if (!list) {
|
||||
return;
|
||||
}
|
||||
list.style.height = `${this.clientHeight - 49}px`;
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
@@ -135,11 +152,17 @@ export class HaFilterStates extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
filterPanelStyles,
|
||||
css`
|
||||
ha-list {
|
||||
:host {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
:host([expanded]) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 0;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--ha-card-border-radius: var(--ha-border-radius-square);
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
|
||||
@@ -2,13 +2,8 @@ import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import {
|
||||
FilterPanelController,
|
||||
filterPanelStyles,
|
||||
} from "../common/controllers/filter-panel-controller";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
@@ -39,15 +34,16 @@ export class HaFilterVoiceAssistants extends LitElement {
|
||||
|
||||
@state() private _voiceAssistantOptions: string[] = [];
|
||||
|
||||
private _content = createRef<HTMLElement>();
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
private _panel = new FilterPanelController(this, this._content);
|
||||
@query("ha-list") private _list?: HTMLElement;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
@@ -62,11 +58,9 @@ export class HaFilterVoiceAssistants extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
${
|
||||
this._panel.showContent
|
||||
? html`<div class="content" ${ref(this._content)}>
|
||||
<ha-list
|
||||
${
|
||||
this._shouldRender
|
||||
? html`<ha-list
|
||||
@selected=${this._assistantsSelected}
|
||||
class="ha-scrollbar"
|
||||
multi
|
||||
@@ -89,10 +83,10 @@ export class HaFilterVoiceAssistants extends LitElement {
|
||||
${voiceAssistants[voiceAssistantId].name}
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</ha-list>
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
</ha-list> `
|
||||
: nothing
|
||||
}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -101,6 +95,19 @@ export class HaFilterVoiceAssistants extends LitElement {
|
||||
this._voiceAssistantOptions = Object.keys(voiceAssistants);
|
||||
}
|
||||
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this._list!.style.height = `${this.clientHeight - 49}px`;
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
@@ -141,11 +148,18 @@ export class HaFilterVoiceAssistants extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
filterPanelStyles,
|
||||
css`
|
||||
ha-list {
|
||||
:host {
|
||||
position: relative;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
:host([expanded]) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 0;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--ha-card-border-radius: var(--ha-border-radius-square);
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
|
||||
@@ -81,6 +81,7 @@ export class HaChooseSelector extends LitElement {
|
||||
.required=${this.required}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
.helper=${this.helper}
|
||||
.localizeValue=${this.localizeValue}
|
||||
></ha-selector>`;
|
||||
}
|
||||
|
||||
@@ -107,7 +108,12 @@ export class HaChooseSelector extends LitElement {
|
||||
: {
|
||||
[this._activeChoice!]: this.value,
|
||||
};
|
||||
this._activeChoice = ev.detail?.value || ev.target.value;
|
||||
const choice = ev.detail?.value || ev.target.value;
|
||||
this._activeChoice = choice;
|
||||
if (choice && "constant" in this.selector.choose.choices[choice].selector) {
|
||||
value[choice] =
|
||||
this.selector.choose.choices[choice].selector.constant?.value;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...value,
|
||||
|
||||
@@ -664,6 +664,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
overscroll-behavior: contain;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
|
||||
+10
-3
@@ -126,11 +126,18 @@ export const listDatadisks = async (
|
||||
timeout: null,
|
||||
});
|
||||
|
||||
export const fetchHostDisksUsage = async (hass: HomeAssistant) =>
|
||||
// `disk` is "default" for the data disk, or a mount name. Omitting maxDepth
|
||||
// leaves the depth to the Supervisor, which defaults per target — walking a
|
||||
// mount costs a round trip per directory, so mounts want no depth at all.
|
||||
export const fetchHostDisksUsage = async (
|
||||
hass: HomeAssistant,
|
||||
disk = "default",
|
||||
maxDepth?: number
|
||||
) =>
|
||||
hass.callWS<HostDisksUsage>({
|
||||
type: "supervisor/api",
|
||||
endpoint: "/host/disks/default/usage",
|
||||
endpoint: `/host/disks/${disk}/usage`,
|
||||
method: "get",
|
||||
timeout: 3600, // seconds. This can take a while
|
||||
params: { max_depth: 3 },
|
||||
...(maxDepth === undefined ? {} : { params: { max_depth: maxDepth } }),
|
||||
});
|
||||
|
||||
@@ -22,7 +22,6 @@ export enum VacuumEntityFeature {
|
||||
STOP = 8,
|
||||
RETURN_HOME = 16,
|
||||
FAN_SPEED = 32,
|
||||
BATTERY = 64,
|
||||
STATUS = 128,
|
||||
SEND_COMMAND = 256,
|
||||
LOCATE = 512,
|
||||
@@ -34,7 +33,6 @@ export enum VacuumEntityFeature {
|
||||
}
|
||||
|
||||
interface VacuumEntityAttributes extends HassEntityAttributeBase {
|
||||
battery_level?: number;
|
||||
fan_speed?: string;
|
||||
fan_speed_list?: string[];
|
||||
[key: string]: any;
|
||||
|
||||
+3
-2
@@ -566,9 +566,10 @@ export const getWeatherStateIcon = (
|
||||
if (userDefinedIcon) {
|
||||
return html`
|
||||
<div
|
||||
style="background-size: cover;${styleMap({
|
||||
style=${styleMap({
|
||||
"background-size": "cover",
|
||||
"background-image": userDefinedIcon,
|
||||
})}"
|
||||
})}
|
||||
></div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -159,23 +159,6 @@ class MoreInfoVacuum extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
// Use deprecated battery_level and battery_icon attributes
|
||||
if (
|
||||
supportsFeature(this.stateObj, VacuumEntityFeature.BATTERY) &&
|
||||
this.stateObj.attributes.battery_level
|
||||
) {
|
||||
return html`
|
||||
<span class="battery" slot="after-time">
|
||||
<span
|
||||
>${Math.round(
|
||||
this.stateObj.attributes.battery_level
|
||||
)}${blankBeforePercent(this._i18n.locale)}%</span
|
||||
>
|
||||
<ha-icon .icon=${this.stateObj.attributes.battery_icon}></ha-icon>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
return nothing;
|
||||
}
|
||||
|
||||
|
||||
@@ -74,11 +74,6 @@ export class MockVacuumEntity extends MockBaseEntity {
|
||||
stateAttrs.fan_speed = attrs.fan_speed ?? null;
|
||||
}
|
||||
|
||||
if (supportsFeatureFromAttributes(attrs, VacuumEntityFeature.BATTERY)) {
|
||||
stateAttrs.battery_level = attrs.battery_level ?? null;
|
||||
stateAttrs.battery_icon = attrs.battery_icon ?? null;
|
||||
}
|
||||
|
||||
if (supportsFeatureFromAttributes(attrs, VacuumEntityFeature.STATUS)) {
|
||||
stateAttrs.status = attrs.status ?? null;
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ import type {
|
||||
SortingDirection,
|
||||
} from "../components/data-table/ha-data-table";
|
||||
import { showDataTableSettingsDialog } from "../components/data-table/show-dialog-data-table-settings";
|
||||
import "../components/ha-adaptive-dialog";
|
||||
import "../components/ha-button";
|
||||
import "../components/ha-dialog";
|
||||
import "../components/ha-dialog-footer";
|
||||
import "../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../components/ha-dropdown";
|
||||
@@ -576,17 +576,16 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
</hass-tabs-subpage>
|
||||
${
|
||||
this.showFilters && !showPane
|
||||
? html`<ha-adaptive-dialog
|
||||
open
|
||||
flexcontent
|
||||
? html`<ha-dialog
|
||||
.open=${true}
|
||||
width="full"
|
||||
header-title=${localize("ui.components.subpage-data-table.filters")}
|
||||
@closed=${this._closeFilters}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="headerNavigationIcon"
|
||||
data-dialog="close"
|
||||
.path=${mdiClose}
|
||||
@click=${this._closeFilters}
|
||||
.label=${localize(
|
||||
"ui.components.subpage-data-table.close_filter"
|
||||
)}
|
||||
@@ -607,13 +606,13 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
<slot name="filter-pane"></slot>
|
||||
</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button slot="primaryAction" data-dialog="close">
|
||||
<ha-button slot="primaryAction" @click=${this._closeFilters}>
|
||||
${localize("ui.components.subpage-data-table.show_results", {
|
||||
number: this.data.length,
|
||||
})}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-adaptive-dialog>`
|
||||
</ha-dialog>`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
@@ -966,19 +965,20 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
--md-assist-chip-trailing-space: 8px;
|
||||
}
|
||||
|
||||
ha-adaptive-dialog {
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
/* Fixed height so the sheet does not resize while filtering. */
|
||||
--ha-bottom-sheet-height: calc(100dvh - var(--ha-space-12));
|
||||
--ha-dialog-min-height: calc(var(--safe-height) - var(--ha-space-20));
|
||||
}
|
||||
|
||||
.filter-dialog-content {
|
||||
height: calc(
|
||||
100vh -
|
||||
70px - var(--header-height, 0px) - var(
|
||||
--safe-area-inset-top,
|
||||
0px
|
||||
) - var(--safe-area-inset-bottom, 0px)
|
||||
);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
ha-dropdown ha-assist-chip {
|
||||
|
||||
@@ -124,7 +124,7 @@ class HaBackupConfigData extends LitElement {
|
||||
|
||||
private async _fetchStorageInfo() {
|
||||
try {
|
||||
this._storageInfo = await fetchHostDisksUsage(this.hass);
|
||||
this._storageInfo = await fetchHostDisksUsage(this.hass, "default", 3);
|
||||
} catch (_err: any) {
|
||||
this._storageInfo = null;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mdiPencil } from "@mdi/js";
|
||||
import { mdiAccount, mdiAccountPlus, mdiPencil } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -19,6 +19,7 @@ import type { PersonMutableParams } from "../../../data/person";
|
||||
import type { User } from "../../../data/user";
|
||||
import {
|
||||
deleteUser,
|
||||
fetchUsers,
|
||||
SYSTEM_GROUP_ID_ADMIN,
|
||||
SYSTEM_GROUP_ID_USER,
|
||||
updateUser,
|
||||
@@ -36,6 +37,8 @@ import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showAddUserDialog } from "../users/show-dialog-add-user";
|
||||
import { showAdminChangePasswordDialog } from "../users/show-dialog-admin-change-password";
|
||||
import type { PersonDetailDialogParams } from "./show-dialog-person-detail";
|
||||
import { showListItemsDialog } from "../../../dialogs/dialog-list-items/show-list-items-dialog";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
|
||||
const includeDomains = ["device_tracker"];
|
||||
|
||||
@@ -85,6 +88,8 @@ class DialogPersonDetail
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
private _linkedExistingUser = false;
|
||||
|
||||
private _deviceTrackersAvailable = memoizeOne((hass) =>
|
||||
Object.keys(hass.states).some(
|
||||
(entityId) =>
|
||||
@@ -140,7 +145,7 @@ class DialogPersonDetail
|
||||
private _dialogClosed() {
|
||||
// If we do not have a person ID yet (= person creation dialog was just cancelled), but
|
||||
// we already created a user ID for it, delete it now to not have it "free floating".
|
||||
if (!this._personExists && this._userId) {
|
||||
if (!this._personExists && this._userId && !this._linkedExistingUser) {
|
||||
const callback = this._params?.refreshUsers;
|
||||
deleteUser(this.hass, this._userId).then(() => {
|
||||
callback?.();
|
||||
@@ -426,26 +431,71 @@ class DialogPersonDetail
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private async _linkUser(user: User, newUser: boolean) {
|
||||
this._linkedExistingUser = !newUser;
|
||||
if (this._params!.entry && this._params!.updateEntry) {
|
||||
await this._params!.updateEntry({ user_id: user.id });
|
||||
}
|
||||
if (newUser) {
|
||||
this._params?.refreshUsers?.();
|
||||
}
|
||||
this._user = user;
|
||||
this._userId = user.id;
|
||||
this._isAdmin = user.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
|
||||
this._localOnly = user.local_only;
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private async _allowLoginChanged(ev): Promise<void> {
|
||||
const target = ev.target;
|
||||
if (target.checked) {
|
||||
target.checked = false;
|
||||
showAddUserDialog(this, {
|
||||
userAddedCallback: async (user?: User) => {
|
||||
if (user) {
|
||||
target.checked = true;
|
||||
if (this._params!.entry && this._params!.updateEntry) {
|
||||
await this._params!.updateEntry({ user_id: user.id });
|
||||
|
||||
const users = await fetchUsers(this.hass);
|
||||
const currentLinkedUsers = new Set(
|
||||
Object.values(this.hass.states)
|
||||
.filter(
|
||||
(s) =>
|
||||
computeDomain(s.entity_id) === "person" && s.attributes.user_id
|
||||
)
|
||||
.map((s) => s.attributes.user_id)
|
||||
);
|
||||
const eligibleUsers = users.filter(
|
||||
(u) =>
|
||||
!currentLinkedUsers.has(u.id) && !u.system_generated && u.username
|
||||
);
|
||||
const addUserDialog = () =>
|
||||
showAddUserDialog(this, {
|
||||
userAddedCallback: async (user?: User) => {
|
||||
if (user) {
|
||||
target.checked = true;
|
||||
this._linkUser(user, true);
|
||||
}
|
||||
this._params?.refreshUsers?.();
|
||||
this._user = user;
|
||||
this._userId = user.id;
|
||||
this._isAdmin = user.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
|
||||
this._localOnly = user.local_only;
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
},
|
||||
name: this._name,
|
||||
},
|
||||
name: this._name,
|
||||
});
|
||||
|
||||
if (eligibleUsers.length === 0) {
|
||||
addUserDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
showListItemsDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.person.detail.select_user"),
|
||||
items: [
|
||||
{
|
||||
iconPath: mdiAccountPlus,
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.person.detail.create_new_user"
|
||||
),
|
||||
action: addUserDialog,
|
||||
},
|
||||
...eligibleUsers.map((user) => ({
|
||||
iconPath: mdiAccount,
|
||||
label: `${user.name} (${user.username})`,
|
||||
action: () => this._linkUser(user, false),
|
||||
})),
|
||||
],
|
||||
});
|
||||
} else if (this._userId) {
|
||||
if (
|
||||
|
||||
@@ -9,10 +9,12 @@ import {
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { blankBeforePercent } from "../../../common/translations/blank_before_percent";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-bar";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-next";
|
||||
@@ -20,6 +22,7 @@ import "../../../components/ha-list";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-segmented-bar";
|
||||
import type { Segment } from "../../../components/ha-segmented-bar";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import type { HassioHostInfo, HostDisksUsage } from "../../../data/hassio/host";
|
||||
@@ -41,6 +44,7 @@ import {
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { bytesToString } from "../../../util/bytes-to-string";
|
||||
import "../core/ha-config-analytics";
|
||||
import { showMoveDatadiskDialog } from "./show-dialog-move-datadisk";
|
||||
import { showMountViewDialog } from "./show-dialog-view-mount";
|
||||
@@ -62,6 +66,13 @@ class HaConfigSectionStorage extends LitElement {
|
||||
|
||||
@state() private _mountsInfo?: SupervisorMounts | null;
|
||||
|
||||
// Keyed by mount name. A missing key means the request is still in flight;
|
||||
// null means it failed, and that row simply shows no usage.
|
||||
@state() private _mountUsage: Record<string, HostDisksUsage | null> = {};
|
||||
|
||||
// Guards against a slow response from a previous reload landing in a newer one.
|
||||
private _mountUsageGeneration = 0;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
if (isComponentLoaded(this.hass.config, "hassio")) {
|
||||
@@ -173,6 +184,7 @@ class HaConfigSectionStorage extends LitElement {
|
||||
graphic="avatar"
|
||||
.mount=${mount}
|
||||
twoline
|
||||
multiline-secondary
|
||||
hasMeta
|
||||
@click=${this._changeMount}
|
||||
>
|
||||
@@ -193,13 +205,16 @@ class HaConfigSectionStorage extends LitElement {
|
||||
${mount.name}
|
||||
</span>
|
||||
<span slot="secondary">
|
||||
${mount.server}${
|
||||
mount.port ? `:${mount.port}` : nothing
|
||||
}${
|
||||
mount.type === SupervisorMountType.NFS
|
||||
? mount.path
|
||||
: `:${mount.share}`
|
||||
}
|
||||
<span class="mount-address">
|
||||
${mount.server}${
|
||||
mount.port ? `:${mount.port}` : ""
|
||||
}${
|
||||
mount.type === SupervisorMountType.NFS
|
||||
? mount.path
|
||||
: `:${mount.share}`
|
||||
}
|
||||
</span>
|
||||
${this._renderMountUsage(mount)}
|
||||
</span>
|
||||
${
|
||||
mount.state !== SupervisorMountState.ACTIVE
|
||||
@@ -289,6 +304,39 @@ class HaConfigSectionStorage extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderMountUsage(mount: SupervisorMount) {
|
||||
if (mount.state !== SupervisorMountState.ACTIVE) {
|
||||
return nothing;
|
||||
}
|
||||
if (!(mount.name in this._mountUsage)) {
|
||||
return html`<div class="mount-usage">
|
||||
<ha-spinner size="tiny"></ha-spinner>
|
||||
</div>`;
|
||||
}
|
||||
const usage = this._mountUsage[mount.name];
|
||||
// Without a total there is no ratio to show, so show nothing rather than a
|
||||
// bar that means something else.
|
||||
if (!usage?.total_bytes) {
|
||||
return nothing;
|
||||
}
|
||||
const percent = (usage.used_bytes / usage.total_bytes) * 100;
|
||||
return html`<div class="mount-usage">
|
||||
<ha-bar
|
||||
class=${classMap({
|
||||
"target-warning": percent > 85,
|
||||
"target-critical": percent > 95,
|
||||
})}
|
||||
.value=${percent}
|
||||
></ha-bar>
|
||||
<span>
|
||||
${this.hass.localize("ui.panel.config.storage.detailed_description", {
|
||||
used: bytesToString(usage.used_bytes),
|
||||
total: bytesToString(usage.total_bytes),
|
||||
})}
|
||||
</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private async _load() {
|
||||
this._loadStorageInfo();
|
||||
try {
|
||||
@@ -305,7 +353,7 @@ class HaConfigSectionStorage extends LitElement {
|
||||
|
||||
private async _loadStorageInfo() {
|
||||
try {
|
||||
this._storageInfo = await fetchHostDisksUsage(this.hass);
|
||||
this._storageInfo = await fetchHostDisksUsage(this.hass, "default", 3);
|
||||
} catch (err: any) {
|
||||
this._error = err.message || err;
|
||||
this._storageInfo = null;
|
||||
@@ -361,6 +409,34 @@ class HaConfigSectionStorage extends LitElement {
|
||||
this._error = err.message || err;
|
||||
this._mountsInfo = null;
|
||||
}
|
||||
this._loadMountUsage();
|
||||
}
|
||||
|
||||
// Deliberately not awaited: a mount on a slow or unreachable server can take
|
||||
// ~30 s to answer, and the rows must paint before then. Only active mounts are
|
||||
// asked, since the endpoint has nothing to report for the others.
|
||||
private _loadMountUsage(): void {
|
||||
const generation = ++this._mountUsageGeneration;
|
||||
this._mountUsage = {};
|
||||
this._mountsInfo?.mounts
|
||||
.filter((mount) => mount.state === SupervisorMountState.ACTIVE)
|
||||
.forEach((mount) => {
|
||||
fetchHostDisksUsage(this.hass, mount.name).then(
|
||||
(usage) => this._setMountUsage(generation, mount.name, usage),
|
||||
() => this._setMountUsage(generation, mount.name, null)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private _setMountUsage(
|
||||
generation: number,
|
||||
name: string,
|
||||
usage: HostDisksUsage | null
|
||||
): void {
|
||||
if (generation !== this._mountUsageGeneration) {
|
||||
return;
|
||||
}
|
||||
this._mountUsage = { ...this._mountUsage, [name]: usage };
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
@@ -409,6 +485,41 @@ class HaConfigSectionStorage extends LitElement {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
/* multiline-secondary lets the secondary slot wrap, so the address keeps its
|
||||
own single ellipsized line and only the usage sits below it. */
|
||||
.mount-address {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mount-usage {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
margin-top: var(--ha-space-1);
|
||||
}
|
||||
|
||||
.mount-usage ha-bar {
|
||||
flex: 0 0 72px;
|
||||
--ha-bar-primary-color: var(--metric-bar-ok-color, var(--success-color));
|
||||
}
|
||||
|
||||
.mount-usage ha-bar.target-warning {
|
||||
--ha-bar-primary-color: var(
|
||||
--metric-bar-warning-color,
|
||||
var(--warning-color)
|
||||
);
|
||||
}
|
||||
|
||||
.mount-usage ha-bar.target-critical {
|
||||
--ha-bar-primary-color: var(
|
||||
--metric-bar-critical-color,
|
||||
var(--error-color)
|
||||
);
|
||||
}
|
||||
|
||||
.mounts-not-supported {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
@@ -20,9 +20,10 @@ type ReloadableDomain = Exclude<
|
||||
"heading" | "introduction" | "reload"
|
||||
>;
|
||||
|
||||
interface TranslatedReloadableDomain {
|
||||
domain: ReloadableDomain;
|
||||
interface TranslatedReloadableItem {
|
||||
domain: ReloadableDomain | "frontend";
|
||||
name: string;
|
||||
service?: string;
|
||||
}
|
||||
|
||||
@customElement("tools-yaml-config")
|
||||
@@ -37,7 +38,7 @@ export class ToolsYamlConfig extends LitElement {
|
||||
|
||||
@state() private _validating = false;
|
||||
|
||||
@state() private _reloadableDomains: TranslatedReloadableDomain[] = [];
|
||||
@state() private _reloadableItems: TranslatedReloadableItem[] = [];
|
||||
|
||||
@state() private _validateResult?: CheckConfigResult;
|
||||
|
||||
@@ -54,10 +55,10 @@ export class ToolsYamlConfig extends LitElement {
|
||||
oldHass.config.components !== this.hass.config.components ||
|
||||
oldHass.localize !== this.hass.localize)
|
||||
) {
|
||||
this._reloadableDomains = (
|
||||
this._reloadableItems = (
|
||||
componentsWithService(this.hass, "reload") as ReloadableDomain[]
|
||||
)
|
||||
.map((domain) => ({
|
||||
.map<TranslatedReloadableItem>((domain) => ({
|
||||
domain,
|
||||
name:
|
||||
this.hass.localize(
|
||||
@@ -68,6 +69,15 @@ export class ToolsYamlConfig extends LitElement {
|
||||
{ domain: domainToName(this.hass.localize, domain) }
|
||||
),
|
||||
}))
|
||||
.concat([
|
||||
{
|
||||
domain: "frontend",
|
||||
service: "reload_themes",
|
||||
name: this.hass.localize(
|
||||
`ui.panel.config.tools.tabs.yaml.section.reloading.themes`
|
||||
),
|
||||
},
|
||||
])
|
||||
.sort((a, b) =>
|
||||
stringCompare(a.name, b.name, this.hass.locale.language)
|
||||
);
|
||||
@@ -190,12 +200,12 @@ export class ToolsYamlConfig extends LitElement {
|
||||
)}
|
||||
</ha-call-service-button>
|
||||
</div>
|
||||
${this._reloadableDomains.map(
|
||||
${this._reloadableItems.map(
|
||||
(reloadable) => html`
|
||||
<div class="card-actions">
|
||||
<ha-call-service-button
|
||||
.domain=${reloadable.domain}
|
||||
service="reload"
|
||||
.service=${reloadable.service || "reload"}
|
||||
>${reloadable.name}
|
||||
</ha-call-service-button>
|
||||
</div>
|
||||
|
||||
@@ -271,6 +271,10 @@ export class HuiEnergyUsageGraphCard
|
||||
);
|
||||
|
||||
private async _getStatistics(energyData: EnergyData): Promise<void> {
|
||||
if (!this.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const datasets: BarSeriesOption[] = [];
|
||||
|
||||
let yMin = Infinity;
|
||||
|
||||
@@ -175,6 +175,10 @@ export class HuiPowerSourcesGraphCard
|
||||
);
|
||||
|
||||
private async _getStatistics(energyData: EnergyData): Promise<void> {
|
||||
if (!this.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = generatePowerSourcesGraphData({
|
||||
localize: this.hass.localize,
|
||||
states: this.hass.states,
|
||||
|
||||
@@ -36,7 +36,7 @@ import type { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import { hasAction, hasAnyAction } from "../common/has-action";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import type {
|
||||
@@ -300,6 +300,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
return html`
|
||||
<ha-card
|
||||
class=${classMap({
|
||||
"no-action": !hasAnyAction(this._config!),
|
||||
[this._sizeController.value?.height]: Boolean(
|
||||
this._sizeController.value
|
||||
),
|
||||
@@ -730,6 +731,10 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
ha-card.no-action {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
@@ -15,13 +15,22 @@ const _loadLovelaceResource = (
|
||||
hass.auth.data.hassUrl
|
||||
).toString();
|
||||
|
||||
const logLoadError = (err: unknown) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`Failed to load Lovelace resource ${normalizedUrl} (type: ${resource.type}). Check that the URL is correct and the file exists.`,
|
||||
err
|
||||
);
|
||||
};
|
||||
|
||||
switch (resource.type) {
|
||||
case "css": {
|
||||
if (normalizedUrl in CSS_CACHE) {
|
||||
return CSS_CACHE[normalizedUrl];
|
||||
}
|
||||
|
||||
const loadTask = loadCSS(normalizedUrl);
|
||||
// Catch before caching, so a cache hit cannot log the failure again
|
||||
const loadTask = loadCSS(normalizedUrl).catch(logLoadError);
|
||||
CSS_CACHE[normalizedUrl] = loadTask;
|
||||
return loadTask;
|
||||
}
|
||||
@@ -31,13 +40,13 @@ const _loadLovelaceResource = (
|
||||
return JS_CACHE[normalizedUrl];
|
||||
}
|
||||
|
||||
const loadTask = loadJS(normalizedUrl);
|
||||
const loadTask = loadJS(normalizedUrl).catch(logLoadError);
|
||||
JS_CACHE[normalizedUrl] = loadTask;
|
||||
return loadTask;
|
||||
}
|
||||
|
||||
case "module":
|
||||
return loadModule(normalizedUrl);
|
||||
return loadModule(normalizedUrl).catch(logLoadError);
|
||||
|
||||
default:
|
||||
// eslint-disable-next-line
|
||||
|
||||
@@ -7041,6 +7041,8 @@
|
||||
"confirm_delete_user_text": "The user account for ''{name}'' will be permanently deleted. You can still track the user, but the person will no longer be able to log in.",
|
||||
"allow_login": "Allow login",
|
||||
"allow_login_description": "Allow access using username and password.",
|
||||
"select_user": "Select user account",
|
||||
"create_new_user": "Create a new username",
|
||||
"username": "[%key:ui::panel::config::users::editor::username%]",
|
||||
"password": "[%key:ui::panel::config::users::editor::password%]",
|
||||
"admin": "[%key:ui::panel::config::users::editor::admin%]",
|
||||
|
||||
@@ -279,6 +279,85 @@ test.describe("Lovelace dashboard", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Energy dashboard", () => {
|
||||
test("returns to Energy after repeatedly opening the device dialog", async ({
|
||||
page,
|
||||
}) => {
|
||||
const errors = trackPageErrors(page);
|
||||
await goToPanel(
|
||||
page,
|
||||
"/?historyBack=1&backPath=%2Flovelace#/energy/overview"
|
||||
);
|
||||
const energyRoot = page.locator("ha-panel-energy hui-root");
|
||||
await expect(energyRoot).toBeAttached({ timeout: PANEL_TIMEOUT });
|
||||
|
||||
const editDashboard = energyRoot.getByRole("button", {
|
||||
name: /^Edit dashboard\b/,
|
||||
});
|
||||
const dashboardMenu = energyRoot.getByRole("button", {
|
||||
name: "Open dashboard menu",
|
||||
});
|
||||
await expect(editDashboard.or(dashboardMenu)).toBeVisible({
|
||||
timeout: QUICK_TIMEOUT,
|
||||
});
|
||||
if (await editDashboard.isVisible()) {
|
||||
await editDashboard.click();
|
||||
} else {
|
||||
await dashboardMenu.click();
|
||||
await page.getByRole("menuitem", { name: /^Edit dashboard\b/ }).click();
|
||||
}
|
||||
|
||||
await expect(page.locator("ha-config-energy")).toBeAttached({
|
||||
timeout: PANEL_TIMEOUT,
|
||||
});
|
||||
|
||||
const backLink = page
|
||||
.locator("ha-config-energy hass-tabs-subpage")
|
||||
.getByRole("link", { name: "Back" });
|
||||
await expect(backLink).toHaveAttribute(
|
||||
"href",
|
||||
"/config/lovelace/dashboards"
|
||||
);
|
||||
|
||||
const addDevice = page
|
||||
.locator("ha-energy-device-settings")
|
||||
.locator("ha-button")
|
||||
.first();
|
||||
const openAndCancelDeviceDialog = async () => {
|
||||
await addDevice.click();
|
||||
const dialog = page.locator("dialog-energy-device-settings");
|
||||
const cancel = dialog.locator("ha-dialog-footer ha-button").first();
|
||||
await expect(cancel).toBeVisible({ timeout: QUICK_TIMEOUT });
|
||||
await cancel.click();
|
||||
await expect(cancel).toBeHidden({ timeout: QUICK_TIMEOUT });
|
||||
};
|
||||
await openAndCancelDeviceDialog();
|
||||
await openAndCancelDeviceDialog();
|
||||
await openAndCancelDeviceDialog();
|
||||
|
||||
await backLink.click();
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
page.evaluate(() => ({
|
||||
hash: window.location.hash,
|
||||
search: window.location.search,
|
||||
})),
|
||||
{ timeout: PANEL_TIMEOUT }
|
||||
)
|
||||
.toEqual({
|
||||
hash: "#/energy/overview",
|
||||
search: "?historyBack=1&backPath=%2Flovelace",
|
||||
});
|
||||
await expect(page.locator("ha-panel-energy")).toBeAttached();
|
||||
await expect(
|
||||
energyRoot.getByRole("link", { name: "Back" })
|
||||
).toHaveAttribute("href", "/lovelace");
|
||||
expectNoPageErrors(errors);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// More-info dialog (light)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
// HomeAssistantAppEl registers this path; keep it inert in E2E tests.
|
||||
void 0;
|
||||
@@ -1 +1,2 @@
|
||||
import "../../../../src/resources/append-ha-style";
|
||||
import "./ha-test";
|
||||
|
||||
@@ -247,16 +247,12 @@ export class HaTest extends HomeAssistantAppEl {
|
||||
window.__mockHass = hass;
|
||||
|
||||
// SPA navigation
|
||||
document.body.addEventListener(
|
||||
"click",
|
||||
(e) => {
|
||||
const href = isNavigationClick(e);
|
||||
if (!href) return;
|
||||
e.preventDefault();
|
||||
window.addEventListener("click", (e) => {
|
||||
const href = isNavigationClick(e);
|
||||
if (href) {
|
||||
navigate(href);
|
||||
},
|
||||
{ capture: true }
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.hassConnected();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { fetchHostDisksUsage } from "../../src/data/hassio/host";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
|
||||
const mockHass = () => {
|
||||
const callWS = vi.fn().mockResolvedValue({
|
||||
id: "root",
|
||||
label: "Total",
|
||||
total_bytes: 2000398934016,
|
||||
used_bytes: 1240247081779,
|
||||
});
|
||||
return { hass: { callWS } as unknown as HomeAssistant, callWS };
|
||||
};
|
||||
|
||||
const sentMessage = (callWS: ReturnType<typeof vi.fn>) =>
|
||||
callWS.mock.calls[0][0];
|
||||
|
||||
describe("fetchHostDisksUsage", () => {
|
||||
it("targets the data disk and sends no depth by default", async () => {
|
||||
const { hass, callWS } = mockHass();
|
||||
await fetchHostDisksUsage(hass);
|
||||
expect(callWS).toHaveBeenCalledWith({
|
||||
type: "supervisor/api",
|
||||
endpoint: "/host/disks/default/usage",
|
||||
method: "get",
|
||||
timeout: 3600,
|
||||
});
|
||||
});
|
||||
|
||||
it("addresses a mount by name", async () => {
|
||||
const { hass, callWS } = mockHass();
|
||||
await fetchHostDisksUsage(hass, "media_nas");
|
||||
expect(sentMessage(callWS).endpoint).toBe("/host/disks/media_nas/usage");
|
||||
});
|
||||
|
||||
// Walking a mount costs a round trip per directory, so the row fetch must let
|
||||
// the Supervisor apply its own per-target default rather than pinning one.
|
||||
it("omits max_depth for a mount", async () => {
|
||||
const { hass, callWS } = mockHass();
|
||||
await fetchHostDisksUsage(hass, "media_nas");
|
||||
expect(sentMessage(callWS)).not.toHaveProperty("params");
|
||||
});
|
||||
|
||||
it("sends max_depth only when a caller asks for it", async () => {
|
||||
const { hass, callWS } = mockHass();
|
||||
await fetchHostDisksUsage(hass, "default", 3);
|
||||
expect(sentMessage(callWS).params).toEqual({ max_depth: 3 });
|
||||
});
|
||||
|
||||
it("sends max_depth 0 when explicitly asked, rather than dropping it", async () => {
|
||||
const { hass, callWS } = mockHass();
|
||||
await fetchHostDisksUsage(hass, "media_nas", 0);
|
||||
expect(sentMessage(callWS).params).toEqual({ max_depth: 0 });
|
||||
});
|
||||
|
||||
it("returns the usage tree", async () => {
|
||||
const { hass } = mockHass();
|
||||
await expect(fetchHostDisksUsage(hass, "media_nas")).resolves.toMatchObject(
|
||||
{
|
||||
total_bytes: 2000398934016,
|
||||
used_bytes: 1240247081779,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,241 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import type * as HostModule from "../../../../src/data/hassio/host";
|
||||
import type * as MountsModule from "../../../../src/data/supervisor/mounts";
|
||||
import {
|
||||
fetchHassioHostInfo,
|
||||
fetchHostDisksUsage,
|
||||
} from "../../../../src/data/hassio/host";
|
||||
import {
|
||||
fetchSupervisorMounts,
|
||||
SupervisorMountState,
|
||||
SupervisorMountType,
|
||||
SupervisorMountUsage,
|
||||
} from "../../../../src/data/supervisor/mounts";
|
||||
import "../../../../src/panels/config/storage/ha-config-section-storage";
|
||||
|
||||
// Heavy children are irrelevant here and drag in echarts / ResizeObserver.
|
||||
vi.mock("../../../../src/panels/config/storage/storage-breakdown-chart", () => {
|
||||
customElements.define(
|
||||
"storage-breakdown-chart",
|
||||
class extends HTMLElement {}
|
||||
);
|
||||
return {};
|
||||
});
|
||||
vi.mock("../../../../src/layouts/hass-subpage", () => {
|
||||
customElements.define("hass-subpage", class extends HTMLElement {});
|
||||
return {};
|
||||
});
|
||||
vi.mock("../../../../src/panels/config/core/ha-config-analytics", () => ({}));
|
||||
vi.mock("../../../../src/components/ha-segmented-bar", () => {
|
||||
customElements.define("ha-segmented-bar", class extends HTMLElement {});
|
||||
return {};
|
||||
});
|
||||
vi.mock("../../../../src/components/ha-list", () => {
|
||||
customElements.define("ha-list", class extends HTMLElement {});
|
||||
return {};
|
||||
});
|
||||
vi.mock("../../../../src/components/ha-list-item", () => {
|
||||
customElements.define("ha-list-item", class extends HTMLElement {});
|
||||
return {};
|
||||
});
|
||||
vi.mock("../../../../src/components/ha-card", () => {
|
||||
customElements.define("ha-card", class extends HTMLElement {});
|
||||
return {};
|
||||
});
|
||||
vi.mock("../../../../src/components/ha-alert", () => {
|
||||
customElements.define("ha-alert", class extends HTMLElement {});
|
||||
return {};
|
||||
});
|
||||
vi.mock("../../../../src/components/ha-button", () => {
|
||||
customElements.define("ha-button", class extends HTMLElement {});
|
||||
return {};
|
||||
});
|
||||
vi.mock("../../../../src/components/ha-icon-button", () => {
|
||||
customElements.define("ha-icon-button", class extends HTMLElement {});
|
||||
return {};
|
||||
});
|
||||
vi.mock("../../../../src/components/ha-icon-next", () => {
|
||||
customElements.define("ha-icon-next", class extends HTMLElement {});
|
||||
return {};
|
||||
});
|
||||
vi.mock("../../../../src/components/ha-svg-icon", () => {
|
||||
customElements.define("ha-svg-icon", class extends HTMLElement {});
|
||||
return {};
|
||||
});
|
||||
vi.mock("../../../../src/components/ha-bar", () => {
|
||||
customElements.define("ha-bar", class extends HTMLElement {});
|
||||
return {};
|
||||
});
|
||||
vi.mock("../../../../src/components/ha-spinner", () => {
|
||||
customElements.define("ha-spinner", class extends HTMLElement {});
|
||||
return {};
|
||||
});
|
||||
|
||||
vi.mock("../../../../src/data/hassio/host", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof HostModule>()),
|
||||
fetchHassioHostInfo: vi.fn(),
|
||||
fetchHostDisksUsage: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../../../src/data/supervisor/mounts", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof MountsModule>()),
|
||||
fetchSupervisorMounts: vi.fn(),
|
||||
reloadSupervisorMount: vi.fn(),
|
||||
}));
|
||||
|
||||
interface Deferred<T> {
|
||||
promise: Promise<T>;
|
||||
resolve: (value: T) => void;
|
||||
reject: (reason?: unknown) => void;
|
||||
}
|
||||
|
||||
const deferred = <T>(): 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 };
|
||||
};
|
||||
|
||||
const mount = (name: string, state: SupervisorMountState) => ({
|
||||
name,
|
||||
usage: SupervisorMountUsage.MEDIA,
|
||||
type: SupervisorMountType.CIFS,
|
||||
server: `${name}.local`,
|
||||
port: 445,
|
||||
share: "media",
|
||||
state,
|
||||
});
|
||||
|
||||
const hass = {
|
||||
localize: (key: string, params?: Record<string, unknown>) =>
|
||||
key === "ui.panel.config.storage.detailed_description"
|
||||
? `${params!.used} of ${params!.total} used`
|
||||
: key,
|
||||
config: { components: ["hassio"] },
|
||||
// _renderDiskLifeTime formats a percentage, which needs the locale.
|
||||
locale: { language: "en" },
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const nextTask = () =>
|
||||
new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
|
||||
const rows = (el: HTMLElement) =>
|
||||
Array.from(el.shadowRoot!.querySelectorAll("ha-list-item"));
|
||||
|
||||
const rowFor = (el: HTMLElement, name: string) =>
|
||||
rows(el).find((row) => row.textContent?.includes(name));
|
||||
|
||||
describe("ha-config-section-storage per-mount usage", () => {
|
||||
let usageCalls: Record<string, Deferred<any>>;
|
||||
|
||||
beforeEach(() => {
|
||||
usageCalls = {};
|
||||
vi.mocked(fetchHassioHostInfo).mockResolvedValue({
|
||||
features: ["mount"],
|
||||
disk_life_time: 5,
|
||||
} as any);
|
||||
vi.mocked(fetchSupervisorMounts).mockResolvedValue({
|
||||
default_backup_mount: null,
|
||||
mounts: [
|
||||
mount("alpha", SupervisorMountState.ACTIVE),
|
||||
mount("beta", SupervisorMountState.ACTIVE),
|
||||
mount("broken", SupervisorMountState.FAILED),
|
||||
],
|
||||
} as any);
|
||||
vi.mocked(fetchHostDisksUsage).mockImplementation(
|
||||
(_hass, disk = "default") => {
|
||||
// The panel's own disk-metrics call must not interfere with the row calls.
|
||||
if (disk === "default") {
|
||||
return new Promise(() => {
|
||||
// never settles
|
||||
});
|
||||
}
|
||||
usageCalls[disk] = deferred<any>();
|
||||
return usageCalls[disk].promise;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.replaceChildren();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const render = async () => {
|
||||
const el = document.createElement("ha-config-section-storage");
|
||||
(el as any).hass = hass;
|
||||
document.body.append(el);
|
||||
await nextTask();
|
||||
await (el as any).updateComplete;
|
||||
return el as HTMLElement;
|
||||
};
|
||||
|
||||
it("asks only active mounts for usage", async () => {
|
||||
await render();
|
||||
const asked = vi
|
||||
.mocked(fetchHostDisksUsage)
|
||||
.mock.calls.map((call) => call[1])
|
||||
.filter((disk) => disk !== "default");
|
||||
expect(asked).toEqual(["alpha", "beta"]);
|
||||
expect(asked).not.toContain("broken");
|
||||
});
|
||||
|
||||
it("renders the rows before any usage arrives", async () => {
|
||||
const el = await render();
|
||||
expect(rows(el)).toHaveLength(3);
|
||||
expect(rowFor(el, "alpha")!.querySelector("ha-spinner")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("fills in each row as its own request settles", async () => {
|
||||
const el = await render();
|
||||
|
||||
usageCalls.alpha.resolve({
|
||||
id: "alpha",
|
||||
label: "alpha",
|
||||
total_bytes: 1000,
|
||||
used_bytes: 500,
|
||||
});
|
||||
await nextTask();
|
||||
await (el as any).updateComplete;
|
||||
|
||||
expect(rowFor(el, "alpha")!.textContent).toContain(
|
||||
"500 Bytes of 1000 Bytes used"
|
||||
);
|
||||
// beta has not answered yet, so it must still be pending, not blanked.
|
||||
expect(rowFor(el, "beta")!.querySelector("ha-spinner")).not.toBeNull();
|
||||
expect(rowFor(el, "alpha")!.querySelector("ha-spinner")).toBeNull();
|
||||
});
|
||||
|
||||
it("leaves a row without usage when its request fails, and keeps the others", async () => {
|
||||
const el = await render();
|
||||
|
||||
usageCalls.alpha.resolve({
|
||||
id: "alpha",
|
||||
label: "alpha",
|
||||
total_bytes: 1000,
|
||||
used_bytes: 500,
|
||||
});
|
||||
usageCalls.beta.reject(new Error("dead server"));
|
||||
await nextTask();
|
||||
await (el as any).updateComplete;
|
||||
|
||||
const beta = rowFor(el, "beta")!;
|
||||
expect(beta.querySelector("ha-spinner")).toBeNull();
|
||||
expect(beta.querySelector("ha-bar")).toBeNull();
|
||||
expect(beta.textContent).not.toContain("used");
|
||||
expect(rowFor(el, "alpha")!.querySelector("ha-bar")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("never shows usage for a mount that is not active", async () => {
|
||||
const el = await render();
|
||||
const broken = rowFor(el, "broken")!;
|
||||
expect(broken.querySelector("ha-spinner")).toBeNull();
|
||||
expect(broken.querySelector("ha-bar")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { loadJS, loadModule } from "../../../../src/common/dom/load_resource";
|
||||
import { loadLovelaceResourcesAndWait } from "../../../../src/panels/lovelace/common/load-resources";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
|
||||
vi.mock("../../../../src/common/dom/load_resource", () => ({
|
||||
loadCSS: vi.fn(),
|
||||
loadJS: vi.fn(),
|
||||
loadModule: vi.fn(),
|
||||
}));
|
||||
|
||||
const hass = {
|
||||
auth: { data: { hassUrl: "http://localhost:8123" } },
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
describe("loadLovelaceResourcesAndWait", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("logs an error naming the resource that failed to load", async () => {
|
||||
const consoleError = vi
|
||||
.spyOn(console, "error")
|
||||
.mockImplementation(() => undefined);
|
||||
const loadError = new Error("load failed");
|
||||
vi.mocked(loadModule).mockImplementation(() => Promise.reject(loadError));
|
||||
|
||||
await loadLovelaceResourcesAndWait(
|
||||
[{ id: "1", url: "/local/missing.js", type: "module" }],
|
||||
hass
|
||||
);
|
||||
|
||||
expect(consoleError).toHaveBeenCalledTimes(1);
|
||||
expect(consoleError.mock.calls[0][0]).toContain(
|
||||
"http://localhost:8123/local/missing.js"
|
||||
);
|
||||
expect(consoleError.mock.calls[0][0]).toContain("module");
|
||||
// the rejection reason is forwarded, so any detail it carries is not lost
|
||||
expect(consoleError.mock.calls[0][1]).toBe(loadError);
|
||||
});
|
||||
|
||||
it("logs only once for a cached resource that failed to load", async () => {
|
||||
const consoleError = vi
|
||||
.spyOn(console, "error")
|
||||
.mockImplementation(() => undefined);
|
||||
vi.mocked(loadJS).mockImplementation((url) => Promise.reject(url));
|
||||
const resources = [
|
||||
{ id: "2", url: "/local/broken.js", type: "js" as const },
|
||||
];
|
||||
|
||||
await loadLovelaceResourcesAndWait(resources, hass);
|
||||
await loadLovelaceResourcesAndWait(resources, hass);
|
||||
|
||||
expect(loadJS).toHaveBeenCalledTimes(1);
|
||||
expect(consoleError).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -4648,22 +4648,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rsdoctor/client@npm:1.6.1":
|
||||
version: 1.6.1
|
||||
resolution: "@rsdoctor/client@npm:1.6.1"
|
||||
checksum: 10/aefc3a378e3ecdc9f03c21272dfb06d4eb68ca5395d83edb0a44fd816ea96b45e894de8c862e605fcb02611d306014ffef4ca09723945cf75c9a33b7062d5d4b
|
||||
"@rsdoctor/client@npm:1.6.2":
|
||||
version: 1.6.2
|
||||
resolution: "@rsdoctor/client@npm:1.6.2"
|
||||
checksum: 10/7b57cf3a9c6e3dc1b64b0f07969330579fd0e84963843cb3517b7c1160bc9b2fa08e709eaf835139d8a7e8c32977af8aae050d100df80daed44371ced8272642
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rsdoctor/core@npm:1.6.1":
|
||||
version: 1.6.1
|
||||
resolution: "@rsdoctor/core@npm:1.6.1"
|
||||
"@rsdoctor/core@npm:1.6.2":
|
||||
version: 1.6.2
|
||||
resolution: "@rsdoctor/core@npm:1.6.2"
|
||||
dependencies:
|
||||
"@rsbuild/plugin-check-syntax": "npm:^1.6.1"
|
||||
"@rsdoctor/graph": "npm:1.6.1"
|
||||
"@rsdoctor/sdk": "npm:1.6.1"
|
||||
"@rsdoctor/types": "npm:1.6.1"
|
||||
"@rsdoctor/utils": "npm:1.6.1"
|
||||
"@rsdoctor/graph": "npm:1.6.2"
|
||||
"@rsdoctor/sdk": "npm:1.6.2"
|
||||
"@rsdoctor/types": "npm:1.6.2"
|
||||
"@rsdoctor/utils": "npm:1.6.2"
|
||||
"@rspack/resolver": "npm:^0.2.8"
|
||||
browserslist-load-config: "npm:^1.0.2"
|
||||
es-toolkit: "npm:^1.49.0"
|
||||
@@ -4671,60 +4671,60 @@ __metadata:
|
||||
fs-extra: "npm:^11.1.1"
|
||||
semver: "npm:^7.8.5"
|
||||
source-map: "npm:^0.7.6"
|
||||
checksum: 10/de5708e15e1efd67a23d753ed906e573ca6a6857428dbdfea0aff2f85f1ed3f631ff34e23bf40e955855ed878ed548c073c620a4edbd12959c20a3f9ff9c1cfd
|
||||
checksum: 10/803bb41042b2af07b096f2dde6383d516bb22b62705aeb9693959788e8a63df80e6d25514f1d92639a267e46e73732689abf5ba2c25679fbd80da9dd126f7ed9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rsdoctor/graph@npm:1.6.1":
|
||||
version: 1.6.1
|
||||
resolution: "@rsdoctor/graph@npm:1.6.1"
|
||||
"@rsdoctor/graph@npm:1.6.2":
|
||||
version: 1.6.2
|
||||
resolution: "@rsdoctor/graph@npm:1.6.2"
|
||||
dependencies:
|
||||
"@rsdoctor/types": "npm:1.6.1"
|
||||
"@rsdoctor/utils": "npm:1.6.1"
|
||||
"@rsdoctor/types": "npm:1.6.2"
|
||||
"@rsdoctor/utils": "npm:1.6.2"
|
||||
es-toolkit: "npm:^1.49.0"
|
||||
path-browserify: "npm:1.0.1"
|
||||
source-map: "npm:^0.7.6"
|
||||
checksum: 10/065c485f11939143021c12a1acff47a2539c22e9a9d5f8501c906578213ca16b71a35d08bc0a83035529d42a7906fc098b6e3c536d895cd13ee74b5340a39e40
|
||||
checksum: 10/f28aaedc529a6cff782f1813faa6d19d10f76ab8f529bbe2899385b5659d247f0543149e2fcd700264a00e9568605a85fde04cbf634e821f24e9bf239eb1b009
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rsdoctor/rspack-plugin@npm:1.6.1":
|
||||
version: 1.6.1
|
||||
resolution: "@rsdoctor/rspack-plugin@npm:1.6.1"
|
||||
"@rsdoctor/rspack-plugin@npm:1.6.2":
|
||||
version: 1.6.2
|
||||
resolution: "@rsdoctor/rspack-plugin@npm:1.6.2"
|
||||
dependencies:
|
||||
"@rsdoctor/core": "npm:1.6.1"
|
||||
"@rsdoctor/graph": "npm:1.6.1"
|
||||
"@rsdoctor/sdk": "npm:1.6.1"
|
||||
"@rsdoctor/types": "npm:1.6.1"
|
||||
"@rsdoctor/utils": "npm:1.6.1"
|
||||
"@rsdoctor/core": "npm:1.6.2"
|
||||
"@rsdoctor/graph": "npm:1.6.2"
|
||||
"@rsdoctor/sdk": "npm:1.6.2"
|
||||
"@rsdoctor/types": "npm:1.6.2"
|
||||
"@rsdoctor/utils": "npm:1.6.2"
|
||||
peerDependencies:
|
||||
"@rspack/core": "*"
|
||||
peerDependenciesMeta:
|
||||
"@rspack/core":
|
||||
optional: true
|
||||
checksum: 10/8908f4a3c2b46fdaba2c6d39dd535623ddcc411fb50d358de949142a5628a512827f5206a93ae975bd93e5de6b72d5cd71f63463c65a37ba718727567187ce8e
|
||||
checksum: 10/dec9f0424d14ec7ed03cd6cbc5e0b11cd74c29be12c3f79466bf8da0f317a4bdba858a58daf64e4a7c16d20a1d58433b1d2c038b62ff771c1d045bca3d71c3ad
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rsdoctor/sdk@npm:1.6.1":
|
||||
version: 1.6.1
|
||||
resolution: "@rsdoctor/sdk@npm:1.6.1"
|
||||
"@rsdoctor/sdk@npm:1.6.2":
|
||||
version: 1.6.2
|
||||
resolution: "@rsdoctor/sdk@npm:1.6.2"
|
||||
dependencies:
|
||||
"@rsdoctor/client": "npm:1.6.1"
|
||||
"@rsdoctor/graph": "npm:1.6.1"
|
||||
"@rsdoctor/types": "npm:1.6.1"
|
||||
"@rsdoctor/utils": "npm:1.6.1"
|
||||
"@rsdoctor/client": "npm:1.6.2"
|
||||
"@rsdoctor/graph": "npm:1.6.2"
|
||||
"@rsdoctor/types": "npm:1.6.2"
|
||||
"@rsdoctor/utils": "npm:1.6.2"
|
||||
launch-editor: "npm:^2.13.2"
|
||||
safer-buffer: "npm:2.1.2"
|
||||
socket.io: "npm:4.8.1"
|
||||
tapable: "npm:2.3.3"
|
||||
checksum: 10/d667d62d730bed7606bcf1ed2063fb7597024e31d9108c3ba9729313764866407c216499ccb3c7102f239d7d82463e510f2b2ec71270af6a40039cbe6f876d23
|
||||
checksum: 10/89d5850145b490bbf3f43486176ebdd1f9eb67cdbe96a07549bb0b400ab76d85f3b6d5667be7cf8304434281f0db74fbaaab3360228eb1650f0a8ce640101208
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rsdoctor/types@npm:1.6.1":
|
||||
version: 1.6.1
|
||||
resolution: "@rsdoctor/types@npm:1.6.1"
|
||||
"@rsdoctor/types@npm:1.6.2":
|
||||
version: 1.6.2
|
||||
resolution: "@rsdoctor/types@npm:1.6.2"
|
||||
dependencies:
|
||||
"@types/connect": "npm:3.4.38"
|
||||
"@types/estree": "npm:1.0.5"
|
||||
@@ -4738,16 +4738,16 @@ __metadata:
|
||||
optional: true
|
||||
webpack:
|
||||
optional: true
|
||||
checksum: 10/1c4ae6c1aa6fa525d65ab78396954d86358880959ee3fdb68247c2f3215a35e4cb2aab3836cb82f6c208c2c3834cfe0dc8bf575ba59fd8e2ff89ffc92322fcad
|
||||
checksum: 10/39328739b31f7df46b663a9f6b5ac8c1e290c6b7ce0f68d9b45a423b89a1a296d32fbee0c26f555fc40d60b356a3605909f7bbe19c77d9c6bca3603d6928aef2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rsdoctor/utils@npm:1.6.1":
|
||||
version: 1.6.1
|
||||
resolution: "@rsdoctor/utils@npm:1.6.1"
|
||||
"@rsdoctor/utils@npm:1.6.2":
|
||||
version: 1.6.2
|
||||
resolution: "@rsdoctor/utils@npm:1.6.2"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:7.26.2"
|
||||
"@rsdoctor/types": "npm:1.6.1"
|
||||
"@rsdoctor/types": "npm:1.6.2"
|
||||
"@types/estree": "npm:1.0.5"
|
||||
acorn: "npm:^8.10.0"
|
||||
acorn-import-attributes: "npm:^1.9.5"
|
||||
@@ -4761,83 +4761,83 @@ __metadata:
|
||||
picocolors: "npm:^1.1.1"
|
||||
rslog: "npm:^2.1.2"
|
||||
strip-ansi: "npm:^7.2.0"
|
||||
checksum: 10/11f4e9136dd849fa05b0ab56ab0de3be3de43880d530dcbfb1835780dccad9976636f6a743852893948ae053e71fb7cd6144cac3ffac27929a87710e3981c048
|
||||
checksum: 10/fb6a147e452756018e93789e07dbe015354fc2690699755ef411748a393c7db86fdff2614cdf9ce07822434abcac3eef362aea8c63cbca9fe3f96c958fde56dc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-darwin-arm64@npm:2.1.9":
|
||||
version: 2.1.9
|
||||
resolution: "@rspack/binding-darwin-arm64@npm:2.1.9"
|
||||
"@rspack/binding-darwin-arm64@npm:2.1.10":
|
||||
version: 2.1.10
|
||||
resolution: "@rspack/binding-darwin-arm64@npm:2.1.10"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-darwin-x64@npm:2.1.9":
|
||||
version: 2.1.9
|
||||
resolution: "@rspack/binding-darwin-x64@npm:2.1.9"
|
||||
"@rspack/binding-darwin-x64@npm:2.1.10":
|
||||
version: 2.1.10
|
||||
resolution: "@rspack/binding-darwin-x64@npm:2.1.10"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-arm64-gnu@npm:2.1.9":
|
||||
version: 2.1.9
|
||||
resolution: "@rspack/binding-linux-arm64-gnu@npm:2.1.9"
|
||||
"@rspack/binding-linux-arm64-gnu@npm:2.1.10":
|
||||
version: 2.1.10
|
||||
resolution: "@rspack/binding-linux-arm64-gnu@npm:2.1.10"
|
||||
conditions: os=linux & cpu=arm64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-arm64-musl@npm:2.1.9":
|
||||
version: 2.1.9
|
||||
resolution: "@rspack/binding-linux-arm64-musl@npm:2.1.9"
|
||||
"@rspack/binding-linux-arm64-musl@npm:2.1.10":
|
||||
version: 2.1.10
|
||||
resolution: "@rspack/binding-linux-arm64-musl@npm:2.1.10"
|
||||
conditions: os=linux & cpu=arm64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-ppc64-gnu@npm:2.1.9":
|
||||
version: 2.1.9
|
||||
resolution: "@rspack/binding-linux-ppc64-gnu@npm:2.1.9"
|
||||
"@rspack/binding-linux-ppc64-gnu@npm:2.1.10":
|
||||
version: 2.1.10
|
||||
resolution: "@rspack/binding-linux-ppc64-gnu@npm:2.1.10"
|
||||
conditions: os=linux & cpu=ppc64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-riscv64-gnu@npm:2.1.9":
|
||||
version: 2.1.9
|
||||
resolution: "@rspack/binding-linux-riscv64-gnu@npm:2.1.9"
|
||||
"@rspack/binding-linux-riscv64-gnu@npm:2.1.10":
|
||||
version: 2.1.10
|
||||
resolution: "@rspack/binding-linux-riscv64-gnu@npm:2.1.10"
|
||||
conditions: os=linux & cpu=riscv64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-riscv64-musl@npm:2.1.9":
|
||||
version: 2.1.9
|
||||
resolution: "@rspack/binding-linux-riscv64-musl@npm:2.1.9"
|
||||
"@rspack/binding-linux-riscv64-musl@npm:2.1.10":
|
||||
version: 2.1.10
|
||||
resolution: "@rspack/binding-linux-riscv64-musl@npm:2.1.10"
|
||||
conditions: os=linux & cpu=riscv64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-s390x-gnu@npm:2.1.9":
|
||||
version: 2.1.9
|
||||
resolution: "@rspack/binding-linux-s390x-gnu@npm:2.1.9"
|
||||
"@rspack/binding-linux-s390x-gnu@npm:2.1.10":
|
||||
version: 2.1.10
|
||||
resolution: "@rspack/binding-linux-s390x-gnu@npm:2.1.10"
|
||||
conditions: os=linux & cpu=s390x & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-x64-gnu@npm:2.1.9":
|
||||
version: 2.1.9
|
||||
resolution: "@rspack/binding-linux-x64-gnu@npm:2.1.9"
|
||||
"@rspack/binding-linux-x64-gnu@npm:2.1.10":
|
||||
version: 2.1.10
|
||||
resolution: "@rspack/binding-linux-x64-gnu@npm:2.1.10"
|
||||
conditions: os=linux & cpu=x64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-x64-musl@npm:2.1.9":
|
||||
version: 2.1.9
|
||||
resolution: "@rspack/binding-linux-x64-musl@npm:2.1.9"
|
||||
"@rspack/binding-linux-x64-musl@npm:2.1.10":
|
||||
version: 2.1.10
|
||||
resolution: "@rspack/binding-linux-x64-musl@npm:2.1.10"
|
||||
conditions: os=linux & cpu=x64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-wasm32-wasi@npm:2.1.9":
|
||||
version: 2.1.9
|
||||
resolution: "@rspack/binding-wasm32-wasi@npm:2.1.9"
|
||||
"@rspack/binding-wasm32-wasi@npm:2.1.10":
|
||||
version: 2.1.10
|
||||
resolution: "@rspack/binding-wasm32-wasi@npm:2.1.10"
|
||||
dependencies:
|
||||
"@emnapi/core": "npm:1.11.3"
|
||||
"@emnapi/runtime": "npm:1.11.3"
|
||||
@@ -4846,45 +4846,45 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-win32-arm64-msvc@npm:2.1.9":
|
||||
version: 2.1.9
|
||||
resolution: "@rspack/binding-win32-arm64-msvc@npm:2.1.9"
|
||||
"@rspack/binding-win32-arm64-msvc@npm:2.1.10":
|
||||
version: 2.1.10
|
||||
resolution: "@rspack/binding-win32-arm64-msvc@npm:2.1.10"
|
||||
conditions: os=win32 & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-win32-ia32-msvc@npm:2.1.9":
|
||||
version: 2.1.9
|
||||
resolution: "@rspack/binding-win32-ia32-msvc@npm:2.1.9"
|
||||
"@rspack/binding-win32-ia32-msvc@npm:2.1.10":
|
||||
version: 2.1.10
|
||||
resolution: "@rspack/binding-win32-ia32-msvc@npm:2.1.10"
|
||||
conditions: os=win32 & cpu=ia32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-win32-x64-msvc@npm:2.1.9":
|
||||
version: 2.1.9
|
||||
resolution: "@rspack/binding-win32-x64-msvc@npm:2.1.9"
|
||||
"@rspack/binding-win32-x64-msvc@npm:2.1.10":
|
||||
version: 2.1.10
|
||||
resolution: "@rspack/binding-win32-x64-msvc@npm:2.1.10"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding@npm:2.1.9":
|
||||
version: 2.1.9
|
||||
resolution: "@rspack/binding@npm:2.1.9"
|
||||
"@rspack/binding@npm:2.1.10":
|
||||
version: 2.1.10
|
||||
resolution: "@rspack/binding@npm:2.1.10"
|
||||
dependencies:
|
||||
"@rspack/binding-darwin-arm64": "npm:2.1.9"
|
||||
"@rspack/binding-darwin-x64": "npm:2.1.9"
|
||||
"@rspack/binding-linux-arm64-gnu": "npm:2.1.9"
|
||||
"@rspack/binding-linux-arm64-musl": "npm:2.1.9"
|
||||
"@rspack/binding-linux-ppc64-gnu": "npm:2.1.9"
|
||||
"@rspack/binding-linux-riscv64-gnu": "npm:2.1.9"
|
||||
"@rspack/binding-linux-riscv64-musl": "npm:2.1.9"
|
||||
"@rspack/binding-linux-s390x-gnu": "npm:2.1.9"
|
||||
"@rspack/binding-linux-x64-gnu": "npm:2.1.9"
|
||||
"@rspack/binding-linux-x64-musl": "npm:2.1.9"
|
||||
"@rspack/binding-wasm32-wasi": "npm:2.1.9"
|
||||
"@rspack/binding-win32-arm64-msvc": "npm:2.1.9"
|
||||
"@rspack/binding-win32-ia32-msvc": "npm:2.1.9"
|
||||
"@rspack/binding-win32-x64-msvc": "npm:2.1.9"
|
||||
"@rspack/binding-darwin-arm64": "npm:2.1.10"
|
||||
"@rspack/binding-darwin-x64": "npm:2.1.10"
|
||||
"@rspack/binding-linux-arm64-gnu": "npm:2.1.10"
|
||||
"@rspack/binding-linux-arm64-musl": "npm:2.1.10"
|
||||
"@rspack/binding-linux-ppc64-gnu": "npm:2.1.10"
|
||||
"@rspack/binding-linux-riscv64-gnu": "npm:2.1.10"
|
||||
"@rspack/binding-linux-riscv64-musl": "npm:2.1.10"
|
||||
"@rspack/binding-linux-s390x-gnu": "npm:2.1.10"
|
||||
"@rspack/binding-linux-x64-gnu": "npm:2.1.10"
|
||||
"@rspack/binding-linux-x64-musl": "npm:2.1.10"
|
||||
"@rspack/binding-wasm32-wasi": "npm:2.1.10"
|
||||
"@rspack/binding-win32-arm64-msvc": "npm:2.1.10"
|
||||
"@rspack/binding-win32-ia32-msvc": "npm:2.1.10"
|
||||
"@rspack/binding-win32-x64-msvc": "npm:2.1.10"
|
||||
dependenciesMeta:
|
||||
"@rspack/binding-darwin-arm64":
|
||||
optional: true
|
||||
@@ -4914,15 +4914,15 @@ __metadata:
|
||||
optional: true
|
||||
"@rspack/binding-win32-x64-msvc":
|
||||
optional: true
|
||||
checksum: 10/bf3ef6f75b0fa3dafc8dc73bfb49ae31e706e466699a88e0ef35d7273800e3c241489def836041688dded31b9ec3eca61d12bc0c30f8fa18839ae879ab8d2182
|
||||
checksum: 10/b15410b23aac7bf934d271a17994fc13dd10ddf65eeaa20a84dedb2b2baf753cd411c4b56e971728c3ee054be5bafd46238e7fc9721ae883a0a8a5c5e121bd66
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/core@npm:2.1.9":
|
||||
version: 2.1.9
|
||||
resolution: "@rspack/core@npm:2.1.9"
|
||||
"@rspack/core@npm:2.1.10":
|
||||
version: 2.1.10
|
||||
resolution: "@rspack/core@npm:2.1.10"
|
||||
dependencies:
|
||||
"@rspack/binding": "npm:2.1.9"
|
||||
"@rspack/binding": "npm:2.1.10"
|
||||
peerDependencies:
|
||||
"@module-federation/runtime-tools": ^0.24.1 || ^2.0.0
|
||||
"@swc/helpers": ^0.5.23
|
||||
@@ -4931,7 +4931,7 @@ __metadata:
|
||||
optional: true
|
||||
"@swc/helpers":
|
||||
optional: true
|
||||
checksum: 10/81a8d5b4855ea8783d18e9e0c422e18337e50b820bf1f867d5287dc5a3643307c03e39011049f485e63e676480ba71f5fb0e7fb7f4ceea72e4bc2eec2651c13d
|
||||
checksum: 10/bf1e7183aa5e507668cc3233562a74d2d61ddbfb7279748ae9569094f43e041ced5893fe7191de3e2d44306cc017c6db0c0c4d823bc2fd9c6fbc53fbb18a8f22
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9698,10 +9698,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"globals@npm:17.9.0":
|
||||
version: 17.9.0
|
||||
resolution: "globals@npm:17.9.0"
|
||||
checksum: 10/8619386d449e8cbb81e242eced8bca0a42e5c3633a3a35996b8b1618beb17ff9fb33bfe703d60d83d4b7f6f8a996dede4faf02f9e966e50ff895e0cf6122ae15
|
||||
"globals@npm:17.11.0":
|
||||
version: 17.11.0
|
||||
resolution: "globals@npm:17.11.0"
|
||||
checksum: 10/9cff5b8099b585862ab0d31e8b93dc1c3e7b59239dc58b59447a38709715751df8794ee0d28f434f8e421b01aee3cc10051e2455b5c967d960e79f882e68aa0f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9888,10 +9888,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hls.js@npm:1.6.17":
|
||||
version: 1.6.17
|
||||
resolution: "hls.js@npm:1.6.17"
|
||||
checksum: 10/345d6c98f7f32f25f1924d34d4177f37aa9ae10c95fe6518c4ca10b295acbf174fbeba113696fbce673a1ee90461b530e101dd2b2f4d958af82577706d6e24c2
|
||||
"hls.js@npm:1.7.0":
|
||||
version: 1.7.0
|
||||
resolution: "hls.js@npm:1.7.0"
|
||||
checksum: 10/8bb97ba86b72a3be057bd0036b01df83adfe7dc10c76a5e177b24bae3f380e19d4180e061d8140b3b826e10c9f61a7b230e10019dcb81619ab2fc41e4d292ed9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9955,8 +9955,8 @@ __metadata:
|
||||
"@octokit/rest": "npm:22.0.1"
|
||||
"@playwright/test": "npm:1.62.1"
|
||||
"@replit/codemirror-indentation-markers": "npm:6.5.3"
|
||||
"@rsdoctor/rspack-plugin": "npm:1.6.1"
|
||||
"@rspack/core": "npm:2.1.9"
|
||||
"@rsdoctor/rspack-plugin": "npm:1.6.2"
|
||||
"@rspack/core": "npm:2.1.10"
|
||||
"@rspack/dev-server": "npm:2.2.0"
|
||||
"@swc/helpers": "npm:0.5.23"
|
||||
"@thomasloven/round-slider": "npm:0.6.0"
|
||||
@@ -10012,11 +10012,11 @@ __metadata:
|
||||
fuse.js: "npm:7.5.0"
|
||||
generate-license-file: "npm:4.2.1"
|
||||
glob: "npm:13.0.6"
|
||||
globals: "npm:17.9.0"
|
||||
globals: "npm:17.11.0"
|
||||
gulp: "npm:5.0.1"
|
||||
gulp-json-transform: "npm:0.5.0"
|
||||
gulp-rename: "npm:2.1.0"
|
||||
hls.js: "npm:1.6.17"
|
||||
hls.js: "npm:1.7.0"
|
||||
home-assistant-js-websocket: "npm:9.6.0"
|
||||
html-minifier-terser: "npm:7.2.0"
|
||||
husky: "npm:9.1.7"
|
||||
|
||||
Reference in New Issue
Block a user