mirror of
https://github.com/home-assistant/frontend.git
synced 2026-07-17 16:46:44 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 88ef6fe7ad | |||
| bbd26ce5a9 | |||
| d3c23dd704 | |||
| 7d4e5dc03f | |||
| db22be5a30 | |||
| b896b20409 | |||
| cb3b528ba3 | |||
| 2ed8e0bd0d | |||
| adc3393422 | |||
| 06b9b08809 | |||
| ece84d42dc |
@@ -22,7 +22,7 @@ jobs:
|
||||
if: github.event_name != 'push' || github.ref_name != 'master'
|
||||
environment:
|
||||
name: Demo Development
|
||||
url: ${{ steps.deploy.outputs.netlify_url }}
|
||||
url: ${{ steps.deploy.outputs.unique_deploy_url }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
+3
-3
@@ -102,7 +102,7 @@
|
||||
"dialog-polyfill": "0.5.6",
|
||||
"echarts": "6.1.0",
|
||||
"element-internals-polyfill": "3.0.2",
|
||||
"fuse.js": "7.4.2",
|
||||
"fuse.js": "7.5.0",
|
||||
"gulp-zopfli-green": "7.0.0",
|
||||
"hls.js": "1.6.16",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
@@ -146,7 +146,7 @@
|
||||
"@bundle-stats/plugin-webpack-filter": "4.22.2",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@html-eslint/eslint-plugin": "0.64.0",
|
||||
"@lokalise/node-api": "16.0.0",
|
||||
"@lokalise/node-api": "16.1.0",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
@@ -211,7 +211,7 @@
|
||||
"terser-webpack-plugin": "5.6.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.63.0",
|
||||
"typescript-eslint": "8.64.0",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.10",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
|
||||
@@ -1,4 +1,53 @@
|
||||
const regexp =
|
||||
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||
// Unanchored regex fragments, shared as the single source of truth for both
|
||||
// the boolean validators below and the HTML `pattern` attribute (the browser
|
||||
// anchors a pattern as `^(?:…)$`).
|
||||
const IPV4 =
|
||||
"(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)";
|
||||
|
||||
export const isIPAddress = (input: string): boolean => regexp.test(input);
|
||||
const IPV6 =
|
||||
"(?:([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))";
|
||||
|
||||
// CIDR prefix lengths: 0-32 for IPv4, 0-128 for IPv6.
|
||||
const IPV4_PREFIX = "(?:3[0-2]|[12]?[0-9])";
|
||||
const IPV6_PREFIX = "(?:12[0-8]|1[01][0-9]|[1-9]?[0-9])";
|
||||
|
||||
// IPv4 or IPv6 address.
|
||||
export const IP_ADDRESS_PATTERN = `${IPV4}|${IPV6}`;
|
||||
|
||||
// IPv4/IPv6 address, optionally with a CIDR prefix (network).
|
||||
export const IP_ADDRESS_OR_NETWORK_PATTERN = `${IPV4}(?:/${IPV4_PREFIX})?|${IPV6}(?:/${IPV6_PREFIX})?`;
|
||||
|
||||
const anchored = (pattern: string): RegExp => new RegExp(`^(?:${pattern})$`);
|
||||
|
||||
const ipv4Regexp = anchored(IPV4);
|
||||
const ipv6Regexp = anchored(IPV6);
|
||||
|
||||
// IPv4 address, e.g. 192.168.1.10
|
||||
export const isIPAddress = (input: string): boolean => ipv4Regexp.test(input);
|
||||
|
||||
// IPv6 address, e.g. fe80::85d:e82c:9446:7995
|
||||
export const isIPv6Address = (input: string): boolean => ipv6Regexp.test(input);
|
||||
|
||||
// IPv4 or IPv6 address
|
||||
export const isIPAddressV4OrV6 = (input: string): boolean =>
|
||||
isIPAddress(input) || isIPv6Address(input);
|
||||
|
||||
// IP network in CIDR notation, e.g. 192.168.1.0/24 or fd00::/8
|
||||
export const isIPNetwork = (input: string): boolean => {
|
||||
const parts = input.split("/");
|
||||
if (parts.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
const [address, prefix] = parts;
|
||||
if (!/^\d{1,3}$/.test(prefix)) {
|
||||
return false;
|
||||
}
|
||||
const prefixLength = Number(prefix);
|
||||
if (isIPAddress(address)) {
|
||||
return prefixLength >= 0 && prefixLength <= 32;
|
||||
}
|
||||
if (isIPv6Address(address)) {
|
||||
return prefixLength >= 0 && prefixLength <= 128;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -73,6 +73,40 @@ interface ClockDatePartSectionData {
|
||||
items: PickerComboBoxItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out sections whose group already has a value in `value`, so only
|
||||
* one non-separator value per group can be selected in the picker. The whole
|
||||
* group of the item at `excludeIndex` (the one being edited) stays
|
||||
* selectable, even if that group has more than one value in `value` (e.g. a
|
||||
* pre-existing config authored outside the picker via YAML).
|
||||
* The separator group is always kept.
|
||||
*/
|
||||
export const getAvailableClockDatePartSections = (
|
||||
sections: ClockDatePartSectionData[],
|
||||
value: string[],
|
||||
excludeIndex?: number
|
||||
): ClockDatePartSectionData[] => {
|
||||
const editedItem = excludeIndex != null ? value[excludeIndex] : undefined;
|
||||
const editedSection = editedItem
|
||||
? getClockDatePartSection(editedItem as ClockCardDatePart)
|
||||
: undefined;
|
||||
|
||||
const usedSections = new Set<ClockDatePartSection>();
|
||||
|
||||
value.forEach((item) => {
|
||||
const section = getClockDatePartSection(item as ClockCardDatePart);
|
||||
|
||||
if (section !== "separator" && section !== editedSection) {
|
||||
usedSections.add(section);
|
||||
}
|
||||
});
|
||||
|
||||
return sections.filter(
|
||||
(sectionData) =>
|
||||
sectionData.id === "separator" || !usedSections.has(sectionData.id)
|
||||
);
|
||||
};
|
||||
|
||||
interface ClockDatePartValueItem {
|
||||
key: string;
|
||||
item: string;
|
||||
@@ -103,12 +137,17 @@ export class HaClockDateFormatPicker extends LitElement {
|
||||
|
||||
@query("ha-generic-picker", true) private _picker?: HaGenericPicker;
|
||||
|
||||
private _editIndex?: number;
|
||||
@state() private _editIndex?: number;
|
||||
|
||||
protected render() {
|
||||
const value = this._value;
|
||||
const valueItems = this._getValueItems(value);
|
||||
const sections = this._buildSections();
|
||||
const pickerSections = getAvailableClockDatePartSections(
|
||||
sections,
|
||||
value,
|
||||
this._editIndex
|
||||
);
|
||||
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
@@ -117,8 +156,8 @@ export class HaClockDateFormatPicker extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
.value=${this._getPickerValue()}
|
||||
.sections=${this._getSectionHeaders(sections)}
|
||||
.getItems=${this._getItems(sections)}
|
||||
.sections=${this._getSectionHeaders(pickerSections)}
|
||||
.getItems=${this._getItems(pickerSections)}
|
||||
@value-changed=${this._pickerValueChanged}
|
||||
>
|
||||
<div slot="field" class="container">
|
||||
|
||||
@@ -28,6 +28,10 @@ export class HaTextSelector extends LitElement {
|
||||
|
||||
@query("ha-input, ha-textarea") private _input?: HTMLInputElement;
|
||||
|
||||
@query("ha-input-multi") private _inputMulti?: {
|
||||
reportValidity: () => boolean;
|
||||
};
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
this._input?.focus();
|
||||
@@ -35,7 +39,7 @@ export class HaTextSelector extends LitElement {
|
||||
|
||||
public reportValidity(): boolean {
|
||||
if (this.selector.text?.multiple) {
|
||||
return true;
|
||||
return this._inputMulti?.reportValidity() ?? true;
|
||||
}
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
@@ -52,6 +56,8 @@ export class HaTextSelector extends LitElement {
|
||||
.inputPrefix=${this.selector.text?.prefix}
|
||||
.helper=${this.helper}
|
||||
.autocomplete=${this.selector.text?.autocomplete}
|
||||
.pattern=${this.selector.text?.pattern}
|
||||
.validationMessage=${this.selector.text?.validation_message}
|
||||
@value-changed=${this._handleChange}
|
||||
>
|
||||
</ha-input-multi>
|
||||
@@ -80,6 +86,9 @@ export class HaTextSelector extends LitElement {
|
||||
.hint=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.type=${this.selector.text?.type}
|
||||
.pattern=${this.selector.text?.pattern}
|
||||
.validationMessage=${this.selector.text?.validation_message}
|
||||
.autoValidate=${this.selector.text?.pattern !== undefined}
|
||||
@input=${this._handleChange}
|
||||
@change=${this._handleChange}
|
||||
.label=${this.label || ""}
|
||||
|
||||
@@ -2,7 +2,13 @@ import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiDeleteOutline, mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
query,
|
||||
queryAll,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { uid } from "../../common/util/uid";
|
||||
@@ -50,6 +56,13 @@ class HaInputMulti extends LitElement {
|
||||
|
||||
@property() public autocomplete?: string;
|
||||
|
||||
/** Regular expression each entry is validated against (HTML `pattern`). */
|
||||
@property() public pattern?: string;
|
||||
|
||||
/** Message shown on an entry when it fails `pattern` validation. */
|
||||
@property({ attribute: "validation-message" })
|
||||
public validationMessage?: string;
|
||||
|
||||
@property({ attribute: "add-label" }) public addLabel?: string;
|
||||
|
||||
@property({ attribute: "remove-label" }) public removeLabel?: string;
|
||||
@@ -70,6 +83,8 @@ class HaInputMulti extends LitElement {
|
||||
|
||||
@query("ha-input[data-last]") private _lastInput?: HaInput;
|
||||
|
||||
@queryAll("ha-input") private _inputs?: NodeListOf<HaInput>;
|
||||
|
||||
// Stable key per row, kept in sync with `value`. Because items are plain
|
||||
// strings we cannot use a WeakMap (as the object-based sortable lists do),
|
||||
// so we track keys in a parallel array. Keys stay fixed while a row is
|
||||
@@ -89,6 +104,16 @@ class HaInputMulti extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
let valid = true;
|
||||
this._inputs?.forEach((input) => {
|
||||
if (!input.reportValidity()) {
|
||||
valid = false;
|
||||
}
|
||||
});
|
||||
return valid;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-sortable
|
||||
@@ -109,6 +134,9 @@ class HaInputMulti extends LitElement {
|
||||
.type=${this.inputType}
|
||||
.autocomplete=${this.autocomplete}
|
||||
.disabled=${this.disabled}
|
||||
.pattern=${this.pattern}
|
||||
.validationMessage=${this.validationMessage}
|
||||
.autoValidate=${this.pattern !== undefined}
|
||||
dialogInitialFocus=${index}
|
||||
.index=${index}
|
||||
class="flex-auto"
|
||||
|
||||
@@ -140,7 +140,7 @@ export interface HassioAddonSetOptionParams {
|
||||
export const reloadHassioAddons = async (hass: HomeAssistant) => {
|
||||
await hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: "/addons/reload",
|
||||
endpoint: "/store/reload",
|
||||
method: "post",
|
||||
});
|
||||
};
|
||||
|
||||
+49
-5
@@ -15,16 +15,60 @@ export interface HttpConfig {
|
||||
ssl_profile?: "modern" | "intermediate";
|
||||
}
|
||||
|
||||
export interface HttpConfigState {
|
||||
stable: HttpConfig;
|
||||
pending: HttpConfig | null;
|
||||
revert_at: string | null;
|
||||
// The slot the running HTTP server was actually started with.
|
||||
export type ActiveConfigType = "stable" | "pending" | "default";
|
||||
|
||||
// A stored config slot carries metadata alongside the editable fields:
|
||||
// - created_at: when the slot was staged
|
||||
// - error: null while healthy; set once a slot could not be applied or a
|
||||
// pending trial was not confirmed (then it is kept for display, not retried)
|
||||
export interface HttpConfigWithMeta extends HttpConfig {
|
||||
created_at?: string;
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export interface HttpConfigState {
|
||||
stable: HttpConfigWithMeta;
|
||||
pending: HttpConfigWithMeta | null;
|
||||
revert_at: string | null;
|
||||
// Added in the "active HTTP config slot" backend change; optional so the
|
||||
// frontend keeps working against cores without it.
|
||||
active_config_type?: ActiveConfigType;
|
||||
default?: HttpConfigWithMeta;
|
||||
}
|
||||
|
||||
export const HTTP_CONFIG_FIELDS: (keyof HttpConfig)[] = [
|
||||
"server_port",
|
||||
"server_host",
|
||||
"ssl_certificate",
|
||||
"ssl_key",
|
||||
"ssl_peer_certificate",
|
||||
"ssl_profile",
|
||||
"cors_allowed_origins",
|
||||
"use_x_forwarded_for",
|
||||
"trusted_proxies",
|
||||
"use_x_frame_options",
|
||||
"ip_ban_enabled",
|
||||
"login_attempts_threshold",
|
||||
];
|
||||
|
||||
export interface SaveHttpConfigResult {
|
||||
restart: boolean;
|
||||
}
|
||||
|
||||
// Keep only the editable fields; the backend storage schema rejects unknown
|
||||
// keys, so the created_at/error metadata that rides along on a fetched slot
|
||||
// must be dropped before configuring.
|
||||
export const stripHttpConfigMeta = (config: HttpConfig): HttpConfig => {
|
||||
const stripped: Partial<Record<keyof HttpConfig, unknown>> = {};
|
||||
for (const key of HTTP_CONFIG_FIELDS) {
|
||||
if (config[key] !== undefined) {
|
||||
stripped[key] = config[key];
|
||||
}
|
||||
}
|
||||
return stripped as HttpConfig;
|
||||
};
|
||||
|
||||
export const fetchHttpConfig = (hass: HomeAssistant) =>
|
||||
hass.callWS<HttpConfigState>({ type: "http/config" });
|
||||
|
||||
@@ -34,7 +78,7 @@ export const saveHttpConfig = (
|
||||
) =>
|
||||
hass.callWS<SaveHttpConfigResult>({
|
||||
type: "http/config/configure",
|
||||
config,
|
||||
config: config ? stripHttpConfigMeta(config) : null,
|
||||
});
|
||||
|
||||
export const promoteHttpConfig = (hass: HomeAssistant) =>
|
||||
|
||||
@@ -531,6 +531,10 @@ export interface StringSelector {
|
||||
placeholder?: string;
|
||||
autocomplete?: string;
|
||||
multiple?: true;
|
||||
// Regular expression the value must match (HTML `pattern`); with `multiple`
|
||||
// every entry is validated. `validation_message` is shown when it fails.
|
||||
pattern?: string;
|
||||
validation_message?: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,27 @@
|
||||
import { mdiArrowRight } from "@mdi/js";
|
||||
import { ERR_CONNECTION_LOST } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { formatNumericDuration } from "../../common/datetime/format_duration";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-svg-icon";
|
||||
import type { HttpConfig } from "../../data/http";
|
||||
import { promoteHttpConfig, saveHttpConfig } from "../../data/http";
|
||||
import {
|
||||
HTTP_CONFIG_FIELDS,
|
||||
promoteHttpConfig,
|
||||
saveHttpConfig,
|
||||
} from "../../data/http";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HassDialog } from "../make-dialog-manager";
|
||||
import type { HttpPendingConfigDialogParams } from "./show-dialog-http-pending-config";
|
||||
|
||||
const HTTP_FIELDS: (keyof HttpConfig)[] = [
|
||||
"server_port",
|
||||
"server_host",
|
||||
"ssl_certificate",
|
||||
"ssl_key",
|
||||
"ssl_peer_certificate",
|
||||
"ssl_profile",
|
||||
"cors_allowed_origins",
|
||||
"use_x_forwarded_for",
|
||||
"trusted_proxies",
|
||||
"use_x_frame_options",
|
||||
"ip_ban_enabled",
|
||||
"login_attempts_threshold",
|
||||
];
|
||||
|
||||
@customElement("dialog-http-pending-config")
|
||||
export class DialogHttpPendingConfig
|
||||
extends LitElement
|
||||
@@ -57,6 +50,10 @@ export class DialogHttpPendingConfig
|
||||
this._error = undefined;
|
||||
this._reverted = false;
|
||||
this._startCountdown();
|
||||
// The field labels live in the config panel fragment, which is not loaded
|
||||
// yet when this dialog pops up on startup. Load it so the changed-field
|
||||
// names resolve; the dialog re-renders once hass updates.
|
||||
this.hass.loadFragmentTranslation("config");
|
||||
}
|
||||
|
||||
public closeDialog(): boolean {
|
||||
@@ -114,17 +111,44 @@ export class DialogHttpPendingConfig
|
||||
return [];
|
||||
}
|
||||
const { stable, pending } = this._params.state;
|
||||
return HTTP_FIELDS.filter(
|
||||
return HTTP_CONFIG_FIELDS.filter(
|
||||
(key) => JSON.stringify(stable[key]) !== JSON.stringify(pending[key])
|
||||
);
|
||||
}
|
||||
|
||||
private _formatValue(key: keyof HttpConfig, value: unknown): string {
|
||||
if (value === undefined || value === null || value === "") {
|
||||
return this.hass.localize("ui.dialogs.http_pending_config.not_set");
|
||||
}
|
||||
if (typeof value === "boolean") {
|
||||
return this.hass.localize(value ? "ui.common.yes" : "ui.common.no");
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.length
|
||||
? value.join(", ")
|
||||
: this.hass.localize("ui.dialogs.http_pending_config.not_set");
|
||||
}
|
||||
if (key === "ssl_profile") {
|
||||
return (
|
||||
this.hass.localize(
|
||||
`ui.panel.config.network.http.ssl_profile_${value}` as any
|
||||
) || String(value)
|
||||
);
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const changes = this._changedFields;
|
||||
const { stable, pending } = this._params.state;
|
||||
const rtl = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
@@ -187,9 +211,25 @@ export class DialogHttpPendingConfig
|
||||
${changes.map(
|
||||
(key) => html`
|
||||
<li>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.network.http.fields.${key}` as any
|
||||
)}
|
||||
<span class="field">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.network.http.fields.${key}` as any
|
||||
)}
|
||||
</span>
|
||||
<span class="values">
|
||||
<span class="old"
|
||||
>${this._formatValue(key, stable[key])}</span
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiArrowRight}
|
||||
style=${styleMap({
|
||||
transform: rtl ? "scaleX(-1)" : "",
|
||||
})}
|
||||
></ha-svg-icon>
|
||||
<span class="new"
|
||||
>${this._formatValue(key, pending![key])}</span
|
||||
>
|
||||
</span>
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
@@ -320,15 +360,37 @@ export class DialogHttpPendingConfig
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0 0 var(--ha-space-4) 0;
|
||||
padding-left: var(--ha-space-6);
|
||||
padding-inline-start: var(--ha-space-6);
|
||||
padding-inline-end: initial;
|
||||
color: var(--secondary-text-color);
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
padding: var(--ha-space-2) 0;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.field {
|
||||
display: block;
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
margin-bottom: var(--ha-space-1);
|
||||
}
|
||||
.values {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-2);
|
||||
color: var(--secondary-text-color);
|
||||
word-break: break-word;
|
||||
}
|
||||
.values .new {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.values ha-svg-icon {
|
||||
--mdc-icon-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-top: var(--ha-space-4);
|
||||
|
||||
@@ -123,6 +123,10 @@ interface EMOutgoingMessageConnectionStatus extends EMMessage {
|
||||
payload: { event: string };
|
||||
}
|
||||
|
||||
interface EMOutgoingMessageFrontendLoaded extends EMMessage {
|
||||
type: "frontend/loaded"; // Fired once the launch screen is removed (connected and essential data loaded)
|
||||
}
|
||||
|
||||
interface EMOutgoingMessageAppConfiguration extends EMMessage {
|
||||
type: "config_screen/show";
|
||||
}
|
||||
@@ -201,6 +205,7 @@ type EMOutgoingMessageWithoutAnswer =
|
||||
| EMOutgoingMessageBarCodeNotify
|
||||
| EMOutgoingMessageBarCodeScan
|
||||
| EMOutgoingMessageConnectionStatus
|
||||
| EMOutgoingMessageFrontendLoaded
|
||||
| EMOutgoingMessageExoplayerPlayHLS
|
||||
| EMOutgoingMessageExoplayerResize
|
||||
| EMOutgoingMessageExoplayerStop
|
||||
|
||||
@@ -120,6 +120,7 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
this.render = this.renderHass;
|
||||
this.update = super.update;
|
||||
removeLaunchScreen();
|
||||
this.hass.auth.external?.fireMessage({ type: "frontend/loaded" });
|
||||
}
|
||||
super.update(changedProps);
|
||||
}
|
||||
@@ -249,7 +250,14 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
// The check re-runs on the next reconnect; ignore transient failures.
|
||||
return;
|
||||
}
|
||||
if (!httpConfig.pending || this._httpPendingDialogOpen) {
|
||||
// Only prompt for an active trial. A pending config with an error was
|
||||
// already reverted/failed and is kept only for display in the config form,
|
||||
// so it must not pop the confirm/revert dialog.
|
||||
if (
|
||||
!httpConfig.pending ||
|
||||
httpConfig.pending.error ||
|
||||
this._httpPendingDialogOpen
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._httpPendingDialogOpen = true;
|
||||
|
||||
@@ -3,6 +3,10 @@ import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
IP_ADDRESS_OR_NETWORK_PATTERN,
|
||||
IP_ADDRESS_PATTERN,
|
||||
} from "../../../common/string/is_ip_address";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
@@ -10,8 +14,16 @@ import "../../../components/ha-card";
|
||||
import "../../../components/ha-form/ha-form";
|
||||
import type { HaForm } from "../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../components/ha-form/types";
|
||||
import { fetchHttpConfig, saveHttpConfig } from "../../../data/http";
|
||||
import type { HttpConfig } from "../../../data/http";
|
||||
import {
|
||||
fetchHttpConfig,
|
||||
HTTP_CONFIG_FIELDS,
|
||||
saveHttpConfig,
|
||||
} from "../../../data/http";
|
||||
import type {
|
||||
ActiveConfigType,
|
||||
HttpConfig,
|
||||
HttpConfigWithMeta,
|
||||
} from "../../../data/http";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
@@ -24,10 +36,6 @@ const SCHEMA = memoizeOne(
|
||||
required: true,
|
||||
selector: { number: { min: 1, max: 65535, mode: "box" } },
|
||||
},
|
||||
{
|
||||
name: "server_host",
|
||||
selector: { text: { multiple: true } },
|
||||
},
|
||||
{
|
||||
name: "ssl",
|
||||
type: "expandable",
|
||||
@@ -81,7 +89,15 @@ const SCHEMA = memoizeOne(
|
||||
},
|
||||
{
|
||||
name: "trusted_proxies",
|
||||
selector: { text: { multiple: true } },
|
||||
selector: {
|
||||
text: {
|
||||
multiple: true,
|
||||
pattern: IP_ADDRESS_OR_NETWORK_PATTERN,
|
||||
validation_message: localize(
|
||||
"ui.panel.config.network.http.invalid_network"
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -108,6 +124,18 @@ const SCHEMA = memoizeOne(
|
||||
flatten: true,
|
||||
title: localize("ui.panel.config.network.http.sections.advanced"),
|
||||
schema: [
|
||||
{
|
||||
name: "server_host",
|
||||
selector: {
|
||||
text: {
|
||||
multiple: true,
|
||||
pattern: IP_ADDRESS_PATTERN,
|
||||
validation_message: localize(
|
||||
"ui.panel.config.network.http.invalid_host"
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cors_allowed_origins",
|
||||
selector: { text: { multiple: true } },
|
||||
@@ -137,6 +165,11 @@ class HaConfigHttpForm extends LitElement {
|
||||
|
||||
@state() private _showNoChanges = false;
|
||||
|
||||
@state() private _activeConfigType?: ActiveConfigType;
|
||||
|
||||
// A pending config that was reverted/failed and kept only for display.
|
||||
@state() private _revertedPending?: HttpConfigWithMeta;
|
||||
|
||||
@query("ha-form") private _form?: HaForm;
|
||||
|
||||
@query("ha-alert") private _firstAlert?: HTMLElement;
|
||||
@@ -165,6 +198,9 @@ class HaConfigHttpForm extends LitElement {
|
||||
|
||||
const schema = SCHEMA(this.hass.localize);
|
||||
|
||||
const portChanged =
|
||||
!!this._stable && this._config?.server_port !== this._stable.server_port;
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
@@ -174,6 +210,51 @@ class HaConfigHttpForm extends LitElement {
|
||||
<p class="description">
|
||||
${this.hass.localize("ui.panel.config.network.http.description")}
|
||||
</p>
|
||||
${
|
||||
this._activeConfigType === "default"
|
||||
? html`
|
||||
<ha-alert alert-type="warning">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.network.http.running_default"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._revertedPending
|
||||
? html`
|
||||
<ha-alert alert-type="warning">
|
||||
${
|
||||
this._revertedPending.error === "not_promoted"
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.network.http.reverted_not_confirmed"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.network.http.reverted_failed",
|
||||
{ error: this._revertedPending.error ?? "" }
|
||||
)
|
||||
}
|
||||
<ha-button slot="action" @click=${this._reviewReverted}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.network.http.reverted_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
portChanged
|
||||
? html`
|
||||
<ha-alert alert-type="warning">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.network.http.port_warning"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
@@ -228,16 +309,30 @@ class HaConfigHttpForm extends LitElement {
|
||||
|
||||
private async _fetchConfig(): Promise<void> {
|
||||
try {
|
||||
// Pending is exclusively handled by the global confirm/revert dialog, so
|
||||
// the form only ever displays stable.
|
||||
const { stable } = await fetchHttpConfig(this.hass);
|
||||
const { stable, pending, active_config_type } = await fetchHttpConfig(
|
||||
this.hass
|
||||
);
|
||||
this._stable = stable;
|
||||
this._config = { ...stable };
|
||||
this._activeConfigType = active_config_type;
|
||||
// An active trial pending (no error) is handled by the global
|
||||
// confirm/revert dialog. A pending carrying an error was reverted or
|
||||
// failed to apply and is kept only so we can surface it here.
|
||||
this._revertedPending = pending?.error ? pending : undefined;
|
||||
} catch (err: any) {
|
||||
this._error = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
private _reviewReverted(): void {
|
||||
if (!this._revertedPending) {
|
||||
return;
|
||||
}
|
||||
// Load the reverted values into the form so the user can fix and re-save.
|
||||
this._config = { ...this._revertedPending };
|
||||
this._revertedPending = undefined;
|
||||
}
|
||||
|
||||
private _computeLabel = (
|
||||
schema: SchemaUnion<ReturnType<typeof SCHEMA>>
|
||||
): string => {
|
||||
@@ -318,15 +413,7 @@ class HaConfigHttpForm extends LitElement {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// voluptuous formats errors as "<message> @ data['<field>']".
|
||||
// If a field is identified, mark it inline; otherwise show a card-level
|
||||
// alert.
|
||||
const fieldMatch = err.message?.match(/\bdata\['([^']+)'\]/);
|
||||
if (fieldMatch) {
|
||||
this._fieldErrors = { [fieldMatch[1]]: err.message };
|
||||
} else {
|
||||
this._error = err.message;
|
||||
}
|
||||
this._handleSaveError(err);
|
||||
} finally {
|
||||
this._saving = false;
|
||||
}
|
||||
@@ -340,6 +427,34 @@ class HaConfigHttpForm extends LitElement {
|
||||
target?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
|
||||
private _handleSaveError(err: any): void {
|
||||
const rawMessage =
|
||||
(typeof err === "string" ? err : err?.message) ||
|
||||
this.hass.localize("ui.panel.config.network.http.save_error");
|
||||
// Voluptuous formats validation errors as
|
||||
// "<reason> @ data['config']['<field>'][<index>]. Got '<value>'"
|
||||
// Strip the internal data path for display and pick the deepest known
|
||||
// field name so it can also be flagged inline.
|
||||
const message =
|
||||
rawMessage.replace(/\s*@\s*data(\['[^']*'\]|\[\d+\])+/g, "").trim() ||
|
||||
rawMessage;
|
||||
const field = [...rawMessage.matchAll(/\['([^']+)'\]/g)]
|
||||
.map((match) => match[1])
|
||||
.reverse()
|
||||
.find((name) => HTTP_CONFIG_FIELDS.includes(name as keyof HttpConfig)) as
|
||||
keyof HttpConfig | undefined;
|
||||
|
||||
if (field) {
|
||||
// Show a card-level alert too — the field may sit in a collapsed section.
|
||||
this._error = `${this.hass.localize(
|
||||
`ui.panel.config.network.http.fields.${field}` as any
|
||||
)}: ${message}`;
|
||||
this._fieldErrors = { [field]: message };
|
||||
} else {
|
||||
this._error = message;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
||||
@@ -49,8 +49,8 @@ import { resolveEntityIDs } from "../../data/selector";
|
||||
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle, haStyleScrollbar } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { fileDownload } from "../../util/file_download";
|
||||
import { addEntitiesToLovelaceView } from "../lovelace/editor/add-entities-to-view";
|
||||
import { csvSafeString, csvDownload } from "../../util/csv";
|
||||
|
||||
@customElement("ha-panel-history")
|
||||
class HaPanelHistory extends LitElement {
|
||||
@@ -443,8 +443,8 @@ class HaPanelHistory extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const csv: string[] = [""]; // headers will be replaced later.
|
||||
const headers = ["entity_id", "state", "last_changed"];
|
||||
const csv: string[][] = [[]]; // headers will be replaced later.
|
||||
const processedDomainAttributes = new Set<string>();
|
||||
const domainAttributes: Record<string, Record<string, number>> = {
|
||||
climate: {
|
||||
@@ -486,7 +486,7 @@ class HaPanelHistory extends LitElement {
|
||||
|
||||
if (entity.statistics) {
|
||||
for (const s of entity.statistics) {
|
||||
csv.push(`${entityId},${s.state},${formatDate(s.last_changed)}\n`);
|
||||
csv.push([entityId, s.state, formatDate(s.last_changed)]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,25 +503,22 @@ class HaPanelHistory extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
csv.push(data.join(",") + "\n");
|
||||
csv.push(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const timeline of this._mungedStateHistory.timeline) {
|
||||
const entityId = timeline.entity_id;
|
||||
for (const s of timeline.data) {
|
||||
const safeState = /,|"/.test(s.state)
|
||||
? `"${s.state.replaceAll('"', '""')}"`
|
||||
: s.state;
|
||||
csv.push(`${entityId},${safeState},${formatDate(s.last_changed)}\n`);
|
||||
csv.push([
|
||||
entityId,
|
||||
csvSafeString(s.state),
|
||||
formatDate(s.last_changed),
|
||||
]);
|
||||
}
|
||||
}
|
||||
csv[0] = headers.join(",") + "\n";
|
||||
const blob = new Blob(csv, {
|
||||
type: "text/csv",
|
||||
});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
fileDownload(url, "history.csv");
|
||||
csv[0] = headers;
|
||||
csvDownload(csv, "history.csv");
|
||||
}
|
||||
|
||||
private _suggestCard() {
|
||||
|
||||
@@ -100,6 +100,10 @@ export class HaLogbook extends LitElement {
|
||||
|
||||
private _readyListenerAttached = false;
|
||||
|
||||
public getEntries(): LogbookEntry[] {
|
||||
return this._logbookEntries || [];
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!isComponentLoaded(this.hass.config, "logbook")) {
|
||||
return nothing;
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { mdiFilterRemove, mdiRefresh } from "@mdi/js";
|
||||
import {
|
||||
mdiDotsVertical,
|
||||
mdiDownload,
|
||||
mdiFilterRemove,
|
||||
mdiRefresh,
|
||||
} from "@mdi/js";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fromUnixTime } from "date-fns";
|
||||
import { storage } from "../../common/decorators/storage";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
||||
@@ -18,6 +24,9 @@ import {
|
||||
} from "../../common/url/search-params";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import "../../components/date-picker/ha-date-range-picker";
|
||||
import "../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../components/ha-dropdown";
|
||||
import "../../components/ha-dropdown-item";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-target-picker";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
@@ -27,6 +36,8 @@ import { resolveEntityIDs } from "../../data/selector";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./ha-logbook";
|
||||
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { csvDownload, csvSafeString } from "../../util/csv";
|
||||
|
||||
interface LogbookState {
|
||||
time: { range: [Date, Date] };
|
||||
@@ -77,12 +88,24 @@ export class HaPanelLogbook extends LitElement {
|
||||
.path=${mdiFilterRemove}
|
||||
.label=${this.hass.localize("ui.common.reset")}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
slot="actionItems"
|
||||
@click=${this._refreshLogbook}
|
||||
.path=${mdiRefresh}
|
||||
.label=${this.hass!.localize("ui.common.refresh")}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-dropdown slot="actionItems" @wa-select=${this._handleMenuAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-dropdown-item value="refresh">
|
||||
${this.hass.localize("ui.common.refresh")}
|
||||
<ha-svg-icon slot="icon" .path=${mdiRefresh}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-dropdown-item value="download">
|
||||
${this.hass.localize("ui.panel.logbook.download_data")}
|
||||
<ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
|
||||
<div class="content">
|
||||
<div class="filters">
|
||||
@@ -268,6 +291,67 @@ export class HaPanelLogbook extends LitElement {
|
||||
this.shadowRoot!.querySelector("ha-logbook")?.refresh();
|
||||
}
|
||||
|
||||
private async _handleMenuAction(ev: HaDropdownSelectEvent) {
|
||||
const action = ev.detail.item.value;
|
||||
switch (action) {
|
||||
case "download":
|
||||
this._downloadData();
|
||||
break;
|
||||
case "refresh":
|
||||
this._refreshLogbook();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _downloadData() {
|
||||
const data =
|
||||
this.shadowRoot!.querySelector("ha-logbook")?.getEntries() || [];
|
||||
|
||||
if (data.length === 0) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.panel.logbook.download_data_error"),
|
||||
text: this.hass.localize("ui.panel.logbook.error_no_data"),
|
||||
warning: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = [
|
||||
"time",
|
||||
"entity_id",
|
||||
"state",
|
||||
"event_type",
|
||||
"context_id",
|
||||
"context_user_id",
|
||||
"context_event_type",
|
||||
"context_domain",
|
||||
"context_service",
|
||||
"context_entity_id",
|
||||
"context_state",
|
||||
"context_source",
|
||||
];
|
||||
const csv: string[][] = [headers];
|
||||
|
||||
for (const d of data) {
|
||||
const time = fromUnixTime(d.when).toISOString();
|
||||
csv.push([
|
||||
time,
|
||||
d.entity_id || "",
|
||||
csvSafeString(d.state),
|
||||
csvSafeString(d.attributes?.event_type),
|
||||
d.context_id || "",
|
||||
d.context_user_id || "",
|
||||
csvSafeString(d.context_event_type),
|
||||
d.context_domain || "",
|
||||
d.context_service || "",
|
||||
d.context_entity_id || "",
|
||||
csvSafeString(d.context_state),
|
||||
d.context_source || "",
|
||||
]);
|
||||
}
|
||||
csvDownload(csv, "activity.csv");
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
haStyle,
|
||||
|
||||
@@ -23,6 +23,7 @@ import type {
|
||||
LovelaceGridOptions,
|
||||
LovelaceHeaderFooter,
|
||||
} from "../types";
|
||||
import { migrateEntitiesCardConfig } from "./migrate-card-config";
|
||||
import type { EntitiesCardConfig } from "./types";
|
||||
import { haStyleScrollbar } from "../../../resources/styles";
|
||||
|
||||
@@ -49,32 +50,7 @@ export const computeShowHeaderToggle = <
|
||||
return !!config.show_header_toggle;
|
||||
};
|
||||
|
||||
export const migrateEntitiesCardConfig = (
|
||||
config: EntitiesCardConfig
|
||||
): EntitiesCardConfig => {
|
||||
let changed = false;
|
||||
const newEntities = config.entities?.map((e) => {
|
||||
if (typeof e !== "object") {
|
||||
return e;
|
||||
}
|
||||
if (!("format" in e)) {
|
||||
return e;
|
||||
}
|
||||
changed = true;
|
||||
const { format, ...rest } = e;
|
||||
return {
|
||||
...rest,
|
||||
time_format: (rest as EntityConfig).time_format ?? format,
|
||||
};
|
||||
});
|
||||
if (!changed) {
|
||||
return config;
|
||||
}
|
||||
return {
|
||||
...config,
|
||||
entities: newEntities as (LovelaceRowConfig | string)[],
|
||||
};
|
||||
};
|
||||
export { migrateEntitiesCardConfig };
|
||||
|
||||
@customElement("hui-entities-card")
|
||||
class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@@ -31,35 +31,11 @@ import "../components/hui-timestamp-display";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import "../components/hui-warning-element";
|
||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { migrateGlanceCardConfig } from "./migrate-card-config";
|
||||
import type { GlanceCardConfig, GlanceConfigEntity } from "./types";
|
||||
import { TIMESTAMP_STATE_DOMAINS } from "../../../common/const";
|
||||
|
||||
export const migrateGlanceCardConfig = (
|
||||
config: GlanceCardConfig
|
||||
): GlanceCardConfig => {
|
||||
let changed = false;
|
||||
const newEntities = config.entities?.map((e) => {
|
||||
if (typeof e !== "object") {
|
||||
return e;
|
||||
}
|
||||
if (!("format" in e)) {
|
||||
return e;
|
||||
}
|
||||
changed = true;
|
||||
const { format, ...rest } = e;
|
||||
return {
|
||||
...rest,
|
||||
time_format: rest.time_format ?? format,
|
||||
};
|
||||
});
|
||||
if (!changed) {
|
||||
return config;
|
||||
}
|
||||
return {
|
||||
...config,
|
||||
entities: newEntities as (GlanceConfigEntity | string)[],
|
||||
};
|
||||
};
|
||||
export { migrateGlanceCardConfig };
|
||||
|
||||
@customElement("hui-glance-card")
|
||||
export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { EntityConfig, LovelaceRowConfig } from "../entity-rows/types";
|
||||
import type {
|
||||
EntitiesCardConfig,
|
||||
GlanceCardConfig,
|
||||
GlanceConfigEntity,
|
||||
} from "./types";
|
||||
|
||||
export const migrateEntitiesCardConfig = (
|
||||
config: EntitiesCardConfig
|
||||
): EntitiesCardConfig => {
|
||||
let changed = false;
|
||||
const newEntities = config.entities?.map((e) => {
|
||||
if (typeof e !== "object") {
|
||||
return e;
|
||||
}
|
||||
// Custom rows own their config schema and may use `format` with a
|
||||
// different meaning (e.g. custom:multiple-entity-row), so leave it
|
||||
// untouched.
|
||||
if (e.type?.startsWith("custom:")) {
|
||||
return e;
|
||||
}
|
||||
if (!("format" in e)) {
|
||||
return e;
|
||||
}
|
||||
changed = true;
|
||||
const { format, ...rest } = e;
|
||||
return {
|
||||
...rest,
|
||||
time_format: (rest as EntityConfig).time_format ?? format,
|
||||
};
|
||||
});
|
||||
if (!changed) {
|
||||
return config;
|
||||
}
|
||||
return {
|
||||
...config,
|
||||
entities: newEntities as (LovelaceRowConfig | string)[],
|
||||
};
|
||||
};
|
||||
|
||||
export const migrateGlanceCardConfig = (
|
||||
config: GlanceCardConfig
|
||||
): GlanceCardConfig => {
|
||||
let changed = false;
|
||||
const newEntities = config.entities?.map((e) => {
|
||||
if (typeof e !== "object") {
|
||||
return e;
|
||||
}
|
||||
if (!("format" in e)) {
|
||||
return e;
|
||||
}
|
||||
changed = true;
|
||||
const { format, ...rest } = e;
|
||||
return {
|
||||
...rest,
|
||||
time_format: rest.time_format ?? format,
|
||||
};
|
||||
});
|
||||
if (!changed) {
|
||||
return config;
|
||||
}
|
||||
return {
|
||||
...config,
|
||||
entities: newEntities as (GlanceConfigEntity | string)[],
|
||||
};
|
||||
};
|
||||
@@ -1486,6 +1486,7 @@
|
||||
"auto_revert": "Settings will automatically revert in {time}.",
|
||||
"reverted": "Home Assistant reverted the HTTP server settings.",
|
||||
"changes_label": "Changed settings:",
|
||||
"not_set": "Not set",
|
||||
"confirm": "Confirm",
|
||||
"revert": "Revert",
|
||||
"close": "Close",
|
||||
@@ -8679,6 +8680,14 @@
|
||||
"description": "Configure how Home Assistant serves its web interface. Saving restarts Home Assistant.",
|
||||
"save": "Save",
|
||||
"save_no_changes": "Nothing changed — no restart needed.",
|
||||
"save_error": "Could not save the HTTP configuration.",
|
||||
"port_warning": "Clients such as the Home Assistant mobile apps will lose their connection until you update the URL in their settings. If Home Assistant is not confirmed reachable on the new port, the change is rolled back automatically after 5 minutes.",
|
||||
"invalid_host": "Enter a valid IP address.",
|
||||
"invalid_network": "Enter a valid IP address or network.",
|
||||
"running_default": "Your saved HTTP configuration could not be applied, so Home Assistant is running on the built-in default configuration.",
|
||||
"reverted_not_confirmed": "The last HTTP configuration change was not confirmed in time and was rolled back. Home Assistant is running on the previous configuration.",
|
||||
"reverted_failed": "The last HTTP configuration change could not be applied and was rolled back. Home Assistant is running on the previous configuration. Reason: {error}",
|
||||
"reverted_action": "Review the change",
|
||||
"save_confirm": {
|
||||
"title": "Restart required",
|
||||
"text": "Saving will restart Home Assistant to apply the new HTTP settings.",
|
||||
@@ -8690,7 +8699,7 @@
|
||||
"ssl": "SSL/TLS",
|
||||
"reverse_proxy": "Reverse proxy",
|
||||
"ip_banning": "IP banning",
|
||||
"advanced": "Advanced"
|
||||
"advanced": "More options"
|
||||
},
|
||||
"fields": {
|
||||
"server_port": "Server port",
|
||||
@@ -11566,6 +11575,11 @@
|
||||
"add_card": "Add current view as card",
|
||||
"add_card_error": "Unable to add card",
|
||||
"error_no_data": "You need to select some data sources first."
|
||||
},
|
||||
"logbook": {
|
||||
"download_data": "[%key:ui::panel::history::download_data%]",
|
||||
"download_data_error": "[%key:ui::panel::history::download_data_error%]",
|
||||
"error_no_data": "No activity for the selected target in the selected time range."
|
||||
}
|
||||
},
|
||||
"tips": {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { fileDownload } from "./file_download";
|
||||
|
||||
export function csvSafeString(s: string | undefined): string {
|
||||
if (!s) return "";
|
||||
return /,|"/.test(s) ? `"${s.replaceAll('"', '""')}"` : s;
|
||||
}
|
||||
|
||||
export function csvDownload(data: string[][], filename: string) {
|
||||
const csv = data.map((row) => row.join(",").concat("\n"));
|
||||
const blob = new Blob(csv, {
|
||||
type: "text/csv",
|
||||
});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
fileDownload(url, filename);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
IP_ADDRESS_OR_NETWORK_PATTERN,
|
||||
IP_ADDRESS_PATTERN,
|
||||
isIPAddress,
|
||||
isIPAddressV4OrV6,
|
||||
isIPNetwork,
|
||||
isIPv6Address,
|
||||
} from "../../../src/common/string/is_ip_address";
|
||||
|
||||
describe("isIPAddress (IPv4)", () => {
|
||||
it("accepts valid IPv4 addresses", () => {
|
||||
expect(isIPAddress("192.168.1.10")).toBe(true);
|
||||
expect(isIPAddress("0.0.0.0")).toBe(true);
|
||||
expect(isIPAddress("255.255.255.255")).toBe(true);
|
||||
});
|
||||
it("rejects invalid IPv4 addresses", () => {
|
||||
expect(isIPAddress("256.1.1.1")).toBe(false);
|
||||
expect(isIPAddress("192.168.1")).toBe(false);
|
||||
expect(isIPAddress("192.168.1.10/24")).toBe(false);
|
||||
expect(isIPAddress("fe80::1")).toBe(false);
|
||||
expect(isIPAddress("")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isIPv6Address", () => {
|
||||
it("accepts valid IPv6 addresses", () => {
|
||||
expect(isIPv6Address("fe80::85d:e82c:9446:7995")).toBe(true);
|
||||
expect(isIPv6Address("::1")).toBe(true);
|
||||
expect(isIPv6Address("1050:0000:0000:0000:0005:0600:300c:326b")).toBe(true);
|
||||
expect(isIPv6Address("::ffff:192.168.1.1")).toBe(true);
|
||||
});
|
||||
it("rejects invalid IPv6 addresses", () => {
|
||||
expect(isIPv6Address("192.168.1.10")).toBe(false);
|
||||
expect(isIPv6Address("fe80::85d::7995")).toBe(false);
|
||||
expect(isIPv6Address("gggg::1")).toBe(false);
|
||||
expect(isIPv6Address("")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isIPAddressV4OrV6", () => {
|
||||
it("accepts both families", () => {
|
||||
expect(isIPAddressV4OrV6("192.168.1.10")).toBe(true);
|
||||
expect(isIPAddressV4OrV6("fe80::1")).toBe(true);
|
||||
});
|
||||
it("rejects networks and garbage", () => {
|
||||
expect(isIPAddressV4OrV6("192.168.1.0/24")).toBe(false);
|
||||
expect(isIPAddressV4OrV6("not-an-ip")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isIPNetwork (CIDR)", () => {
|
||||
it("accepts valid networks", () => {
|
||||
expect(isIPNetwork("192.168.1.0/24")).toBe(true);
|
||||
expect(isIPNetwork("10.0.0.0/8")).toBe(true);
|
||||
expect(isIPNetwork("172.16.0.0/12")).toBe(true);
|
||||
expect(isIPNetwork("0.0.0.0/0")).toBe(true);
|
||||
expect(isIPNetwork("fd00::/8")).toBe(true);
|
||||
expect(isIPNetwork("fe80::/128")).toBe(true);
|
||||
});
|
||||
it("rejects invalid networks", () => {
|
||||
// Prefix out of range — the reported production error.
|
||||
expect(isIPNetwork("172.30.33.0/24444444")).toBe(false);
|
||||
expect(isIPNetwork("192.168.1.0/33")).toBe(false);
|
||||
expect(isIPNetwork("fd00::/129")).toBe(false);
|
||||
expect(isIPNetwork("192.168.1.0")).toBe(false);
|
||||
expect(isIPNetwork("192.168.1.0/24/24")).toBe(false);
|
||||
expect(isIPNetwork("not-a-network/24")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// The HTML `pattern` attribute is anchored by the browser as `^(?:…)$`.
|
||||
const matchesPattern = (pattern: string, value: string): boolean =>
|
||||
new RegExp(`^(?:${pattern})$`).test(value);
|
||||
|
||||
describe("IP_ADDRESS_PATTERN", () => {
|
||||
it("accepts IPv4 and IPv6 addresses", () => {
|
||||
expect(matchesPattern(IP_ADDRESS_PATTERN, "192.168.1.10")).toBe(true);
|
||||
expect(matchesPattern(IP_ADDRESS_PATTERN, "fe80::1")).toBe(true);
|
||||
});
|
||||
it("rejects networks and garbage", () => {
|
||||
expect(matchesPattern(IP_ADDRESS_PATTERN, "192.168.1.0/24")).toBe(false);
|
||||
expect(matchesPattern(IP_ADDRESS_PATTERN, "not-an-ip")).toBe(false);
|
||||
expect(matchesPattern(IP_ADDRESS_PATTERN, "256.1.1.1")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("IP_ADDRESS_OR_NETWORK_PATTERN", () => {
|
||||
it("accepts addresses and networks", () => {
|
||||
expect(matchesPattern(IP_ADDRESS_OR_NETWORK_PATTERN, "192.168.1.10")).toBe(
|
||||
true
|
||||
);
|
||||
expect(
|
||||
matchesPattern(IP_ADDRESS_OR_NETWORK_PATTERN, "192.168.1.0/24")
|
||||
).toBe(true);
|
||||
expect(matchesPattern(IP_ADDRESS_OR_NETWORK_PATTERN, "fd00::/8")).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
it("rejects out-of-range prefixes and garbage", () => {
|
||||
// The reported production error.
|
||||
expect(
|
||||
matchesPattern(IP_ADDRESS_OR_NETWORK_PATTERN, "172.30.33.0/24444444")
|
||||
).toBe(false);
|
||||
expect(
|
||||
matchesPattern(IP_ADDRESS_OR_NETWORK_PATTERN, "192.168.1.0/33")
|
||||
).toBe(false);
|
||||
expect(
|
||||
matchesPattern(IP_ADDRESS_OR_NETWORK_PATTERN, "1.1.1.1/233444")
|
||||
).toBe(false);
|
||||
expect(matchesPattern(IP_ADDRESS_OR_NETWORK_PATTERN, "not-an-ip")).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { getAvailableClockDatePartSections } from "../../src/components/ha-clock-date-format-picker";
|
||||
|
||||
type TestSection = "weekday" | "day" | "month" | "year" | "separator";
|
||||
|
||||
const section = (id: TestSection, itemIds: string[]) => ({
|
||||
id,
|
||||
title: id,
|
||||
items: itemIds.map((itemId) => ({ id: itemId, primary: itemId })),
|
||||
});
|
||||
|
||||
const ALL_SECTIONS = [
|
||||
section("day", ["day-numeric", "day-2-digit"]),
|
||||
section("month", ["month-numeric", "month-short", "month-long"]),
|
||||
section("year", ["year-numeric", "year-2-digit"]),
|
||||
section("weekday", ["weekday-short", "weekday-long"]),
|
||||
section("separator", [
|
||||
"separator-dash",
|
||||
"separator-slash",
|
||||
"separator-dot",
|
||||
"separator-new-line",
|
||||
]),
|
||||
];
|
||||
|
||||
describe("getAvailableClockDatePartSections", () => {
|
||||
it("returns every section when no value is set", () => {
|
||||
const result = getAvailableClockDatePartSections(ALL_SECTIONS, []);
|
||||
expect(result.map((sectionData) => sectionData.id)).toEqual([
|
||||
"day",
|
||||
"month",
|
||||
"year",
|
||||
"weekday",
|
||||
"separator",
|
||||
]);
|
||||
});
|
||||
|
||||
it("hides a group's section once a value from that group is used", () => {
|
||||
const result = getAvailableClockDatePartSections(ALL_SECTIONS, [
|
||||
"day-numeric",
|
||||
]);
|
||||
expect(result.map((sectionData) => sectionData.id)).toEqual([
|
||||
"month",
|
||||
"year",
|
||||
"weekday",
|
||||
"separator",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps the edited item's own group visible via excludeIndex", () => {
|
||||
const value = ["day-numeric", "month-short"];
|
||||
const result = getAvailableClockDatePartSections(ALL_SECTIONS, value, 0);
|
||||
expect(result.map((sectionData) => sectionData.id)).toEqual([
|
||||
"day",
|
||||
"year",
|
||||
"weekday",
|
||||
"separator",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps the edited item's group visible even when a duplicate of that group exists elsewhere", () => {
|
||||
const value = ["day-numeric", "day-2-digit", "month-long"];
|
||||
const result = getAvailableClockDatePartSections(ALL_SECTIONS, value, 0);
|
||||
expect(result.map((sectionData) => sectionData.id)).toEqual([
|
||||
"day",
|
||||
"year",
|
||||
"weekday",
|
||||
"separator",
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not crash and keeps normal filtering when excludeIndex is out of range", () => {
|
||||
const value = ["day-numeric", "month-long"];
|
||||
const result = getAvailableClockDatePartSections(ALL_SECTIONS, value, 5);
|
||||
expect(result.map((sectionData) => sectionData.id)).toEqual([
|
||||
"year",
|
||||
"weekday",
|
||||
"separator",
|
||||
]);
|
||||
});
|
||||
|
||||
it("never hides the separator section, even with multiple separators used", () => {
|
||||
const value = [
|
||||
"separator-dash",
|
||||
"separator-slash",
|
||||
"separator-dot",
|
||||
"separator-new-line",
|
||||
];
|
||||
const result = getAvailableClockDatePartSections(ALL_SECTIONS, value);
|
||||
expect(result.map((sectionData) => sectionData.id)).toEqual([
|
||||
"day",
|
||||
"month",
|
||||
"year",
|
||||
"weekday",
|
||||
"separator",
|
||||
]);
|
||||
});
|
||||
|
||||
it("treats an unrecognized token as a separator and does not hide any group", () => {
|
||||
const result = getAvailableClockDatePartSections(ALL_SECTIONS, [
|
||||
"not-a-real-part",
|
||||
]);
|
||||
expect(result.map((sectionData) => sectionData.id)).toEqual([
|
||||
"day",
|
||||
"month",
|
||||
"year",
|
||||
"weekday",
|
||||
"separator",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
migrateEntitiesCardConfig,
|
||||
migrateGlanceCardConfig,
|
||||
} from "../../../../src/panels/lovelace/cards/migrate-card-config";
|
||||
import type {
|
||||
EntitiesCardConfig,
|
||||
GlanceCardConfig,
|
||||
} from "../../../../src/panels/lovelace/cards/types";
|
||||
|
||||
describe("migrateEntitiesCardConfig", () => {
|
||||
it("migrates the legacy `format` option to `time_format` on native rows", () => {
|
||||
const config = {
|
||||
type: "entities",
|
||||
entities: [{ entity: "sensor.last_changed", format: "relative" }],
|
||||
} as unknown as EntitiesCardConfig;
|
||||
|
||||
const result = migrateEntitiesCardConfig(config);
|
||||
|
||||
expect(result.entities).toEqual([
|
||||
{ entity: "sensor.last_changed", time_format: "relative" },
|
||||
]);
|
||||
// `format` is dropped after migration
|
||||
expect(result.entities[0]).not.toHaveProperty("format");
|
||||
});
|
||||
|
||||
it("leaves custom rows untouched", () => {
|
||||
const customRow = {
|
||||
type: "custom:multiple-entity-row",
|
||||
entity: "sensor.power",
|
||||
format: "precision1",
|
||||
};
|
||||
const config = {
|
||||
type: "entities",
|
||||
entities: [customRow],
|
||||
} as unknown as EntitiesCardConfig;
|
||||
|
||||
const result = migrateEntitiesCardConfig(config);
|
||||
|
||||
// Nothing changed, so the same config reference is returned and the
|
||||
// custom row keeps its own `format` semantics.
|
||||
expect(result).toBe(config);
|
||||
expect(result.entities[0]).toEqual(customRow);
|
||||
});
|
||||
|
||||
it("returns the same config reference when there is nothing to migrate", () => {
|
||||
const config = {
|
||||
type: "entities",
|
||||
entities: [{ entity: "sensor.temperature" }, { entity: "light.kitchen" }],
|
||||
} as EntitiesCardConfig;
|
||||
|
||||
expect(migrateEntitiesCardConfig(config)).toBe(config);
|
||||
});
|
||||
|
||||
it("passes string rows through untouched", () => {
|
||||
const config = {
|
||||
type: "entities",
|
||||
entities: ["sensor.temperature", { entity: "sensor.x", format: "time" }],
|
||||
} as EntitiesCardConfig;
|
||||
|
||||
const result = migrateEntitiesCardConfig(config);
|
||||
|
||||
expect(result.entities[0]).toBe("sensor.temperature");
|
||||
expect(result.entities[1]).toEqual({
|
||||
entity: "sensor.x",
|
||||
time_format: "time",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps a pre-existing `time_format` over the legacy `format`", () => {
|
||||
const config = {
|
||||
type: "entities",
|
||||
entities: [
|
||||
{ entity: "sensor.x", format: "relative", time_format: "datetime" },
|
||||
],
|
||||
} as unknown as EntitiesCardConfig;
|
||||
|
||||
const result = migrateEntitiesCardConfig(config);
|
||||
|
||||
expect(result.entities[0]).toEqual({
|
||||
entity: "sensor.x",
|
||||
time_format: "datetime",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("migrateGlanceCardConfig", () => {
|
||||
it("migrates the legacy `format` option to `time_format`", () => {
|
||||
const config = {
|
||||
type: "glance",
|
||||
entities: [{ entity: "sensor.last_changed", format: "relative" }],
|
||||
} as unknown as GlanceCardConfig;
|
||||
|
||||
const result = migrateGlanceCardConfig(config);
|
||||
|
||||
expect(result.entities).toEqual([
|
||||
{ entity: "sensor.last_changed", time_format: "relative" },
|
||||
]);
|
||||
expect(result.entities[0]).not.toHaveProperty("format");
|
||||
});
|
||||
|
||||
it("returns the same config reference when there is nothing to migrate", () => {
|
||||
const config = {
|
||||
type: "glance",
|
||||
entities: ["sensor.temperature", { entity: "light.kitchen" }],
|
||||
} as GlanceCardConfig;
|
||||
|
||||
expect(migrateGlanceCardConfig(config)).toBe(config);
|
||||
});
|
||||
|
||||
it("keeps a pre-existing `time_format` over the legacy `format`", () => {
|
||||
const config = {
|
||||
type: "glance",
|
||||
entities: [
|
||||
{ entity: "sensor.x", format: "relative", time_format: "datetime" },
|
||||
],
|
||||
} as unknown as GlanceCardConfig;
|
||||
|
||||
const result = migrateGlanceCardConfig(config);
|
||||
|
||||
expect(result.entities[0]).toEqual({
|
||||
entity: "sensor.x",
|
||||
time_format: "datetime",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3434,10 +3434,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@lokalise/node-api@npm:16.0.0":
|
||||
version: 16.0.0
|
||||
resolution: "@lokalise/node-api@npm:16.0.0"
|
||||
checksum: 10/88075629f30feb19537f8c40dabf7a85cc0539445322c216686f7e5a68ba6735a083d7ce1eee806ce31606d28ee6250ca39b185a783ce5ec263e1e0e71c3a701
|
||||
"@lokalise/node-api@npm:16.1.0":
|
||||
version: 16.1.0
|
||||
resolution: "@lokalise/node-api@npm:16.1.0"
|
||||
checksum: 10/f77f9e7d25af18be950f15750863061d00382f0abd2cfab1461b4354ba3d7ecc514e30811bc656e9cf382632d205c099d5d3d974b487dba75ff00dc9c1e193e0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -5741,105 +5741,105 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/eslint-plugin@npm:8.63.0":
|
||||
version: 8.63.0
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.63.0"
|
||||
"@typescript-eslint/eslint-plugin@npm:8.64.0":
|
||||
version: 8.64.0
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.64.0"
|
||||
dependencies:
|
||||
"@eslint-community/regexpp": "npm:^4.12.2"
|
||||
"@typescript-eslint/scope-manager": "npm:8.63.0"
|
||||
"@typescript-eslint/type-utils": "npm:8.63.0"
|
||||
"@typescript-eslint/utils": "npm:8.63.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.63.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.64.0"
|
||||
"@typescript-eslint/type-utils": "npm:8.64.0"
|
||||
"@typescript-eslint/utils": "npm:8.64.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.64.0"
|
||||
ignore: "npm:^7.0.5"
|
||||
natural-compare: "npm:^1.4.0"
|
||||
ts-api-utils: "npm:^2.5.0"
|
||||
peerDependencies:
|
||||
"@typescript-eslint/parser": ^8.63.0
|
||||
"@typescript-eslint/parser": ^8.64.0
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/ab60da4c4a66e8b882b6d585457c6cd352492917940d43b255021f71fc057d327fe8e46856316b312a5c5b78b1245acb129869a7c60a7c2fa65e7822ef58c7c6
|
||||
checksum: 10/ec7cbcb44968386a4b9aff9272ce6b75bdcc7f398db5f2d07a21854baf0364ca33c74268c0e19d21386c6b87b9358b141df7bef3d26e4e4e7a9eb5f8f394dc75
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/parser@npm:8.63.0":
|
||||
version: 8.63.0
|
||||
resolution: "@typescript-eslint/parser@npm:8.63.0"
|
||||
"@typescript-eslint/parser@npm:8.64.0":
|
||||
version: 8.64.0
|
||||
resolution: "@typescript-eslint/parser@npm:8.64.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager": "npm:8.63.0"
|
||||
"@typescript-eslint/types": "npm:8.63.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.63.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.63.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.64.0"
|
||||
"@typescript-eslint/types": "npm:8.64.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.64.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.64.0"
|
||||
debug: "npm:^4.4.3"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/40ee2a1c703898b9c6df0731e5584ce0bf63d8c13fb464a952500dae156b68b41b522b2cb4755a2f0cb7e0d529b0888e3ba783e3eeb1236c062a3a7ca227f634
|
||||
checksum: 10/7239b16a6ca6bb1764ad04bc1eb46997336541d049fcffb4966ef404fbb02324b2b33aed50c2b976359ec8d3c97b90668cfd6aa9177228b4b799152f01a3904a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/project-service@npm:8.63.0":
|
||||
version: 8.63.0
|
||||
resolution: "@typescript-eslint/project-service@npm:8.63.0"
|
||||
"@typescript-eslint/project-service@npm:8.64.0":
|
||||
version: 8.64.0
|
||||
resolution: "@typescript-eslint/project-service@npm:8.64.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils": "npm:^8.63.0"
|
||||
"@typescript-eslint/types": "npm:^8.63.0"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:^8.64.0"
|
||||
"@typescript-eslint/types": "npm:^8.64.0"
|
||||
debug: "npm:^4.4.3"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/16b07d0e95abfbe56eccc7c67185caf43e9059740c82a09a0f0f4393539f89a19f8c1b996d396f9476401af55bd804897fd19c988712db3e0f36562d2805c223
|
||||
checksum: 10/511b9a8f1dcb32c99003cab9791309056ac6e889fe46227600deb743e869a63b3e78d7d2d3ef1bf65b2d5e574b73add3040fbef4c0f94aed587368aaea149559
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/scope-manager@npm:8.63.0":
|
||||
version: 8.63.0
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.63.0"
|
||||
"@typescript-eslint/scope-manager@npm:8.64.0":
|
||||
version: 8.64.0
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.64.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.63.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.63.0"
|
||||
checksum: 10/6b0183e85d79e287660274d39fed1fc8321ae03ae40545acaad6718fd2524063d302f3348c04576d038a25f16f8c9ca4e8780a0ad754f1a494a00027e867deda
|
||||
"@typescript-eslint/types": "npm:8.64.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.64.0"
|
||||
checksum: 10/3c72c4915cee19d632ddc7491c3a4668dd04aa8bcb112fde8ac10aea2cc0364aaa8439d9939d0c62a7b4159fc22f4ced7cba3a77df4422b21d76497068f08076
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@npm:8.63.0, @typescript-eslint/tsconfig-utils@npm:^8.63.0":
|
||||
version: 8.63.0
|
||||
resolution: "@typescript-eslint/tsconfig-utils@npm:8.63.0"
|
||||
"@typescript-eslint/tsconfig-utils@npm:8.64.0, @typescript-eslint/tsconfig-utils@npm:^8.64.0":
|
||||
version: 8.64.0
|
||||
resolution: "@typescript-eslint/tsconfig-utils@npm:8.64.0"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/7c23671159f8593b676d53bc52b4a3bd4c3ad62abe2ad99b379b70298abf3b784cdcf18ca2b10f2aa62a9f8e0232e6efb87364940a3bd4a32126a357f39ac3c8
|
||||
checksum: 10/905469c5067a9a2d18d065848c6497081ab787b22a9d7c06499f4bb593b7d67f9fb17e7861a114c46626555853cc52d4542db5311c8dc7cd138e45461a73e589
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/type-utils@npm:8.63.0":
|
||||
version: 8.63.0
|
||||
resolution: "@typescript-eslint/type-utils@npm:8.63.0"
|
||||
"@typescript-eslint/type-utils@npm:8.64.0":
|
||||
version: 8.64.0
|
||||
resolution: "@typescript-eslint/type-utils@npm:8.64.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.63.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.63.0"
|
||||
"@typescript-eslint/utils": "npm:8.63.0"
|
||||
"@typescript-eslint/types": "npm:8.64.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.64.0"
|
||||
"@typescript-eslint/utils": "npm:8.64.0"
|
||||
debug: "npm:^4.4.3"
|
||||
ts-api-utils: "npm:^2.5.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/8a1001edb129aec55c4ef8567f5f33af4b0ee92e89463f45e7d8a006872e438db1a3cbe2364769d205124d9d143018c920e165868788a0e7aee15466ed6d38f0
|
||||
checksum: 10/7ab1fd8c0d292c0155cf137d316b1618681eca0fe4cf446a05d909de41311ec6bb64fed82513e822063a962508d5b3e615fb0155a4a565d6b9c6cb81d06a5cd2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/types@npm:8.63.0, @typescript-eslint/types@npm:^8.56.0, @typescript-eslint/types@npm:^8.63.0":
|
||||
version: 8.63.0
|
||||
resolution: "@typescript-eslint/types@npm:8.63.0"
|
||||
checksum: 10/f72eb114970cae0da8e53f68cd8dca1b51ac410f4438141a2851655fa07aacb3090e24a2ae53462cf0970e4d5b52cabf9693aa6f07abdff5c210874806427e5b
|
||||
"@typescript-eslint/types@npm:8.64.0, @typescript-eslint/types@npm:^8.56.0, @typescript-eslint/types@npm:^8.64.0":
|
||||
version: 8.64.0
|
||||
resolution: "@typescript-eslint/types@npm:8.64.0"
|
||||
checksum: 10/b8951c00ce9b9702f3201f017354774ea5f39c30c2b6f815cb50d91f53f61c325e30f548329daf731d5169d752095cf102da54b754fb202bcfb619faa56fa9f4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/typescript-estree@npm:8.63.0":
|
||||
version: 8.63.0
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.63.0"
|
||||
"@typescript-eslint/typescript-estree@npm:8.64.0":
|
||||
version: 8.64.0
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.64.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service": "npm:8.63.0"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:8.63.0"
|
||||
"@typescript-eslint/types": "npm:8.63.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.63.0"
|
||||
"@typescript-eslint/project-service": "npm:8.64.0"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:8.64.0"
|
||||
"@typescript-eslint/types": "npm:8.64.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.64.0"
|
||||
debug: "npm:^4.4.3"
|
||||
minimatch: "npm:^10.2.2"
|
||||
semver: "npm:^7.7.3"
|
||||
@@ -5847,32 +5847,32 @@ __metadata:
|
||||
ts-api-utils: "npm:^2.5.0"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/793e0227698f4d8b325922816f27a4e737760aae255874c2021c5e2179499fd036b9e87bf0346ead6360444fb7ab4815bb558c94a17d8481632351bbdb2780bf
|
||||
checksum: 10/833101d25e820d1d0e317dfc9beca985b3c87e5458b20ed002c86c2d425667aa506f756f1759b54f724033effb529266f836f912c460ee662f347fb43dded6ed
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/utils@npm:8.63.0":
|
||||
version: 8.63.0
|
||||
resolution: "@typescript-eslint/utils@npm:8.63.0"
|
||||
"@typescript-eslint/utils@npm:8.64.0":
|
||||
version: 8.64.0
|
||||
resolution: "@typescript-eslint/utils@npm:8.64.0"
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils": "npm:^4.9.1"
|
||||
"@typescript-eslint/scope-manager": "npm:8.63.0"
|
||||
"@typescript-eslint/types": "npm:8.63.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.63.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.64.0"
|
||||
"@typescript-eslint/types": "npm:8.64.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.64.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/017bd1998822902b5dde5af9e031908aeb8861565b947aa217d5641bac4758fb831c52629b600bb58a990fcfef1ca58eaf5b44595575bb71a43761c2f40a7f56
|
||||
checksum: 10/ff7e5374bd6ef60d7df723e6440468e3a5d4879d1d9876e322904319a1f1d2f98d858fc9b9169dbbd7ee0786a07102a058f13e2b6c16d1bf9aae9eb836a258a3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/visitor-keys@npm:8.63.0":
|
||||
version: 8.63.0
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.63.0"
|
||||
"@typescript-eslint/visitor-keys@npm:8.64.0":
|
||||
version: 8.64.0
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.64.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.63.0"
|
||||
"@typescript-eslint/types": "npm:8.64.0"
|
||||
eslint-visitor-keys: "npm:^5.0.0"
|
||||
checksum: 10/9edcff478ba98f81f843b0c02874f3ef58c15b1001b2cf74d878416340c460a7c6e37442b4bee3ef226f0029131d620ee05a9ee3d19ec694ffa07fcf9a7a03ca
|
||||
checksum: 10/d149be4ac0e67c51097cdb7335d8e2d29b9dc0de173961c5cc550e938a00a3af28b1f8ecd7ad832fcfe489d1b11e36fdd394b6ea92115a7558123562e31a704f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9470,10 +9470,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fuse.js@npm:7.4.2":
|
||||
version: 7.4.2
|
||||
resolution: "fuse.js@npm:7.4.2"
|
||||
checksum: 10/1605bb929331056f9215a8f0a19b2d22615d9195e17794f41254bdeeb77b0145ed0ad6b6842227b329783a4ab53586d795924e7a4a3fb664295f365ef6e2405c
|
||||
"fuse.js@npm:7.5.0":
|
||||
version: 7.5.0
|
||||
resolution: "fuse.js@npm:7.5.0"
|
||||
checksum: 10/44dafab623a1144971ccbb380d6874ef6c51a73c85474016b0d47d6ad732d819cdb070f056149cd968b5304e7c0975f1890aaf98f3c4def2e9c21aebe4d62e23
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9971,7 +9971,7 @@ __metadata:
|
||||
"@lit/context": "npm:1.1.6"
|
||||
"@lit/reactive-element": "npm:2.1.2"
|
||||
"@lit/task": "npm:1.0.3"
|
||||
"@lokalise/node-api": "npm:16.0.0"
|
||||
"@lokalise/node-api": "npm:16.1.0"
|
||||
"@material/mwc-formfield": "patch:@material/mwc-formfield@npm%3A0.27.0#~/.yarn/patches/@material-mwc-formfield-npm-0.27.0-9528cb60f6.patch"
|
||||
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
|
||||
"@material/web": "npm:2.4.1"
|
||||
@@ -10036,7 +10036,7 @@ __metadata:
|
||||
eslint-plugin-wc: "npm:3.1.0"
|
||||
fancy-log: "npm:2.0.0"
|
||||
fs-extra: "npm:11.3.6"
|
||||
fuse.js: "npm:7.4.2"
|
||||
fuse.js: "npm:7.5.0"
|
||||
generate-license-file: "npm:4.2.1"
|
||||
glob: "npm:13.0.6"
|
||||
globals: "npm:17.7.0"
|
||||
@@ -10089,7 +10089,7 @@ __metadata:
|
||||
tinykeys: "patch:tinykeys@npm%3A4.0.0#~/.yarn/patches/tinykeys-npm-4.0.0-a6ca3fd771.patch"
|
||||
ts-lit-plugin: "npm:2.0.2"
|
||||
typescript: "npm:6.0.3"
|
||||
typescript-eslint: "npm:8.63.0"
|
||||
typescript-eslint: "npm:8.64.0"
|
||||
vite-tsconfig-paths: "npm:6.1.1"
|
||||
vitest: "npm:4.1.10"
|
||||
webpack-stats-plugin: "npm:1.1.3"
|
||||
@@ -15088,18 +15088,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript-eslint@npm:8.63.0":
|
||||
version: 8.63.0
|
||||
resolution: "typescript-eslint@npm:8.63.0"
|
||||
"typescript-eslint@npm:8.64.0":
|
||||
version: 8.64.0
|
||||
resolution: "typescript-eslint@npm:8.64.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin": "npm:8.63.0"
|
||||
"@typescript-eslint/parser": "npm:8.63.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.63.0"
|
||||
"@typescript-eslint/utils": "npm:8.63.0"
|
||||
"@typescript-eslint/eslint-plugin": "npm:8.64.0"
|
||||
"@typescript-eslint/parser": "npm:8.64.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.64.0"
|
||||
"@typescript-eslint/utils": "npm:8.64.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/ab0f3324cbf6ab910903977b9364138c67ccad35675962702bfbe052231440439dcdc50de0c01a1eb1ccecde796a4e117858b10166d9d1153b3b2d82c73a92ba
|
||||
checksum: 10/7812e25506003c2a5b395a51ca3c31929028485d191049e6a813e65829e79fcebb07251ba42c5d75af8d15233a5bbb44dd37138345787779eabe07b88fcf75ec
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user