Merge branch 'rc'

This commit is contained in:
Bram Kragten 2025-05-09 12:11:58 +02:00
commit 7f6ce97199
15 changed files with 204 additions and 63 deletions

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20250507.0"
version = "20250509.0"
license = "Apache-2.0"
license-files = ["LICENSE*"]
description = "The Home Assistant frontend"

View File

@ -90,7 +90,7 @@ export class HaDialog extends DialogBase {
}
.mdc-dialog__actions {
justify-content: var(--justify-action-buttons, flex-end);
padding-bottom: max(env(safe-area-inset-bottom), 24px);
padding: 12px 24px max(env(safe-area-inset-bottom), 12px) 24px;
}
.mdc-dialog__actions span:nth-child(1) {
flex: var(--secondary-action-button-flex, unset);
@ -107,9 +107,6 @@ export class HaDialog extends DialogBase {
.mdc-dialog__title:has(span) {
padding: 12px 12px 0;
}
.mdc-dialog__actions {
padding: 12px 24px 12px 24px;
}
.mdc-dialog__title::before {
content: unset;
}

View File

@ -83,7 +83,7 @@ export class HaServiceControl extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean, reflect: true }) public narrow = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: "show-advanced", type: Boolean }) public showAdvanced =
false;
@ -895,6 +895,9 @@ export class HaServiceControl extends LitElement {
ha-settings-row {
padding: var(--service-control-padding, 0 16px);
}
ha-settings-row[narrow] {
padding-bottom: 8px;
}
ha-settings-row {
--settings-row-content-width: 100%;
--settings-row-prefix-display: contents;
@ -916,7 +919,7 @@ export class HaServiceControl extends LitElement {
margin: var(--service-control-padding, 0 16px);
padding: 16px 0;
}
:host([hidePicker]) p {
:host([hide-picker]) p {
padding-top: 0;
}
.checkbox-spacer {

View File

@ -24,9 +24,10 @@ import {
customElement,
eventOptions,
property,
state,
query,
state,
} from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { storage } from "../common/decorators/storage";
import { fireEvent } from "../common/dom/fire_event";
@ -45,13 +46,13 @@ import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo, Route } from "../types";
import "./ha-icon";
import "./ha-icon-button";
import "./ha-md-list";
import "./ha-md-list-item";
import type { HaMdListItem } from "./ha-md-list-item";
import "./ha-menu-button";
import "./ha-sortable";
import "./ha-svg-icon";
import "./user/ha-user-badge";
import "./ha-md-list";
import "./ha-md-list-item";
import type { HaMdListItem } from "./ha-md-list-item";
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
@ -407,6 +408,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
// prettier-ignore
return html`
<ha-sortable .disabled=${!this.editMode} draggable-selector=".draggable" @item-moved=${this._panelMoved}>
<ha-md-list
class="ha-scrollbar"
@focusin=${this._listboxFocusIn}
@ -420,11 +422,16 @@ class HaSidebar extends SubscribeMixin(LitElement) {
${this._renderSpacer()}
${this._renderPanels(afterSpacer, selectedPanel)}
${this._renderExternalConfiguration()}
</ha-md-list>
</ha-md-list>
</ha-sortable>
`;
}
private _renderPanels(panels: PanelInfo[], selectedPanel: string) {
private _renderPanels(
panels: PanelInfo[],
selectedPanel: string,
sortable = false
) {
return panels.map((panel) =>
this._renderPanel(
panel.url_path,
@ -437,17 +444,26 @@ class HaSidebar extends SubscribeMixin(LitElement) {
: panel.url_path in PANEL_ICONS
? PANEL_ICONS[panel.url_path]
: undefined,
selectedPanel
selectedPanel,
sortable
)
);
}
private _renderPanelsEdit(beforeSpacer: PanelInfo[], selectedPanel: string) {
return html`
${this._renderPanels(beforeSpacer, selectedPanel, true)}
${this._renderSpacer()}${this._renderHiddenPanels()}
`;
}
private _renderPanel(
urlPath: string,
title: string | null,
icon: string | null | undefined,
iconPath: string | null | undefined,
selectedPanel: string
selectedPanel: string,
sortable = false
) {
return urlPath === "config"
? this._renderConfiguration(title, selectedPanel)
@ -455,7 +471,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
<ha-md-list-item
.href=${this.editMode ? undefined : `/${urlPath}`}
type="link"
class=${selectedPanel === urlPath ? "selected" : ""}
class=${classMap({
selected: selectedPanel === urlPath,
draggable: this.editMode && sortable,
})}
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
@ -496,15 +515,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
this._panelOrder = panelOrder;
}
private _renderPanelsEdit(beforeSpacer: PanelInfo[], selectedPanel: string) {
return html`
<ha-sortable .disabled=${!this.editMode} @item-moved=${this._panelMoved}
><div>${this._renderPanels(beforeSpacer, selectedPanel)}</div>
</ha-sortable>
${this._renderSpacer()}${this._renderHiddenPanels()}
`;
}
private _renderHiddenPanels() {
return html`${this._hiddenPanels.length
? html`${this._hiddenPanels.map((url) => {

View File

@ -316,6 +316,12 @@ class StepFlowCreateEntry extends LitElement {
overflow-y: auto;
flex-direction: column;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
.devices {
/* header - margin content - footer */
max-height: calc(100vh - 52px - 20px - 52px);
}
}
.device {
border: 1px solid var(--divider-color);
padding: 6px;
@ -352,11 +358,6 @@ class StepFlowCreateEntry extends LitElement {
margin-inline-start: auto;
margin-inline-end: initial;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
.device {
width: 100%;
}
}
.error {
color: var(--error-color);
}

View File

@ -31,7 +31,7 @@ const DEFAULT_AGENTS = [];
class HaBackupConfigAgents extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public cloudStatus!: CloudStatus;
@property({ attribute: false }) public cloudStatus?: CloudStatus;
@property({ attribute: false }) public agents: BackupAgent[] = [];
@ -48,7 +48,10 @@ class HaBackupConfigAgents extends LitElement {
private _description(agentId: string) {
if (agentId === CLOUD_AGENT) {
if (this.cloudStatus.logged_in && !this.cloudStatus.active_subscription) {
if (
this.cloudStatus?.logged_in &&
!this.cloudStatus.active_subscription
) {
return this.hass.localize(
"ui.panel.config.backup.agents.cloud_agent_no_subcription"
);
@ -106,17 +109,17 @@ class HaBackupConfigAgents extends LitElement {
}
private _availableAgents = memoizeOne(
(agents: BackupAgent[], cloudStatus: CloudStatus) =>
(agents: BackupAgent[], cloudStatus?: CloudStatus) =>
agents.filter(
(agent) => agent.agent_id !== CLOUD_AGENT || cloudStatus.logged_in
(agent) => agent.agent_id !== CLOUD_AGENT || cloudStatus?.logged_in
)
);
private _unavailableAgents = memoizeOne(
(
agents: BackupAgent[],
cloudStatus: CloudStatus,
selectedAgentIds: string[]
selectedAgentIds: string[],
cloudStatus?: CloudStatus
) => {
const availableAgentIds = this._availableAgents(agents, cloudStatus).map(
(agent) => agent.agent_id
@ -167,8 +170,8 @@ class HaBackupConfigAgents extends LitElement {
);
const unavailableAgents = this._unavailableAgents(
this.agents,
this.cloudStatus,
this._value
this._value,
this.cloudStatus
);
const allAgents = [...availableAgents, ...unavailableAgents];
@ -187,7 +190,7 @@ class HaBackupConfigAgents extends LitElement {
const description = this._description(agentId);
const noCloudSubscription =
agentId === CLOUD_AGENT &&
this.cloudStatus.logged_in &&
this.cloudStatus?.logged_in &&
!this.cloudStatus.active_subscription;
return html`

View File

@ -1,5 +1,5 @@
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { clamp } from "../../../../../common/number/clamp";
import "../../../../../components/ha-expansion-panel";
@ -8,6 +8,7 @@ import "../../../../../components/ha-md-select";
import type { HaMdSelect } from "../../../../../components/ha-md-select";
import "../../../../../components/ha-md-select-option";
import "../../../../../components/ha-md-textfield";
import type { HaMdTextfield } from "../../../../../components/ha-md-textfield";
import type { BackupConfig, Retention } from "../../../../../data/backup";
import type { HomeAssistant } from "../../../../../types";
@ -54,16 +55,21 @@ class HaBackupConfigRetention extends LitElement {
@state() private _value = 3;
@query("#value") private _customValueField?: HaMdTextfield;
@query("#type") private _customTypeField?: HaMdSelect;
private _configLoaded = false;
private presetOptions = [
RetentionPreset.COPIES_3,
RetentionPreset.FOREVER,
RetentionPreset.CUSTOM,
];
public willUpdate(properties: PropertyValues) {
super.willUpdate(properties);
if (!this.hasUpdated) {
public willUpdate() {
if (!this._configLoaded && this.retention !== undefined) {
this._configLoaded = true;
if (!this.retention) {
this._preset = RetentionPreset.GLOBAL;
} else if (
@ -94,6 +100,10 @@ class HaBackupConfigRetention extends LitElement {
}
protected render() {
if (!this._configLoaded) {
return nothing;
}
return html`
<ha-md-list-item>
<span slot="headline">
@ -206,10 +216,12 @@ class HaBackupConfigRetention extends LitElement {
const clamped = clamp(value, MIN_VALUE, MAX_VALUE);
target.value = clamped.toString();
const type = this._customTypeField?.value;
fireEvent(this, "value-changed", {
value: {
copies: this._type === "copies" ? clamped : null,
days: this._type === "days" ? clamped : null,
copies: type === "copies" ? clamped : null,
days: type === "days" ? clamped : null,
},
});
}
@ -219,10 +231,12 @@ class HaBackupConfigRetention extends LitElement {
const target = ev.currentTarget as HaMdSelect;
const type = target.value as "copies" | "days";
const value = this._customValueField?.value;
fireEvent(this, "value-changed", {
value: {
copies: type === "copies" ? this._value : null,
days: type === "days" ? this._value : null,
copies: type === "copies" ? Number(value) : null,
days: type === "days" ? Number(value) : null,
},
});
}

View File

@ -125,8 +125,10 @@ class HaConfigBackupDetails extends LitElement {
{ location: agentName }
)}
.hass=${this.hass}
.retention=${this.config?.agents[this.agentId]
?.retention}
.retention=${!this.config
? undefined
: this.config.agents[this.agentId]?.retention ||
null}
@value-changed=${this._retentionChanged}
></ha-backup-config-retention>`}
</ha-card>

View File

@ -41,7 +41,7 @@ import { brandsUrl } from "../../../util/brands-url";
class HaConfigBackupSettings extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public cloudStatus!: CloudStatus;
@property({ attribute: false }) public cloudStatus?: CloudStatus;
@property({ type: Boolean }) public narrow = false;
@ -244,7 +244,7 @@ class HaConfigBackupSettings extends LitElement {
`
: nothing}
</div>
${!this.cloudStatus.logged_in
${!this.cloudStatus?.logged_in
? html`<ha-card class="cloud-info">
<div class="cloud-header">
<img
@ -316,7 +316,7 @@ class HaConfigBackupSettings extends LitElement {
</div>
</ha-card>
${supervisor
? html` <ha-card>
? html`<ha-card>
<div class="card-header">
${this.hass.localize(
"ui.panel.config.backup.settings.addon_update_backup.title"

View File

@ -29,9 +29,8 @@ class DialogBluetoothDeviceInfo extends LitElement implements HassDialog {
}
public showDataAsHex(bytestring: string): string {
return Array.from(new TextEncoder().encode(bytestring))
.map((byte) => byte.toString(16).toUpperCase().padStart(2, "0"))
.join(" ");
const bytes = bytestring.match(/.{2}/g) ?? [];
return bytes.map((byte) => `0x${byte.toUpperCase()}`).join(" ");
}
private async _copyToClipboard(): Promise<void> {

View File

@ -9,6 +9,7 @@ import type { SSDPDiscoveryInfoDialogParams } from "./show-dialog-ssdp-discovery
import "../../../../../components/ha-button";
import { showToast } from "../../../../../util/toast";
import { copyToClipboard } from "../../../../../common/util/copy-clipboard";
import { showSSDPRawDataDialog } from "./show-dialog-ssdp-raw-data";
@customElement("dialog-ssdp-device-info")
class DialogSSDPDiscoveryInfo extends LitElement implements HassDialog {
@ -39,6 +40,16 @@ class DialogSSDPDiscoveryInfo extends LitElement implements HassDialog {
});
}
private _showRawData(key: string, data: Record<string, unknown>) {
return (e: Event) => {
e.preventDefault();
showSSDPRawDataDialog(this, {
key,
data,
});
};
}
protected render(): TemplateResult | typeof nothing {
if (!this._params) {
return nothing;
@ -83,7 +94,20 @@ class DialogSSDPDiscoveryInfo extends LitElement implements HassDialog {
([key, value]) => html`
<tr>
<td><b>${key}</b></td>
<td>${value}</td>
<td>
${typeof value === "object" && value !== null
? html`<a
href="#"
@click=${this._showRawData(
key,
value as Record<string, unknown>
)}
>${this.hass.localize(
"ui.panel.config.ssdp.show_raw_data"
)}</a
>`
: value}
</td>
</tr>
`
)}

View File

@ -0,0 +1,68 @@
import { LitElement, html, nothing, css } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { TemplateResult } from "lit";
import { dump } from "js-yaml";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { HassDialog } from "../../../../../dialogs/make-dialog-manager";
import { createCloseHeading } from "../../../../../components/ha-dialog";
import type { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-code-editor";
export interface SSDPRawDataDialogParams {
key: string;
data: Record<string, unknown>;
}
@customElement("dialog-ssdp-raw-data")
class DialogSSDPRawData extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: SSDPRawDataDialogParams;
public async showDialog(params: SSDPRawDataDialogParams): Promise<void> {
this._params = params;
}
public closeDialog(): boolean {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
return true;
}
protected render(): TemplateResult | typeof nothing {
if (!this._params) {
return nothing;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
`${this.hass.localize("ui.panel.config.ssdp.raw_data_title")}: ${this._params.key}`
)}
>
<ha-code-editor
mode="yaml"
.value=${dump(this._params.data)}
readonly
autofocus
></ha-code-editor>
</ha-dialog>
`;
}
static styles = css`
ha-code-editor {
--code-mirror-max-height: 60vh;
--code-mirror-height: auto;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"dialog-ssdp-raw-data": DialogSSDPRawData;
}
}

View File

@ -0,0 +1,19 @@
import { fireEvent } from "../../../../../common/dom/fire_event";
export interface SSDPRawDataDialogParams {
key: string;
data: Record<string, unknown>;
}
export const loadSSDPRawDataDialog = () => import("./dialog-ssdp-raw-data");
export const showSSDPRawDataDialog = (
element: HTMLElement,
ssdpRawDataDialogParams: SSDPRawDataDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-ssdp-raw-data",
dialogImport: loadSSDPRawDataDialog,
dialogParams: ssdpRawDataDialogParams,
});
};

View File

@ -1,7 +1,7 @@
import { css } from "lit";
export const sidebarEditStyle = css`
ha-sortable ha-md-list-item:nth-child(2n) {
ha-sortable ha-md-list-item.draggable:nth-child(2n) {
animation-name: keyframes1;
animation-iteration-count: infinite;
transform-origin: 50% 10%;
@ -9,7 +9,7 @@ export const sidebarEditStyle = css`
animation-duration: 0.25s;
}
ha-sortable ha-md-list-item:nth-child(2n-1) {
ha-sortable ha-md-list-item.draggable:nth-child(2n-1) {
animation-name: keyframes2;
animation-iteration-count: infinite;
animation-direction: alternate;
@ -18,8 +18,7 @@ export const sidebarEditStyle = css`
animation-duration: 0.33s;
}
ha-sortable ha-md-list-item {
height: 48px;
ha-sortable ha-md-list-item.draggable {
cursor: grab;
}

View File

@ -5586,7 +5586,9 @@
"upnp": "Universal Plug and Play (UPnP)",
"discovery_information": "Discovery information",
"copy_to_clipboard": "Copy to clipboard",
"no_devices_found": "No matching SSDP/UPnP discoveries found"
"no_devices_found": "No matching SSDP/UPnP discoveries found",
"show_raw_data": "Show raw data",
"raw_data_title": "Raw data"
},
"zeroconf": {
"name": "Name",