mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-20 13:37:39 +00:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a0db4a796 | ||
|
|
622f3e97e6 | ||
|
|
3091362040 | ||
|
|
3c7560a4e2 | ||
|
|
a9e178cb0e | ||
|
|
23b83fbd27 | ||
|
|
62849e2630 | ||
|
|
aa3e11f6e9 |
+40
-24
@@ -1,36 +1,52 @@
|
||||
[modern]
|
||||
# Modern builds target recent browsers supporting the latest features to minimize transpilation, polyfills, etc.
|
||||
# It is served to browsers meeting the following requirements:
|
||||
# - released in the last year + current alpha/beta versions
|
||||
# - Firefox extended support release (ESR)
|
||||
# - with global utilization at or above 0.5%
|
||||
# - released in the last 2 years + current alpha/beta versions
|
||||
# - exclude dead browsers (no security maintenance for 2+ years)
|
||||
# - exclude KaiOS, QQ, and UC browsers due to lack of sufficient feature support data
|
||||
# - exclude QQ, and UC browsers due to lack of sufficient feature support data
|
||||
unreleased versions
|
||||
last 1 year
|
||||
Firefox ESR
|
||||
>= 0.5%
|
||||
last 2 years
|
||||
not dead
|
||||
not KaiOS > 0
|
||||
not QQAndroid > 0
|
||||
not UCAndroid > 0
|
||||
|
||||
[legacy]
|
||||
# Legacy builds are served when modern requirements are not met and support browsers:
|
||||
# - released in the last 7 years + current alpha/beta versionss
|
||||
# - with global utilization at or above 0.05%
|
||||
# - exclude dead browsers (no security maintenance for 2+ years)
|
||||
# - exclude Opera Mini which does not support web sockets
|
||||
unreleased versions
|
||||
last 7 years
|
||||
>= 0.05%
|
||||
not dead
|
||||
not op_mini all
|
||||
# Legacy builds are served when modern requirements are not met.
|
||||
# Floors are pinned explicitly (not usage-based) so the support policy is
|
||||
# deliberate and does not drift with global usage statistics, which do not
|
||||
# represent old tablets and wall displays used as Home Assistant dashboards:
|
||||
# - iOS/Safari >= 12: iPad Air 1 / mini 2 / mini 3 (last supported iOS).
|
||||
# Older iPads (iPad 2/3/mini 1 on iOS 9.3, iPad 4 on iOS 10.3) cannot run
|
||||
# the app: their engines lack custom elements, shadow DOM, and/or CSS grid.
|
||||
# - Chrome >= 59: Fire OS 5 tablets (Fire 7/HD 8/HD 10 through ~2017) have
|
||||
# their system WebView pinned at Chromium 59 and commonly run Fully Kiosk;
|
||||
# also covers old kiosk browsers and no-longer-updating webviews above it.
|
||||
# - Edge >= 79: all Chromium-based Edge. Costs nothing (above the Chrome
|
||||
# floor); mainly catches Edge 109 on Windows 7/8.1 and update-frozen
|
||||
# enterprise installs, whose engines can run the legacy build fine.
|
||||
# - Firefox >= 94: the zero-cost floor, not a chased population — pinning it
|
||||
# adds no babel transforms, core-js modules, or Lightning CSS prefixes to
|
||||
# the output. Mozilla-supported Firefox (incl. current ESR) always matches
|
||||
# [modern]; Firefox on old OSes is not supported (use Chrome 109 instead).
|
||||
# If Firefox ever becomes the pin forcing extra output, raise it first.
|
||||
# - Samsung >= 9: Samsung Internet on old Galaxy tablets
|
||||
Chrome >= 59
|
||||
ChromeAndroid >= 59
|
||||
Edge >= 79
|
||||
Firefox >= 94
|
||||
FirefoxAndroid >= 94
|
||||
iOS >= 12
|
||||
Safari >= 12
|
||||
Samsung >= 9
|
||||
|
||||
[legacy-sw]
|
||||
# Same as legacy plus supports service workers
|
||||
unreleased versions
|
||||
last 7 years
|
||||
>= 0.05% and supports serviceworkers
|
||||
not dead
|
||||
not op_mini all
|
||||
# Same as legacy, restricted to browsers that support service workers
|
||||
# (currently resolves to the same set; guards the service worker build if the legacy floor ever drops below them)
|
||||
Chrome >= 59 and supports serviceworkers
|
||||
ChromeAndroid >= 59 and supports serviceworkers
|
||||
Edge >= 79 and supports serviceworkers
|
||||
Firefox >= 94 and supports serviceworkers
|
||||
FirefoxAndroid >= 94 and supports serviceworkers
|
||||
iOS >= 12 and supports serviceworkers
|
||||
Safari >= 12 and supports serviceworkers
|
||||
Samsung >= 9 and supports serviceworkers
|
||||
|
||||
@@ -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": 585518,
|
||||
"core": 57048,
|
||||
"authorize": 552423,
|
||||
"onboarding": 666818
|
||||
"app": 576583,
|
||||
"core": 54790,
|
||||
"authorize": 543547,
|
||||
"onboarding": 655556
|
||||
},
|
||||
"frontend-legacy": {
|
||||
"app": 887384,
|
||||
"core": 244482,
|
||||
"authorize": 844995,
|
||||
"onboarding": 1008816
|
||||
"app": 717124,
|
||||
"core": 181583,
|
||||
"authorize": 699175,
|
||||
"onboarding": 816133
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,8 +63,10 @@ module.exports.htmlMinifierOptions = {
|
||||
};
|
||||
|
||||
module.exports.terserOptions = ({ latestBuild, isTestBuild }) => ({
|
||||
safari10: !latestBuild,
|
||||
ecma: latestBuild ? 2015 : 5,
|
||||
// Highest syntax the minifier may emit; it never downlevels. Every browser
|
||||
// in [modern] is well past ES2020 (universal since spring 2020); the
|
||||
// [legacy] floors (Chrome 59 / Safari 12) top out at ES2017.
|
||||
ecma: latestBuild ? 2020 : 2017,
|
||||
module: latestBuild,
|
||||
format: { comments: false },
|
||||
sourceMap: !isTestBuild,
|
||||
|
||||
@@ -25,6 +25,9 @@ title: Button
|
||||
<ha-button appearance="filled">
|
||||
filled button
|
||||
</ha-button>
|
||||
<ha-button appearance="outlined">
|
||||
outlined button
|
||||
</ha-button>
|
||||
|
||||
<ha-button size="s">
|
||||
small
|
||||
@@ -65,7 +68,7 @@ Check the [webawesome documentation](https://webawesome.com/docs/components/butt
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| ---------- | ---------------------------------------------- | -------- | --------------------------------------------------------------------------------- |
|
||||
| appearance | "accent"/"filled"/"plain" | "accent" | Sets the button appearance. |
|
||||
| appearance | "accent"/"filled"/"outlined"/"plain" | "accent" | Sets the button appearance. |
|
||||
| variants | "brand"/"danger"/"neutral"/"warning"/"success" | "brand" | Sets the button color variant. "brand" is default. |
|
||||
| size | "xs"/"s"/"m"/"l"/"xl" | "m" | Sets the button size. |
|
||||
| loading | Boolean | false | Shows a loading indicator instead of the buttons label and disable buttons click. |
|
||||
|
||||
@@ -9,7 +9,7 @@ import "../../../../src/components/ha-svg-icon";
|
||||
import { mdiHomeAssistant } from "../../../../src/resources/home-assistant-logo-svg";
|
||||
import { THEME_COMPARISON_PANELS } from "../../components/demo-theme-comparison";
|
||||
|
||||
const appearances = ["accent", "filled", "plain"];
|
||||
const appearances = ["accent", "filled", "outlined", "plain"];
|
||||
const variants = ["brand", "danger", "neutral", "warning", "success"];
|
||||
|
||||
@customElement("demo-components-ha-button")
|
||||
|
||||
@@ -29,6 +29,50 @@ const positions: Position[] = ["start", "end"];
|
||||
const selectedStates = [false, true];
|
||||
const disabledStates = [false, true];
|
||||
|
||||
interface TreeChild {
|
||||
key: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface TreeGroup {
|
||||
key: string;
|
||||
label: string;
|
||||
children: TreeChild[];
|
||||
}
|
||||
|
||||
const treeGroups: TreeGroup[] = [
|
||||
{
|
||||
key: "binary_sensor",
|
||||
label: "Binary sensor",
|
||||
children: [
|
||||
{ key: "door", label: "Door" },
|
||||
{ key: "motion", label: "Motion" },
|
||||
{ key: "window", label: "Window" },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "cover",
|
||||
label: "Cover",
|
||||
children: [
|
||||
{ key: "garage", label: "Garage" },
|
||||
{ key: "shutter", label: "Shutter" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
interface TreeRow {
|
||||
group: TreeGroup;
|
||||
child?: TreeChild;
|
||||
}
|
||||
|
||||
const treeRows: TreeRow[] = treeGroups.flatMap((group) => [
|
||||
{ group },
|
||||
...group.children.map((child) => ({ group, child })),
|
||||
]);
|
||||
|
||||
const treeKey = (group: TreeGroup, child: TreeChild) =>
|
||||
`${group.key}/${child.key}`;
|
||||
|
||||
@customElement("demo-components-ha-list")
|
||||
export class DemoHaList extends LitElement {
|
||||
@state() private _buttonClicks = 0;
|
||||
@@ -41,6 +85,8 @@ export class DemoHaList extends LitElement {
|
||||
|
||||
@state() private _multiCheckEnd: number | Set<number> = new Set();
|
||||
|
||||
@state() private _tree = new Set<string>();
|
||||
|
||||
private _options = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon"];
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -274,6 +320,45 @@ selected: ${JSON.stringify(this._toJson(this._multiCheckStart))}</pre>
|
||||
selected: ${JSON.stringify(this._toJson(this._multiCheckEnd))}</pre>
|
||||
</ha-card>
|
||||
|
||||
<ha-card header="Controlled selection with indeterminate groups">
|
||||
<ha-list-selectable
|
||||
multi
|
||||
controlled
|
||||
aria-label="Controlled tree"
|
||||
@ha-list-item-selected=${this._onTreeToggle}
|
||||
@ha-list-item-deselected=${this._onTreeToggle}
|
||||
>
|
||||
${treeGroups.map((group) => {
|
||||
const groupState = this._groupState(group);
|
||||
return html`
|
||||
<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${group.key}
|
||||
?selected=${groupState === "all"}
|
||||
?indeterminate=${groupState === "some"}
|
||||
>
|
||||
<span slot="headline">${group.label}</span>
|
||||
</ha-list-item-option>
|
||||
${group.children.map(
|
||||
(child) => html`
|
||||
<ha-list-item-option
|
||||
class="child"
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${treeKey(group, child)}
|
||||
?selected=${this._tree.has(treeKey(group, child))}
|
||||
>
|
||||
<span slot="headline">${child.label}</span>
|
||||
</ha-list-item-option>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
})}
|
||||
</ha-list-selectable>
|
||||
<pre>selected: ${JSON.stringify([...this._tree])}</pre>
|
||||
</ha-card>
|
||||
|
||||
<ha-card header="Option: all combinations">
|
||||
<div class="grid">
|
||||
${appearances.map((appearance) =>
|
||||
@@ -361,6 +446,43 @@ selected: ${JSON.stringify(this._toJson(this._multiCheckEnd))}</pre>
|
||||
return next;
|
||||
}
|
||||
|
||||
private _groupState(group: TreeGroup): "none" | "some" | "all" {
|
||||
const selected = group.children.filter((child) =>
|
||||
this._tree.has(treeKey(group, child))
|
||||
).length;
|
||||
if (selected === 0) {
|
||||
return "none";
|
||||
}
|
||||
return selected === group.children.length ? "all" : "some";
|
||||
}
|
||||
|
||||
private _onTreeToggle = (ev: CustomEvent<number>) => {
|
||||
const row = treeRows[ev.detail];
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
const next = new Set(this._tree);
|
||||
if (row.child) {
|
||||
const key = treeKey(row.group, row.child);
|
||||
if (next.has(key)) {
|
||||
next.delete(key);
|
||||
} else {
|
||||
next.add(key);
|
||||
}
|
||||
} else {
|
||||
const select = this._groupState(row.group) !== "all";
|
||||
row.group.children.forEach((child) => {
|
||||
const key = treeKey(row.group, child);
|
||||
if (select) {
|
||||
next.add(key);
|
||||
} else {
|
||||
next.delete(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
this._tree = next;
|
||||
};
|
||||
|
||||
private _onSingle = (ev: CustomEvent<number>) => {
|
||||
this._single = ev.detail;
|
||||
};
|
||||
@@ -443,6 +565,9 @@ selected: ${JSON.stringify(this._toJson(this._multiCheckEnd))}</pre>
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
}
|
||||
.child::part(base) {
|
||||
padding-inline-start: var(--ha-space-12);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
+5
-3
@@ -42,14 +42,14 @@
|
||||
"@babel/runtime": "8.0.0",
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@codemirror/autocomplete": "6.20.3",
|
||||
"@codemirror/commands": "6.10.4",
|
||||
"@codemirror/commands": "6.11.0",
|
||||
"@codemirror/lang-jinja": "6.0.1",
|
||||
"@codemirror/lang-yaml": "6.1.3",
|
||||
"@codemirror/language": "6.12.4",
|
||||
"@codemirror/lint": "6.9.7",
|
||||
"@codemirror/search": "6.7.1",
|
||||
"@codemirror/state": "6.7.1",
|
||||
"@codemirror/view": "6.43.8",
|
||||
"@codemirror/view": "6.43.9",
|
||||
"@date-fns/tz": "1.5.0",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.6.0",
|
||||
@@ -145,7 +145,7 @@
|
||||
"@bundle-stats/plugin-webpack-filter": "4.22.2",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@gfx/zopfli": "1.0.15",
|
||||
"@html-eslint/eslint-plugin": "0.64.0",
|
||||
"@html-eslint/eslint-plugin": "0.65.0",
|
||||
"@lokalise/node-api": "16.3.0",
|
||||
"@octokit/auth-oauth-device": "8.0.4",
|
||||
"@octokit/plugin-retry": "8.1.1",
|
||||
@@ -172,6 +172,7 @@
|
||||
"@vitest/coverage-v8": "4.1.10",
|
||||
"babel-loader": "10.1.1",
|
||||
"babel-plugin-polyfill-corejs3": "1.0.0",
|
||||
"browserslist": "4.28.8",
|
||||
"browserslist-useragent-regexp": "4.1.4",
|
||||
"del": "8.0.1",
|
||||
"eslint": "10.8.1",
|
||||
@@ -195,6 +196,7 @@
|
||||
"jsdom": "30.0.1",
|
||||
"jszip": "3.10.1",
|
||||
"license-checker-rseidelsohn": "5.0.1",
|
||||
"lightningcss": "1.33.0",
|
||||
"lint-staged": "17.3.0",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
|
||||
@@ -12,6 +12,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { transform } from "../common/decorators/transform";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import {
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
import {
|
||||
configContext,
|
||||
connectionContext,
|
||||
internationalizationContext,
|
||||
statesContext,
|
||||
} from "../data/context";
|
||||
import { ConversationEntityFeature } from "../data/conversation";
|
||||
@@ -33,8 +35,13 @@ import type {
|
||||
HomeAssistant,
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantConnection,
|
||||
HomeAssistantInternationalization,
|
||||
} from "../types";
|
||||
import { AudioRecorder } from "../util/audio-recorder";
|
||||
import {
|
||||
findAvailableLanguage,
|
||||
getTranslation,
|
||||
} from "../util/common-translation";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import "./ha-alert";
|
||||
import "./ha-markdown";
|
||||
@@ -67,6 +74,17 @@ export const assistPipelineChanged = (
|
||||
current: AssistPipeline | undefined
|
||||
): boolean => previous?.id !== current?.id;
|
||||
|
||||
export const greetingTranslationLanguage = (
|
||||
pipelineLanguage: string | undefined,
|
||||
interfaceLanguage: string | undefined
|
||||
): string | undefined => {
|
||||
if (!pipelineLanguage || pipelineLanguage === interfaceLanguage) {
|
||||
return undefined;
|
||||
}
|
||||
const language = findAvailableLanguage(pipelineLanguage);
|
||||
return language && language !== interfaceLanguage ? language : undefined;
|
||||
};
|
||||
|
||||
@customElement("ha-assist-chat")
|
||||
export class HaAssistChat extends LitElement {
|
||||
@property({ attribute: false }) public pipeline?: AssistPipeline;
|
||||
@@ -101,6 +119,13 @@ export class HaAssistChat extends LitElement {
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@transform<HomeAssistantInternationalization, string>({
|
||||
transformer: ({ language }) => language,
|
||||
})
|
||||
private _language!: string;
|
||||
|
||||
@state()
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states!: HomeAssistant["states"];
|
||||
@@ -115,6 +140,8 @@ export class HaAssistChat extends LitElement {
|
||||
|
||||
private _conversationId: string | null = null;
|
||||
|
||||
private _greetingLoadToken = 0;
|
||||
|
||||
private _initialPromptSubmitted = false;
|
||||
|
||||
private _audioRecorder?: AudioRecorder;
|
||||
@@ -131,17 +158,44 @@ export class HaAssistChat extends LitElement {
|
||||
(changedProperties.has("pipeline") &&
|
||||
assistPipelineChanged(changedProperties.get("pipeline"), this.pipeline))
|
||||
) {
|
||||
this._conversation = [
|
||||
{
|
||||
who: "hass",
|
||||
text: this._localize("ui.dialogs.voice_command.how_can_i_help"),
|
||||
thinking: "",
|
||||
tool_calls: {},
|
||||
},
|
||||
];
|
||||
this._conversation = [];
|
||||
this._loadGreeting();
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadGreeting(): Promise<void> {
|
||||
const token = ++this._greetingLoadToken;
|
||||
const language = greetingTranslationLanguage(
|
||||
this.pipeline?.language,
|
||||
this._language
|
||||
);
|
||||
let greeting: string | undefined;
|
||||
if (language) {
|
||||
try {
|
||||
const result = await getTranslation(null, language, false);
|
||||
if (result.language === language) {
|
||||
greeting = result.data["ui.dialogs.voice_command.how_can_i_help"];
|
||||
}
|
||||
} catch (_err) {
|
||||
// Translation failed to load; fall back to the interface language.
|
||||
}
|
||||
}
|
||||
if (token !== this._greetingLoadToken) {
|
||||
// The pipeline changed while loading; a newer load owns the greeting.
|
||||
return;
|
||||
}
|
||||
this._conversation = [
|
||||
{
|
||||
who: "hass",
|
||||
text:
|
||||
greeting || this._localize("ui.dialogs.voice_command.how_can_i_help"),
|
||||
thinking: "",
|
||||
tool_calls: {},
|
||||
},
|
||||
...this._conversation,
|
||||
];
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (
|
||||
@@ -157,7 +211,7 @@ export class HaAssistChat extends LitElement {
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("_conversation")) {
|
||||
if (changedProps.has("_conversation") && this._conversation.length) {
|
||||
this._scrollMessagesBottom();
|
||||
}
|
||||
if (
|
||||
|
||||
@@ -29,7 +29,7 @@ export type Appearance = "accent" | "filled" | "outlined" | "plain";
|
||||
*
|
||||
* @attr {("xs"|"s"|"m"|"l"|"xl")} size - Sets the button size.
|
||||
* @attr {("brand"|"neutral"|"danger"|"warning"|"success")} variant - Sets the button color variant. "primary" is default.
|
||||
* @attr {("accent"|"filled"|"plain")} appearance - Sets the button appearance.
|
||||
* @attr {("accent"|"filled"|"outlined"|"plain")} appearance - Sets the button appearance.
|
||||
* @attr {boolean} loading - shows a loading indicator instead of the buttons label and disable buttons click.
|
||||
* @attr {boolean} disabled - Disables the button and prevents user interaction.
|
||||
*/
|
||||
@@ -199,6 +199,7 @@ export class HaButton extends Button {
|
||||
:host([appearance~="outlined"]) .button.disabled {
|
||||
background-color: transparent;
|
||||
color: var(--ha-color-on-disabled-quiet);
|
||||
border-color: var(--ha-color-on-disabled-quiet);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
|
||||
@@ -24,6 +24,7 @@ export type HaListItemOptionSelectionPosition = "start" | "end";
|
||||
* @cssprop --ha-list-item-selected-background - Background color when selected (`appearance="line"`).
|
||||
*
|
||||
* @attr {boolean} selected - Whether the option is selected. Set by the parent `ha-list-selectable`.
|
||||
* @attr {boolean} indeterminate - Draws the checkbox in an indeterminate state, for a row that stands for a partially selected set.
|
||||
* @attr {string} value - Value identifying the option.
|
||||
* @attr {("line"|"checkbox")} appearance - Visual style. "line" highlights the row; "checkbox" renders an `ha-checkbox`.
|
||||
* @attr {("start"|"end")} selection-position - Side the checkbox sits on when `appearance="checkbox"`.
|
||||
@@ -32,6 +33,8 @@ export type HaListItemOptionSelectionPosition = "start" | "end";
|
||||
export class HaListItemOption extends HaListItemBase {
|
||||
@property({ type: Boolean, reflect: true }) public selected = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public indeterminate = false;
|
||||
|
||||
@property({ type: String }) public value?: string;
|
||||
|
||||
@property({ type: String, reflect: true })
|
||||
@@ -80,6 +83,7 @@ export class HaListItemOption extends HaListItemBase {
|
||||
return html`<div part="checkbox" class="checkbox" inert>
|
||||
<ha-checkbox
|
||||
.checked=${this.selected}
|
||||
.indeterminate=${this.indeterminate}
|
||||
.disabled=${this.disabled}
|
||||
></ha-checkbox>
|
||||
</div>`;
|
||||
|
||||
@@ -10,6 +10,8 @@ export const SelectableMixin = <T extends Constructor<HaListBase>>(
|
||||
class SelectableClass extends superClass {
|
||||
@property({ type: Boolean, reflect: true }) public multi = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public controlled = false;
|
||||
|
||||
protected override readonly hostRole = "listbox";
|
||||
|
||||
public connectedCallback(): void {
|
||||
@@ -67,15 +69,19 @@ export const SelectableMixin = <T extends Constructor<HaListBase>>(
|
||||
`ha-list-item-${el.selected ? "deselected" : "selected"}`,
|
||||
index
|
||||
);
|
||||
el.toggleAttribute("selected");
|
||||
if (!this.controlled) {
|
||||
el.toggleAttribute("selected");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!el.selected) {
|
||||
fireEvent(this, "ha-list-item-selected", index);
|
||||
// deselect the other optional selected item
|
||||
this.clearSelection();
|
||||
el.toggleAttribute("selected", true);
|
||||
if (!this.controlled) {
|
||||
// deselect the other optional selected item
|
||||
this.clearSelection();
|
||||
el.toggleAttribute("selected", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ import { HaListVirtualized } from "./ha-list-virtualized";
|
||||
*
|
||||
* @attr {boolean} multi - Whether multiple options can be selected at once. In
|
||||
* single-select mode, selecting a row clears any previous selection.
|
||||
* @attr {boolean} controlled - Only notify on click, never toggle a row's `selected` state.
|
||||
* Set it when the consumer derives `selected` from its own state, for example when a row stands
|
||||
* for a group whose click also changes its children.
|
||||
*
|
||||
* @fires ha-list-item-selected - Fires when the user selects a row.
|
||||
* `detail` is the row's index (number).
|
||||
|
||||
@@ -11,6 +11,9 @@ import { SelectableMixin } from "./ha-list-selectable-mixin";
|
||||
* Toggle single vs multi selection via the `multi` attribute.
|
||||
*
|
||||
* @attr {boolean} multi - Whether multiple options can be selected at once.
|
||||
* @attr {boolean} controlled - Only notify on click, never toggle an option's `selected` state.
|
||||
* Set it when the consumer derives `selected` from its own state, for example when a row stands
|
||||
* for a group whose click also changes its children.
|
||||
*
|
||||
* @fires ha-list-item-selected - An option was selected. `detail: number` (option index).
|
||||
* @fires ha-list-item-deselected - An option was deselected (multi mode only). `detail: number` (option index).
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import type { HaFormSchema } from "../components/ha-form/types";
|
||||
import type { ConfigEntry } from "./config_entries";
|
||||
import type { RepairsIssue } from "./repairs";
|
||||
|
||||
export type FlowType =
|
||||
"config_flow" | "config_subentries_flow" | "options_flow" | "repair_flow";
|
||||
@@ -56,26 +57,31 @@ export interface DataEntryFlowStepExternal {
|
||||
translation_domain?: string;
|
||||
}
|
||||
|
||||
export interface DataEntryFlowStepCreateEntry {
|
||||
export interface DataEntryFlowStepCreateEntry<
|
||||
TResult extends ConfigEntry | RepairsIssue = ConfigEntry,
|
||||
> {
|
||||
type: "create_entry";
|
||||
version: number;
|
||||
flow_id: string;
|
||||
next_flow?: [FlowType, string]; // [flow_type, flow_id]
|
||||
handler: string;
|
||||
title: string;
|
||||
result?: ConfigEntry;
|
||||
result?: TResult;
|
||||
description: string;
|
||||
description_placeholders?: Record<string, string>;
|
||||
translation_domain?: string;
|
||||
}
|
||||
|
||||
export interface DataEntryFlowStepAbort {
|
||||
export interface DataEntryFlowStepAbort<
|
||||
TResult extends ConfigEntry | RepairsIssue = ConfigEntry,
|
||||
> {
|
||||
type: "abort";
|
||||
flow_id: string;
|
||||
handler: string;
|
||||
reason: string;
|
||||
description_placeholders?: Record<string, string>;
|
||||
translation_domain?: string;
|
||||
result?: TResult;
|
||||
next_flow?: [FlowType, string]; // [flow_type, flow_id]
|
||||
}
|
||||
|
||||
|
||||
+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 } }),
|
||||
});
|
||||
|
||||
@@ -12,12 +12,14 @@ import "../../components/ha-button";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import "../../components/ha-icon-button";
|
||||
import type { ConfigEntry } from "../../data/config_entries";
|
||||
import type { DataEntryFlowStep } from "../../data/data_entry_flow";
|
||||
import {
|
||||
subscribeDataEntryFlowProgress,
|
||||
subscribeDataEntryFlowProgressed,
|
||||
} from "../../data/data_entry_flow";
|
||||
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
|
||||
import type { RepairsIssue } from "../../data/repairs";
|
||||
import { DirtyStateProviderMixin } from "../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -30,6 +32,7 @@ import type {
|
||||
} from "./show-dialog-data-entry-flow";
|
||||
import { showOptionsFlowDialog } from "./show-dialog-options-flow";
|
||||
import { showSubConfigFlowDialog } from "./show-dialog-sub-config-flow";
|
||||
import { showRepairsFlowDialog } from "../repairs-flow/show-dialog-repair-flow";
|
||||
import "./step-flow-abort";
|
||||
import "./step-flow-create-entry";
|
||||
import "./step-flow-external";
|
||||
@@ -218,7 +221,9 @@ class DataEntryFlowDialog extends DirtyStateProviderMixin<
|
||||
this._params.dialogClosedCallback({
|
||||
flowFinished,
|
||||
entryId:
|
||||
"result" in this._step ? this._step.result?.entry_id : undefined,
|
||||
"result" in this._step
|
||||
? (this._step.result as ConfigEntry)?.entry_id
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -279,7 +284,7 @@ class DataEntryFlowDialog extends DirtyStateProviderMixin<
|
||||
const devicesLength = this._devices(
|
||||
this._params.flowConfig.showDevices,
|
||||
Object.values(this.hass.devices),
|
||||
this._step.result?.entry_id,
|
||||
(this._step.result as ConfigEntry)?.entry_id,
|
||||
this._params.carryOverDevices
|
||||
).length;
|
||||
return this.hass.localize(
|
||||
@@ -485,7 +490,8 @@ class DataEntryFlowDialog extends DirtyStateProviderMixin<
|
||||
.devices=${this._devices(
|
||||
this._params.flowConfig.showDevices,
|
||||
Object.values(this.hass.devices),
|
||||
this._step.result?.entry_id,
|
||||
(this._step.result as ConfigEntry)
|
||||
?.entry_id,
|
||||
this._params.carryOverDevices
|
||||
)}
|
||||
></step-flow-create-entry>
|
||||
@@ -575,7 +581,7 @@ class DataEntryFlowDialog extends DirtyStateProviderMixin<
|
||||
const devices = this._devices(
|
||||
this._params!.flowConfig.showDevices,
|
||||
Object.values(this.hass.devices),
|
||||
this._step.result?.entry_id,
|
||||
(this._step.result as ConfigEntry)?.entry_id,
|
||||
this._params!.carryOverDevices
|
||||
);
|
||||
|
||||
@@ -681,21 +687,23 @@ class DataEntryFlowDialog extends DirtyStateProviderMixin<
|
||||
dialogClosedCallback: this._params!.dialogClosedCallback,
|
||||
});
|
||||
} else if (_step.next_flow[0] === "options_flow") {
|
||||
if (_step.type === "create_entry") {
|
||||
showOptionsFlowDialog(this, _step.result!, {
|
||||
continueFlowId: _step.next_flow[1],
|
||||
navigateToResult: this._params!.navigateToResult,
|
||||
dialogClosedCallback: this._params!.dialogClosedCallback,
|
||||
});
|
||||
}
|
||||
showOptionsFlowDialog(this, _step.result!, {
|
||||
continueFlowId: _step.next_flow[1],
|
||||
navigateToResult: this._params!.navigateToResult,
|
||||
dialogClosedCallback: this._params!.dialogClosedCallback,
|
||||
});
|
||||
} else if (_step.next_flow[0] === "config_subentries_flow") {
|
||||
if (_step.type === "create_entry") {
|
||||
showSubConfigFlowDialog(this, _step.result!, "", {
|
||||
continueFlowId: _step.next_flow[1],
|
||||
navigateToResult: this._params!.navigateToResult,
|
||||
dialogClosedCallback: this._params!.dialogClosedCallback,
|
||||
});
|
||||
}
|
||||
showSubConfigFlowDialog(this, _step.result!, "", {
|
||||
continueFlowId: _step.next_flow[1],
|
||||
navigateToResult: this._params!.navigateToResult,
|
||||
dialogClosedCallback: this._params!.dialogClosedCallback,
|
||||
});
|
||||
} else if (_step.next_flow[0] === "repair_flow") {
|
||||
showRepairsFlowDialog(this, _step.result as unknown as RepairsIssue, {
|
||||
continueFlowId: _step.next_flow[1],
|
||||
navigateToResult: this._params!.navigateToResult,
|
||||
dialogClosedCallback: this._params!.dialogClosedCallback,
|
||||
});
|
||||
} else {
|
||||
this.closeDialog();
|
||||
showAlertDialog(this, {
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { HaInput } from "../../components/input/ha-input";
|
||||
import { assistSatelliteSupportsSetupFlow } from "../../data/assist_satellite";
|
||||
import { getConfigEntries } from "../../data/config_entries";
|
||||
import type { DataEntryFlowStepCreateEntry } from "../../data/data_entry_flow";
|
||||
import type { ConfigEntry } from "../../data/config_entries";
|
||||
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
|
||||
import { updateDeviceRegistryEntry } from "../../data/device/device_registry";
|
||||
import {
|
||||
@@ -40,7 +41,8 @@ class StepFlowCreateEntry extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry;
|
||||
@property({ attribute: false })
|
||||
public step!: DataEntryFlowStepCreateEntry<ConfigEntry>;
|
||||
|
||||
@property({ attribute: false }) public devices!: DeviceRegistryEntry[];
|
||||
|
||||
@@ -79,7 +81,7 @@ class StepFlowCreateEntry extends LitElement {
|
||||
if (
|
||||
this.devices.length !== 1 ||
|
||||
this.devices[0].primary_config_entry !== this.step.result?.entry_id ||
|
||||
this.step.result.domain === "voip"
|
||||
this.step.result?.domain === "voip"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import type { RepairsIssue } from "../../../data/repairs";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import type { RepairsIssue } from "../../data/repairs";
|
||||
|
||||
@customElement("dialog-repairs-issue-subtitle")
|
||||
class DialogRepairsIssueSubtitle extends LitElement {
|
||||
+9
-8
@@ -1,18 +1,19 @@
|
||||
import { html, nothing } from "lit";
|
||||
import type { DataEntryFlowStep } from "../../../data/data_entry_flow";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import type { RepairsIssue } from "../../../data/repairs";
|
||||
import type { DataEntryFlowStep } from "../../data/data_entry_flow";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import type { RepairsIssue } from "../../data/repairs";
|
||||
import {
|
||||
createRepairsFlow,
|
||||
deleteRepairsFlow,
|
||||
fetchRepairsFlow,
|
||||
handleRepairsFlowStep,
|
||||
} from "../../../data/repairs";
|
||||
} from "../../data/repairs";
|
||||
import type { DataEntryFlowDialogParams } from "../config-flow/show-dialog-data-entry-flow";
|
||||
import {
|
||||
loadDataEntryFlowDialog,
|
||||
showFlowDialog,
|
||||
} from "../../../dialogs/config-flow/show-dialog-data-entry-flow";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
} from "../config-flow/show-dialog-data-entry-flow";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./dialog-repairs-issue-subtitle";
|
||||
|
||||
const mergePlaceholders = (issue: RepairsIssue, step: DataEntryFlowStep) =>
|
||||
@@ -36,14 +37,14 @@ export const loadRepairFlowDialog = loadDataEntryFlowDialog;
|
||||
export const showRepairsFlowDialog = (
|
||||
element: HTMLElement,
|
||||
issue: RepairsIssue,
|
||||
dialogClosedCallback?: (params: { flowFinished: boolean }) => void
|
||||
dialogParams?: Omit<DataEntryFlowDialogParams, "flowConfig">
|
||||
): void =>
|
||||
showFlowDialog(
|
||||
element,
|
||||
{
|
||||
startFlowHandler: issue.domain,
|
||||
domain: issue.domain,
|
||||
dialogClosedCallback,
|
||||
...dialogParams,
|
||||
},
|
||||
{
|
||||
flowType: "repair_flow",
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1393,7 +1393,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
invert: this._switchAsInvert,
|
||||
target_domain: this._switchAsDomain,
|
||||
}
|
||||
)) as DataEntryFlowStepCreateEntry;
|
||||
)) as DataEntryFlowStepCreateEntry<ConfigEntry>;
|
||||
if (configFlowResult.result?.entry_id) {
|
||||
try {
|
||||
const entry = await this._waitForEntityRegistryUpdate(
|
||||
@@ -1459,7 +1459,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
invert: this._switchAsInvert,
|
||||
target_domain: this._switchAsDomain,
|
||||
}
|
||||
)) as DataEntryFlowStepCreateEntry;
|
||||
)) as DataEntryFlowStepCreateEntry<ConfigEntry>;
|
||||
if (configFlowResult.result?.entry_id) {
|
||||
try {
|
||||
const entry = await this._waitForEntityRegistryUpdate(
|
||||
|
||||
@@ -13,7 +13,7 @@ import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import "./dialog-repairs-issue-subtitle";
|
||||
import "../../../dialogs/repairs-flow/dialog-repairs-issue-subtitle";
|
||||
import "../../../components/ha-markdown";
|
||||
import type { RepairsIssue } from "../../../data/repairs";
|
||||
import { ignoreRepairsIssue } from "../../../data/repairs";
|
||||
|
||||
@@ -20,7 +20,7 @@ import type { HomeAssistant } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import { fixStatisticsIssue } from "../tools/statistics/fix-statistics";
|
||||
import { showVacuumSegmentMappingDialog } from "../entities/dialogs/show-dialog-vacuum-segment-mapping";
|
||||
import { showRepairsFlowDialog } from "./show-dialog-repair-flow";
|
||||
import { showRepairsFlowDialog } from "../../../dialogs/repairs-flow/show-dialog-repair-flow";
|
||||
import { showRepairsIssueDialog } from "./show-repair-issue-dialog";
|
||||
|
||||
@customElement("ha-config-repairs")
|
||||
|
||||
@@ -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,43 @@ 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;
|
||||
display: flex;
|
||||
align-self: center;
|
||||
--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;
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ export const waColorStyles = css`
|
||||
--wa-color-brand-fill-loud: var(--ha-color-fill-primary-loud-resting);
|
||||
--wa-color-brand-fill-normal: var(--ha-color-fill-primary-normal-resting);
|
||||
--wa-color-brand-fill-quiet: var(--ha-color-fill-primary-quiet-hover);
|
||||
--wa-color-brand-border-loud: var(--ha-color-border-loud);
|
||||
--wa-color-brand-border-loud: var(--ha-color-border-primary-loud);
|
||||
--wa-color-brand-border-normal: var(--ha-color-primary-50);
|
||||
--wa-color-brand-border-quiet: var(--ha-color-border-quiet);
|
||||
--wa-color-brand-border-quiet: var(--ha-color-border-primary-quiet);
|
||||
--wa-color-brand-on-loud: var(--ha-color-on-primary-loud);
|
||||
--wa-color-brand-on-normal: var(--ha-color-on-primary-normal);
|
||||
--wa-color-brand-on-quiet: var(--ha-color-on-primary-quiet);
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { AssistPipeline } from "../../src/data/assist_pipeline";
|
||||
import {
|
||||
assistPipelineChanged,
|
||||
greetingTranslationLanguage,
|
||||
initialPromptToSubmit,
|
||||
} from "../../src/components/ha-assist-chat";
|
||||
|
||||
// common-translation depends on build-time defines and generated translation
|
||||
// metadata that are not available in unit tests.
|
||||
vi.mock("../../src/util/common-translation", () => ({
|
||||
findAvailableLanguage: (language: string) =>
|
||||
({ en: "en", "en-US": "en", nl: "nl", pl: "pl" })[language],
|
||||
getTranslation: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("initialPromptToSubmit", () => {
|
||||
it("returns a trimmed prompt when submission is requested", () => {
|
||||
expect(initialPromptToSubmit(" Turn on the lights ", true)).toBe(
|
||||
@@ -36,3 +45,25 @@ describe("ha-assist-chat pipeline updates", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("greetingTranslationLanguage", () => {
|
||||
it("returns the pipeline language when it differs from the interface language", () => {
|
||||
expect(greetingTranslationLanguage("pl", "en")).toBe("pl");
|
||||
});
|
||||
|
||||
it("returns undefined when the pipeline language matches the interface language", () => {
|
||||
expect(greetingTranslationLanguage("nl", "nl")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when the pipeline language resolves to the interface language", () => {
|
||||
expect(greetingTranslationLanguage("en-US", "en")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when there is no pipeline language", () => {
|
||||
expect(greetingTranslationLanguage(undefined, "en")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when the pipeline language has no available translation", () => {
|
||||
expect(greetingTranslationLanguage("xx", "en")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -2306,15 +2306,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@codemirror/commands@npm:6.10.4":
|
||||
version: 6.10.4
|
||||
resolution: "@codemirror/commands@npm:6.10.4"
|
||||
"@codemirror/commands@npm:6.11.0":
|
||||
version: 6.11.0
|
||||
resolution: "@codemirror/commands@npm:6.11.0"
|
||||
dependencies:
|
||||
"@codemirror/language": "npm:^6.0.0"
|
||||
"@codemirror/state": "npm:^6.7.0"
|
||||
"@codemirror/view": "npm:^6.27.0"
|
||||
"@lezer/common": "npm:^1.1.0"
|
||||
checksum: 10/384808f8767967f7a2bf4007cb57e2b137e98fbeb6bb7239502b79ad06fb8039b67b43ab2f5eaf955a2569c65203c7a0e59ad758e7d90ba727936f9b2a77e47b
|
||||
checksum: 10/a503625e6b5baeb748f391ff9fc83d875d65a0be303f4eefa84b2c128598ce1b792057bf0102276786afdc9e135d2da54074fc7138b5bc2a9be90e3054c6a271
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -2439,15 +2439,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@codemirror/view@npm:6.43.8, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0, @codemirror/view@npm:^6.37.0, @codemirror/view@npm:^6.42.0":
|
||||
version: 6.43.8
|
||||
resolution: "@codemirror/view@npm:6.43.8"
|
||||
"@codemirror/view@npm:6.43.9, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0, @codemirror/view@npm:^6.37.0, @codemirror/view@npm:^6.42.0":
|
||||
version: 6.43.9
|
||||
resolution: "@codemirror/view@npm:6.43.9"
|
||||
dependencies:
|
||||
"@codemirror/state": "npm:^6.7.0"
|
||||
crelt: "npm:^1.0.6"
|
||||
style-mod: "npm:^4.1.0"
|
||||
w3c-keyname: "npm:^2.2.4"
|
||||
checksum: 10/88cd5543b09c4b945862ff128aeea48e259a582e1106fbc22370ddf773e629810b43baaf3abdf743150098e07c601ca481186cdf46b537f0312452fa2e7e8f51
|
||||
checksum: 10/b653dddf81278311985a8f7723f6b0ac50f4ff019725dc4020c40c4d4707f209067e6edbd1f022cb40176d8a0b327c72d3234d2a5c815be689ca2cc1272466d6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -2969,73 +2969,73 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@html-eslint/core@npm:^0.64.0":
|
||||
version: 0.64.0
|
||||
resolution: "@html-eslint/core@npm:0.64.0"
|
||||
"@html-eslint/core@npm:^0.65.0":
|
||||
version: 0.65.0
|
||||
resolution: "@html-eslint/core@npm:0.65.0"
|
||||
dependencies:
|
||||
"@html-eslint/types": "npm:^0.64.0"
|
||||
"@html-eslint/types": "npm:^0.65.0"
|
||||
html-standard: "npm:^0.0.13"
|
||||
checksum: 10/682d735353acd711ee74e681b5949a35f650e425be670509e1ddca5beeb708347b9c3a740b88eb13d19f6cbf54a903e4dcc173988f6c1c6c9fd212b997df6f17
|
||||
checksum: 10/a41d60108adaf874d5911a0082054df7ec9e23fcec43ff8d304e070f8e995f56350785d3a918256abdf3e25b0ec5dd7b565981b03b7326c6c83a98c3258a4700
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@html-eslint/eslint-plugin@npm:0.64.0":
|
||||
version: 0.64.0
|
||||
resolution: "@html-eslint/eslint-plugin@npm:0.64.0"
|
||||
"@html-eslint/eslint-plugin@npm:0.65.0":
|
||||
version: 0.65.0
|
||||
resolution: "@html-eslint/eslint-plugin@npm:0.65.0"
|
||||
dependencies:
|
||||
"@eslint/plugin-kit": "npm:^0.4.1"
|
||||
"@html-eslint/core": "npm:^0.64.0"
|
||||
"@html-eslint/parser": "npm:^0.64.0"
|
||||
"@html-eslint/template-parser": "npm:^0.64.0"
|
||||
"@html-eslint/template-syntax-parser": "npm:^0.64.0"
|
||||
"@html-eslint/types": "npm:^0.64.0"
|
||||
"@html-eslint/core": "npm:^0.65.0"
|
||||
"@html-eslint/parser": "npm:^0.65.0"
|
||||
"@html-eslint/template-parser": "npm:^0.65.0"
|
||||
"@html-eslint/template-syntax-parser": "npm:^0.65.0"
|
||||
"@html-eslint/types": "npm:^0.65.0"
|
||||
"@rviscomi/capo.js": "npm:^2.1.0"
|
||||
html-standard: "npm:^0.0.13"
|
||||
peerDependencies:
|
||||
eslint: ">=8.0.0 || ^10.0.0-0"
|
||||
checksum: 10/3badf5dab3fd62be5f28ca5703004271224a684273ba419cadd7712b47fd57724f6b5ec5e3081e47c3e700fe6ae4066cd71bbde0041d2cb70b3e784a11949eec
|
||||
checksum: 10/d82ed3be3871044cd1ffc2067bf5a8e81a62fe3c26aff767feb84a89b903a2dada55133feb957326aa0072b4573a4d5c737e5bda87f337542ac0e059e0387a10
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@html-eslint/parser@npm:^0.64.0":
|
||||
version: 0.64.0
|
||||
resolution: "@html-eslint/parser@npm:0.64.0"
|
||||
"@html-eslint/parser@npm:^0.65.0":
|
||||
version: 0.65.0
|
||||
resolution: "@html-eslint/parser@npm:0.65.0"
|
||||
dependencies:
|
||||
"@html-eslint/template-syntax-parser": "npm:^0.64.0"
|
||||
"@html-eslint/types": "npm:^0.64.0"
|
||||
"@html-eslint/template-syntax-parser": "npm:^0.65.0"
|
||||
"@html-eslint/types": "npm:^0.65.0"
|
||||
css-tree: "npm:^3.1.0"
|
||||
es-html-parser: "npm:0.3.1"
|
||||
checksum: 10/7ce0205a07cbddf63959ad2bcf39cf8c4252c5a88abce20326c5b02fd13cd0d81ec6ebe569c4c57b4f9a27842ae62592ad5e47bca58d00cb91d413ad1dad98d0
|
||||
checksum: 10/3cbbd5a3ddc47834324b22119860db3fbc1e0a104baf183ef0aae102bfcd0e9fc48520a16cefe6b67eb2b23524b7720a5729018273862ed7812fd2ac5d0d3a62
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@html-eslint/template-parser@npm:^0.64.0":
|
||||
version: 0.64.0
|
||||
resolution: "@html-eslint/template-parser@npm:0.64.0"
|
||||
"@html-eslint/template-parser@npm:^0.65.0":
|
||||
version: 0.65.0
|
||||
resolution: "@html-eslint/template-parser@npm:0.65.0"
|
||||
dependencies:
|
||||
"@html-eslint/types": "npm:^0.64.0"
|
||||
"@html-eslint/types": "npm:^0.65.0"
|
||||
es-html-parser: "npm:0.3.1"
|
||||
checksum: 10/2dcdc87c2355af6f45814217a812d54acbab7eca6edb6c0cf7ef0fbafca5a7c75c03b6f4acb2dd4f843abb2e34dae2720cd623f0ec0ccfd4570961b025f461a1
|
||||
checksum: 10/ae5f8c50bd006459974a62806a28e5c0efee38ec6dfd529613792545cc2659ba7e97dbfb72c1940be77d3f378dc6a17d973a4e923bda71658bc1629ecd2c7199
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@html-eslint/template-syntax-parser@npm:^0.64.0":
|
||||
version: 0.64.0
|
||||
resolution: "@html-eslint/template-syntax-parser@npm:0.64.0"
|
||||
"@html-eslint/template-syntax-parser@npm:^0.65.0":
|
||||
version: 0.65.0
|
||||
resolution: "@html-eslint/template-syntax-parser@npm:0.65.0"
|
||||
dependencies:
|
||||
"@html-eslint/types": "npm:^0.64.0"
|
||||
checksum: 10/774d319f8485cbd20b351767c693e1e281d8e3b7d8c6bf01a2db407684c0656d8789535df0f2357d9bc0df2299023d2d1045a425b72dea1144d265340cfa9031
|
||||
"@html-eslint/types": "npm:^0.65.0"
|
||||
checksum: 10/bf633583d7c18d7c1d5d5bb7974d61a69ffb616936eb12072e2f5228d832548152423d8be48a0e4dce7b52e9909b6ebc7816b8de5be8eab414fabe31d1d6e146
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@html-eslint/types@npm:^0.64.0":
|
||||
version: 0.64.0
|
||||
resolution: "@html-eslint/types@npm:0.64.0"
|
||||
"@html-eslint/types@npm:^0.65.0":
|
||||
version: 0.65.0
|
||||
resolution: "@html-eslint/types@npm:0.65.0"
|
||||
dependencies:
|
||||
"@types/css-tree": "npm:^2.3.11"
|
||||
"@types/estree": "npm:^1.0.6"
|
||||
es-html-parser: "npm:0.3.1"
|
||||
checksum: 10/9813e675f06fb950911a0b51e113ff05b3eabe73e6396a3ac2afeeab557c7608d883d092f40fd77f843f362f6cbfefa3cd45df3e0135ce71b5f9dd281eef0caa
|
||||
checksum: 10/224f2e85eaa08181021acf73c5d915d58e9731085a785db084177c2b899a1e8e39c0e61d0ac28a277bf0f3d53f97ecab9657f2a998cfbc1e4f511981ebe64fb9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -7297,7 +7297,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"browserslist@npm:^4.24.0, browserslist@npm:^4.28.2, browserslist@npm:^4.28.7":
|
||||
"browserslist@npm:4.28.8, browserslist@npm:^4.24.0, browserslist@npm:^4.28.2, browserslist@npm:^4.28.7":
|
||||
version: 4.28.8
|
||||
resolution: "browserslist@npm:4.28.8"
|
||||
dependencies:
|
||||
@@ -9888,14 +9888,14 @@ __metadata:
|
||||
"@braintree/sanitize-url": "npm:7.1.2"
|
||||
"@bundle-stats/plugin-webpack-filter": "npm:4.22.2"
|
||||
"@codemirror/autocomplete": "npm:6.20.3"
|
||||
"@codemirror/commands": "npm:6.10.4"
|
||||
"@codemirror/commands": "npm:6.11.0"
|
||||
"@codemirror/lang-jinja": "npm:6.0.1"
|
||||
"@codemirror/lang-yaml": "npm:6.1.3"
|
||||
"@codemirror/language": "npm:6.12.4"
|
||||
"@codemirror/lint": "npm:6.9.7"
|
||||
"@codemirror/search": "npm:6.7.1"
|
||||
"@codemirror/state": "npm:6.7.1"
|
||||
"@codemirror/view": "npm:6.43.8"
|
||||
"@codemirror/view": "npm:6.43.9"
|
||||
"@date-fns/tz": "npm:1.5.0"
|
||||
"@egjs/hammerjs": "npm:2.0.17"
|
||||
"@eslint/js": "npm:10.0.1"
|
||||
@@ -9916,7 +9916,7 @@ __metadata:
|
||||
"@fullcalendar/timegrid": "npm:6.1.21"
|
||||
"@gfx/zopfli": "npm:1.0.15"
|
||||
"@home-assistant/webawesome": "npm:3.7.0-ha.0"
|
||||
"@html-eslint/eslint-plugin": "npm:0.64.0"
|
||||
"@html-eslint/eslint-plugin": "npm:0.65.0"
|
||||
"@lezer/highlight": "npm:1.2.3"
|
||||
"@lit-labs/motion": "npm:1.1.0"
|
||||
"@lit-labs/observers": "npm:2.1.0"
|
||||
@@ -9965,6 +9965,7 @@ __metadata:
|
||||
babel-loader: "npm:10.1.1"
|
||||
babel-plugin-polyfill-corejs3: "npm:1.0.0"
|
||||
barcode-detector: "npm:3.2.2"
|
||||
browserslist: "npm:4.28.8"
|
||||
browserslist-useragent-regexp: "npm:4.1.4"
|
||||
cally: "npm:0.9.2"
|
||||
color-name: "npm:2.1.1"
|
||||
@@ -10009,6 +10010,7 @@ __metadata:
|
||||
leaflet-draw: "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
|
||||
leaflet.markercluster: "npm:1.5.3"
|
||||
license-checker-rseidelsohn: "npm:5.0.1"
|
||||
lightningcss: "npm:1.33.0"
|
||||
lint-staged: "npm:17.3.0"
|
||||
lit: "npm:3.3.3"
|
||||
lit-analyzer: "npm:2.0.3"
|
||||
@@ -11345,7 +11347,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lightningcss@npm:^1.32.0, lightningcss@npm:^1.33.0":
|
||||
"lightningcss@npm:1.33.0, lightningcss@npm:^1.32.0, lightningcss@npm:^1.33.0":
|
||||
version: 1.33.0
|
||||
resolution: "lightningcss@npm:1.33.0"
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user