Compare commits

..

14 Commits

Author SHA1 Message Date
Aidan Timson
446c2e0ac3 Update src/panels/config/developer-tools/action/developer-tools-action.ts
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-04-30 13:36:51 +01:00
Aidan Timson
cf6a1d46d0 Move toggle group to right of title 2026-04-30 12:31:22 +01:00
Aidan Timson
fcaa42705f Types 2026-04-30 12:29:03 +01:00
Aidan Timson
41515f642b Rename mixin 2026-04-30 12:08:18 +01:00
Aidan Timson
4fbb9e7a02 Remove unneeded spread 2026-04-30 12:00:39 +01:00
Aidan Timson
7aa9805ef0 View transition between modes 2026-04-30 11:41:00 +01:00
Aidan Timson
3ca9cd93a5 Use min height mixin to preserve height when switching to yaml mode 2026-04-30 11:38:48 +01:00
Aidan Timson
c90a29f082 Improve content layout 2026-04-30 11:23:15 +01:00
Aidan Timson
d4fea44844 Remove memo 2026-04-30 11:13:55 +01:00
Aidan Timson
54a5e480c0 Toggle buttons 2026-04-30 11:10:55 +01:00
Aidan Timson
2f04ca9647 Center 2026-04-30 10:54:31 +01:00
Aidan Timson
b3f334add4 More standard 2026-04-30 10:52:37 +01:00
Aidan Timson
cc759df646 Move items into card 2026-04-30 10:46:46 +01:00
Aidan Timson
d1b2d4e9f3 Remove background (and borders) of devtools actions button row 2026-04-30 10:29:08 +01:00
13 changed files with 337 additions and 514 deletions

View File

@@ -189,20 +189,6 @@ export const updateBackupConfig = (
config: BackupMutableConfig
) => hass.callWS({ type: "backup/config/update", ...config });
export const saveBackupConfig = (hass: HomeAssistant, config: BackupConfig) =>
updateBackupConfig(hass, {
create_backup: {
agent_ids: config.create_backup.agent_ids,
include_folders: config.create_backup.include_folders ?? [],
include_database: config.create_backup.include_database,
include_addons: config.create_backup.include_addons ?? [],
include_all_addons: config.create_backup.include_all_addons,
password: config.create_backup.password,
},
retention: config.retention,
schedule: config.schedule,
});
export const getBackupDownloadUrl = (
id: string,
agentId: string,

View File

@@ -0,0 +1,110 @@
import { ResizeController } from "@lit-labs/observers/resize-controller";
import type { LitElement, PropertyValues } from "lit";
import { state } from "lit/decorators";
import type { StyleInfo } from "lit/directives/style-map";
import type { Constructor } from "../types";
/**
* Public interface added by {@link MatchMinHeightMixin}.
*
* Declared separately so consumers can reference the mixin's contributed
* members in their own type annotations, per the Lit mixin authoring guide.
*/
export declare class MatchMinHeightMixinInterface {
/** Most recently observed height of `matchMinHeightTarget`, in pixels. */
protected _matchedMinHeight?: number;
/**
* `StyleInfo` exposing the matched height as a `min-height` declaration.
* Pass to `styleMap` to keep a layout at least as tall as the target
* element. Empty until a height has been observed.
*/
protected get _matchMinHeightStyle(): StyleInfo;
/**
* Element whose height should be matched as a `min-height` floor. Override
* with a getter that returns a `@query` result. Return `null` to pause
* observation (e.g. while the element is not rendered).
*/
protected get matchMinHeightTarget(): HTMLElement | null;
}
/**
* Mixin that observes a target element's height and exposes it as a
* `min-height` style. Useful for keeping a sibling layout (e.g. a YAML
* editor) at least as tall as another (e.g. a UI form) to avoid content
* shift when toggling between them.
*
* Subclasses override `matchMinHeightTarget` (typically returning a
* `@query`-decorated element) to specify which element to observe.
*/
export const MatchMinHeightMixin = <T extends Constructor<LitElement>>(
superClass: T
) => {
class MatchMinHeightMixinClass extends superClass {
@state() protected _matchedMinHeight?: number;
private _matchTarget: HTMLElement | null = null;
private _matchResize = new ResizeController(this, {
target: null,
callback: (entries) => {
const height = entries[0]?.contentRect.height;
if (height) {
this._matchedMinHeight = height;
}
},
});
private static readonly DEFAULT_MATCH_TARGET: HTMLElement | null = null;
protected get matchMinHeightTarget(): HTMLElement | null {
return MatchMinHeightMixinClass.DEFAULT_MATCH_TARGET;
}
protected get _matchMinHeightStyle(): StyleInfo {
return this._matchedMinHeight !== undefined
? { "min-height": `${this._matchedMinHeight}px` }
: {};
}
protected firstUpdated(changedProperties: PropertyValues<this>) {
super.firstUpdated?.(changedProperties);
this._attachMatchTarget();
}
protected updated(changedProperties: PropertyValues<this>) {
super.updated?.(changedProperties);
this._attachMatchTarget();
}
public disconnectedCallback() {
this._detachMatchTarget();
super.disconnectedCallback();
}
private _attachMatchTarget() {
const element = this.matchMinHeightTarget;
if (element === this._matchTarget) {
return;
}
this._detachMatchTarget();
if (!element) {
return;
}
this._matchTarget = element;
this._matchResize.observe(element);
}
private _detachMatchTarget() {
if (!this._matchTarget) {
return;
}
this._matchResize.unobserve?.(this._matchTarget);
this._matchTarget = null;
}
}
return MatchMinHeightMixinClass as unknown as Constructor<MatchMinHeightMixinInterface> &
T;
};

View File

@@ -1,123 +0,0 @@
import { mdiPuzzle } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-svg-icon";
import {
getSupervisorUpdateConfig,
type SupervisorUpdateConfig,
} from "../../../../../data/supervisor/update";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
@customElement("ha-backup-overview-app-update-backup")
class HaBackupOverviewAppUpdateBackup extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _supervisorUpdateConfig?: SupervisorUpdateConfig;
protected firstUpdated() {
this._fetchSupervisorUpdateConfig();
}
public connectedCallback() {
super.connectedCallback();
if (this.hasUpdated) {
this._fetchSupervisorUpdateConfig();
}
}
private async _fetchSupervisorUpdateConfig() {
try {
this._supervisorUpdateConfig = await getSupervisorUpdateConfig(this.hass);
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
}
}
private _appUpdateBackupDescription() {
if (!this._supervisorUpdateConfig) {
return this.hass.localize(
"ui.panel.config.backup.settings.app_update_backup.local_only"
);
}
if (!this._supervisorUpdateConfig.add_on_backup_before_update) {
return this.hass.localize(
"ui.panel.config.backup.schedule.update_preference.skip_backups"
);
}
const copies =
this._supervisorUpdateConfig.add_on_backup_retain_copies || 1;
return `${this.hass.localize(
"ui.panel.config.backup.schedule.update_preference.backup_before_update"
)} ${this.hass.localize(
"ui.panel.config.backup.overview.settings.schedule_copies_backups",
{ count: copies }
)}`;
}
protected render() {
return html`
<ha-card>
<div class="card-header">
${this.hass.localize(
"ui.panel.config.backup.overview.app_update_backup.title"
)}
</div>
<div class="card-content">
<ha-md-list>
<ha-md-list-item
type="link"
href="/config/backup/app-update-backups"
>
<ha-svg-icon slot="start" .path=${mdiPuzzle}></ha-svg-icon>
<div slot="headline">${this._appUpdateBackupDescription()}</div>
<div slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.overview.app_update_backup.description"
)}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
</ha-md-list>
</div>
</ha-card>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.card-header {
padding-bottom: 8px;
}
.card-content {
padding-left: 0;
padding-right: 0;
padding-top: 0;
}
ha-md-list {
padding-top: 0;
padding-bottom: 0;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-backup-overview-app-update-backup": HaBackupOverviewAppUpdateBackup;
}
}

View File

@@ -1,154 +0,0 @@
import { css, html, LitElement, nothing } from "lit";
import type { PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { debounce } from "../../../common/util/debounce";
import "../../../components/ha-alert";
import "../../../components/ha-card";
import {
getSupervisorUpdateConfig,
updateSupervisorUpdateConfig,
type SupervisorUpdateConfig,
} from "../../../data/supervisor/update";
import "../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../types";
import "./components/config/ha-backup-config-addon";
@customElement("ha-config-backup-app-update-backups")
class HaConfigBackupAppUpdateBackups extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@state() private _supervisorUpdateConfig?: SupervisorUpdateConfig;
@state() private _error?: string;
protected willUpdate(changedProps: PropertyValues<this>): void {
super.willUpdate(changedProps);
if (
!this.hasUpdated &&
this.hass &&
isComponentLoaded(this.hass.config, "hassio")
) {
this._getSupervisorUpdateConfig();
}
}
protected render() {
return html`
<hass-subpage
back-path="/config/backup/overview"
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize(
"ui.panel.config.backup.app_update_backups.header"
)}
>
<div class="content">
<ha-card>
<div class="card-content">
<p>
${this.hass.localize(
"ui.panel.config.backup.settings.app_update_backup.description"
)}
</p>
<p>
${this.hass.localize(
"ui.panel.config.backup.settings.app_update_backup.local_only"
)}
</p>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
<ha-backup-config-addon
.hass=${this.hass}
.supervisorUpdateConfig=${this._supervisorUpdateConfig}
@update-config-changed=${this._supervisorUpdateConfigChanged}
></ha-backup-config-addon>
</div>
</ha-card>
</div>
</hass-subpage>
`;
}
private async _getSupervisorUpdateConfig() {
try {
this._supervisorUpdateConfig = await getSupervisorUpdateConfig(this.hass);
this._error = undefined;
} catch (err: any) {
// eslint-disable-next-line no-console
console.error(err);
this._error = this.hass.localize(
"ui.panel.config.backup.settings.app_update_backup.error_load",
{
error: err?.message || err,
}
);
}
}
private async _supervisorUpdateConfigChanged(ev) {
const config = ev.detail.value as SupervisorUpdateConfig;
this._supervisorUpdateConfig = {
...this._supervisorUpdateConfig,
...config,
} as SupervisorUpdateConfig;
this._debounceSaveSupervisorUpdateConfig();
}
private _debounceSaveSupervisorUpdateConfig = debounce(
() => this._saveSupervisorUpdateConfig(),
500
);
private async _saveSupervisorUpdateConfig() {
if (!this._supervisorUpdateConfig) {
return;
}
try {
await updateSupervisorUpdateConfig(
this.hass,
this._supervisorUpdateConfig
);
this._error = undefined;
} catch (err: any) {
// eslint-disable-next-line no-console
console.error(err);
this._error = this.hass.localize(
"ui.panel.config.backup.settings.app_update_backup.error_save",
{
error: err?.message || err?.toString(),
}
);
}
}
static styles = css`
p {
color: var(--secondary-text-color);
}
.content {
padding: 28px 20px 0;
max-width: 690px;
margin: 0 auto;
gap: var(--ha-space-6);
display: flex;
flex-direction: column;
margin-bottom: 24px;
}
.card-content {
padding-bottom: 0;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-backup-app-update-backups": HaConfigBackupAppUpdateBackups;
}
}

View File

@@ -1,9 +1,8 @@
import { mdiDotsVertical, mdiPlus, mdiUpload } from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { debounce } from "../../../common/util/debounce";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-dropdown";
@@ -24,7 +23,6 @@ import {
computeBackupAgentName,
generateBackup,
generateBackupWithAutomaticSettings,
saveBackupConfig,
} from "../../../data/backup";
import type { ManagerStateEvent } from "../../../data/backup_manager";
import type { CloudStatus } from "../../../data/cloud";
@@ -34,12 +32,10 @@ import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types";
import { showAlertDialog } from "../../lovelace/custom-card-helpers";
import "./components/overview/ha-backup-overview-backups";
import "./components/overview/ha-backup-overview-app-update-backup";
import "./components/overview/ha-backup-overview-onboarding";
import "./components/overview/ha-backup-overview-progress";
import "./components/overview/ha-backup-overview-settings";
import "./components/overview/ha-backup-overview-summary";
import "./components/config/ha-backup-config-encryption-key";
import { showBackupOnboardingDialog } from "./dialogs/show-dialog-backup_onboarding";
import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup";
import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup";
@@ -72,54 +68,10 @@ class HaConfigBackupOverview extends LitElement {
{ uploaded_bytes: number; total_bytes: number }
> = {};
@state() private _config?: BackupConfig;
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (changedProperties.has("config") && !this._config) {
this._config = this.config;
}
}
public connectedCallback(): void {
super.connectedCallback();
// Update config when the page is displayed (e.g. when coming back from a settings page)
this._config = this.config;
}
private _uploadBackup = async () => {
await showUploadBackupDialog(this, {});
};
private _encryptionKeyChanged(ev) {
if (!this._config) {
return;
}
const password = ev.detail.value as string;
this._config = {
...this._config,
create_backup: {
...this._config.create_backup,
password,
},
};
this._debounceSaveConfig();
}
private _debounceSaveConfig = debounce(() => this._saveConfig(), 500);
private async _saveConfig() {
if (!this._config) {
return;
}
await saveBackupConfig(this.hass, this._config);
fireEvent(this, "ha-refresh-backup-config");
}
private _handleOnboardingButtonClick(ev) {
ev.stopPropagation();
this._setupAutomaticBackup(true);
@@ -282,41 +234,13 @@ class HaConfigBackupOverview extends LitElement {
.backups=${this.backups}
></ha-backup-overview-backups>
${!this._needsOnboarding && this._config
${!this._needsOnboarding && this.config
? html`
<ha-card>
<div class="card-header">
${this.hass.localize(
"ui.panel.config.backup.settings.encryption_key.title"
)}
</div>
<div class="card-content">
<p>
${this.hass.localize(
"ui.panel.config.backup.settings.encryption_key.description"
)}
</p>
<ha-backup-config-encryption-key
.hass=${this.hass}
.value=${this._config.create_backup.password}
@value-changed=${this._encryptionKeyChanged}
></ha-backup-config-encryption-key>
</div>
</ha-card>
<ha-backup-overview-settings
.hass=${this.hass}
.config=${this._config}
.config=${this.config!}
.agents=${this.agents}
></ha-backup-overview-settings>
${this.hass.config.components.includes("hassio")
? html`
<ha-backup-overview-app-update-backup
.hass=${this.hass}
></ha-backup-overview-app-update-backup>
`
: nothing}
`
: nothing}
</div>
@@ -346,10 +270,6 @@ class HaConfigBackupOverview extends LitElement {
return [
haStyle,
css`
p {
color: var(--secondary-text-color);
}
.content {
padding: 28px 20px 0;
max-width: 690px;
@@ -363,6 +283,10 @@ class HaConfigBackupOverview extends LitElement {
display: flex;
justify-content: flex-end;
}
.card-content {
padding-left: 0;
padding-right: 0;
}
.loading {
display: flex;
}

View File

@@ -16,7 +16,7 @@ import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next";
import "../../../components/ha-svg-icon";
import type { BackupAgent, BackupConfig } from "../../../data/backup";
import { saveBackupConfig } from "../../../data/backup";
import { updateBackupConfig } from "../../../data/backup";
import type { CloudStatus } from "../../../data/cloud";
import {
getSupervisorUpdateConfig,
@@ -27,9 +27,11 @@ import "../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { documentationUrl } from "../../../util/documentation-url";
import "./components/config/ha-backup-config-addon";
import "./components/config/ha-backup-config-agents";
import "./components/config/ha-backup-config-data";
import type { BackupConfigData } from "./components/config/ha-backup-config-data";
import "./components/config/ha-backup-config-encryption-key";
import "./components/config/ha-backup-config-schedule";
import type { BackupConfigSchedule } from "./components/config/ha-backup-config-schedule";
import { showLocalBackupLocationDialog } from "./dialogs/show-dialog-local-backup-location";
@@ -77,7 +79,7 @@ class HaConfigBackupSettings extends LitElement {
// eslint-disable-next-line no-console
console.error(err);
this._supervisorUpdateConfigError = this.hass.localize(
"ui.panel.config.backup.settings.schedule.error_load",
"ui.panel.config.backup.settings.app_update_backup.error_load",
{
error: err?.message || err,
}
@@ -313,6 +315,57 @@ class HaConfigBackupSettings extends LitElement {
: nothing}
</div>
</ha-card>
${supervisor
? html`<ha-card>
<div class="card-header">
${this.hass.localize(
"ui.panel.config.backup.settings.app_update_backup.title"
)}
</div>
<div class="card-content">
<p>
${this.hass.localize(
"ui.panel.config.backup.settings.app_update_backup.description"
)}
</p>
<p>
${this.hass.localize(
"ui.panel.config.backup.settings.app_update_backup.local_only"
)}
</p>
${this._supervisorUpdateConfigError
? html`<ha-alert alert-type="error">
${this._supervisorUpdateConfigError}
</ha-alert>`
: nothing}
<ha-backup-config-addon
.hass=${this.hass}
.supervisorUpdateConfig=${this._supervisorUpdateConfig}
@update-config-changed=${this
._supervisorUpdateConfigChanged}
></ha-backup-config-addon>
</div>
</ha-card>`
: nothing}
<ha-card>
<div class="card-header">
${this.hass.localize(
"ui.panel.config.backup.settings.encryption_key.title"
)}
</div>
<div class="card-content">
<p>
${this.hass.localize(
"ui.panel.config.backup.settings.encryption_key.description"
)}
</p>
<ha-backup-config-encryption-key
.hass=${this.hass}
.value=${this._config.create_backup.password}
@value-changed=${this._encryptionKeyChanged}
></ha-backup-config-encryption-key>
</div>
</ha-card>
</div>
</hass-subpage>
`;
@@ -385,6 +438,18 @@ class HaConfigBackupSettings extends LitElement {
this._debounceSave();
}
private _encryptionKeyChanged(ev) {
const password = ev.detail.value as string;
this._config = {
...this._config!,
create_backup: {
...this._config!.create_backup,
password: password,
},
};
this._debounceSave();
}
private _debounceSaveSupervisorUpdateConfig = debounce(
() => this._saveSupervisorUpdateConfig(),
500
@@ -403,7 +468,7 @@ class HaConfigBackupSettings extends LitElement {
// eslint-disable-next-line no-console
console.error(err);
this._supervisorUpdateConfigError = this.hass.localize(
"ui.panel.config.backup.settings.schedule.error_save",
"ui.panel.config.backup.settings.app_update_backup.error_save",
{
error: err?.message || err?.toString(),
}
@@ -414,7 +479,18 @@ class HaConfigBackupSettings extends LitElement {
private _debounceSave = debounce(() => this._save(), 500);
private async _save() {
await saveBackupConfig(this.hass, this._config!);
await updateBackupConfig(this.hass, {
create_backup: {
agent_ids: this._config!.create_backup.agent_ids,
include_folders: this._config!.create_backup.include_folders ?? [],
include_database: this._config!.create_backup.include_database,
include_addons: this._config!.create_backup.include_addons ?? [],
include_all_addons: this._config!.create_backup.include_all_addons,
password: this._config!.create_backup.password,
},
retention: this._config!.retention,
schedule: this._config!.schedule,
});
fireEvent(this, "ha-refresh-backup-config");
}

View File

@@ -125,11 +125,6 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
load: () => import("./ha-config-backup-settings"),
cache: true,
},
"app-update-backups": {
tag: "ha-config-backup-app-update-backups",
load: () => import("./ha-config-backup-app-update-backups"),
cache: true,
},
location: {
tag: "ha-config-backup-location",
load: () => import("./ha-config-backup-location"),

View File

@@ -5,9 +5,11 @@ import { dump, JSON_SCHEMA, load } from "js-yaml";
import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { until } from "lit/directives/until";
import memoizeOne from "memoize-one";
import { storage } from "../../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { computeObjectId } from "../../../../common/entity/compute_object_id";
import {
@@ -23,6 +25,7 @@ import { showToast } from "../../../../util/toast";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import "../../../../components/ha-button-toggle-group";
import "../../../../components/ha-card";
import "../../../../components/buttons/ha-progress-button";
import "../../../../components/ha-expansion-panel";
@@ -39,12 +42,14 @@ import {
serviceCallWillDisconnect,
} from "../../../../data/service";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import type { HomeAssistant, ToggleButton } from "../../../../types";
import { documentationUrl } from "../../../../util/documentation-url";
import { resolveMediaSource } from "../../../../data/media_source";
import { MatchMinHeightMixin } from "../../../../mixins/match-min-height-mixin";
import { withViewTransition } from "../../../../common/util/view-transition";
@customElement("developer-tools-action")
class HaPanelDevAction extends LitElement {
class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@@ -80,6 +85,12 @@ class HaPanelDevAction extends LitElement {
@query("#yaml-editor") private _yamlEditor?: HaYamlEditor;
@query(".ui-mode-content") private _uiModeContent?: HTMLElement;
protected get matchMinHeightTarget(): HTMLElement | null {
return this._yamlMode ? null : (this._uiModeContent ?? null);
}
protected willUpdate() {
if (
!this.hasUpdated &&
@@ -117,6 +128,21 @@ class HaPanelDevAction extends LitElement {
this._serviceData?.action
);
const modeButtons: ToggleButton[] = [
{
label: this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.ui_mode"
),
value: "ui",
},
{
label: this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.yaml_mode"
),
value: "yaml",
},
];
const domain = this._serviceData?.action
? computeDomain(this._serviceData?.action)
: undefined;
@@ -132,14 +158,34 @@ class HaPanelDevAction extends LitElement {
return html`
<div class="content">
<p>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.description"
)}
</p>
<ha-card>
<div class="card-header">
<div class="header-row">
<div class="header-title">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.title"
)}
</div>
<ha-button-toggle-group
size="small"
class="yaml-mode-toggle"
.buttons=${modeButtons}
.active=${this._yamlMode ? "yaml" : "ui"}
.disabled=${!this._uiAvailable}
@value-changed=${this._modeChanged}
></ha-button-toggle-group>
</div>
<p class="secondary">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.description"
)}
</p>
</div>
${this._yamlMode
? html`<div class="card-content">
? html`<div
class="card-content"
style=${styleMap(this._matchMinHeightStyle)}
>
<ha-service-picker
.hass=${this.hass}
.value=${this._serviceData?.action}
@@ -161,44 +207,27 @@ class HaPanelDevAction extends LitElement {
show-advanced
show-service-id
@value-changed=${this._serviceDataChanged}
class="card-content"
class="card-content ui-mode-content"
></ha-service-control>
`}
${this._error !== undefined
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
</ha-card>
</div>
<div class="button-row">
<div class="buttons">
<div class="switch-mode-container">
<ha-button
appearance="plain"
@click=${this._toggleYaml}
.disabled=${!this._uiAvailable}
>
${this._yamlMode
? this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.ui_mode"
)
: this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.yaml_mode"
)}
</ha-button>
<div class="card-actions">
${!this._uiAvailable
? html`<span class="error"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.no_template_ui_support"
)}</span
>`
: ""}
: nothing}
<ha-progress-button raised @click=${this._callService}>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.call_service"
)}
</ha-progress-button>
</div>
<ha-progress-button raised @click=${this._callService}>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.call_service"
)}
</ha-progress-button>
</div>
</ha-card>
</div>
${this._response?.result
? html`<div class="content response">
@@ -439,7 +468,7 @@ class HaPanelDevAction extends LitElement {
}
);
private async _callService(ev) {
private async _callService(ev: Event) {
const button = ev.currentTarget as HaProgressButton;
if (this._yamlMode && !this._yamlValid) {
@@ -560,13 +589,20 @@ class HaPanelDevAction extends LitElement {
button.actionSuccess();
}
private _toggleYaml() {
this._yamlMode = !this._yamlMode;
this._yamlValid = true;
this._error = undefined;
private _modeChanged(ev: HASSDomEvent<{ value: string }>) {
ev.stopPropagation();
const yamlMode = ev.detail.value === "yaml";
if (yamlMode === this._yamlMode) {
return;
}
withViewTransition(() => {
this._yamlMode = yamlMode;
this._yamlValid = true;
this._error = undefined;
});
}
private _yamlChanged(ev) {
private _yamlChanged(ev: HASSDomEvent<{ value: any; isValid: boolean }>) {
if (!ev.detail.isValid) {
this._yamlValid = false;
return;
@@ -602,7 +638,7 @@ class HaPanelDevAction extends LitElement {
}
}
private _serviceDataChanged(ev) {
private _serviceDataChanged(ev: HASSDomEvent<{ value: any }>) {
if (this._serviceData?.action !== ev.detail.value.action) {
this._error = undefined;
}
@@ -610,7 +646,7 @@ class HaPanelDevAction extends LitElement {
this._checkUiSupported();
}
private _serviceChanged(ev) {
private _serviceChanged(ev: HASSDomEvent<{ value: any }>) {
ev.stopPropagation();
if (ev.detail.value) {
this._serviceData = { action: ev.detail.value, data: {} };
@@ -667,30 +703,55 @@ class HaPanelDevAction extends LitElement {
max-width: 1200px;
margin: auto;
}
.button-row {
padding: var(--ha-space-2) var(--ha-space-4);
border-top: 1px solid var(--divider-color);
border-bottom: 1px solid var(--divider-color);
background: var(--card-background-color);
position: sticky;
bottom: 0;
box-sizing: border-box;
width: 100%;
}
.button-row .buttons {
.card-header {
display: flex;
justify-content: space-between;
max-width: 1200px;
margin: auto;
flex-direction: column;
gap: var(--ha-space-1);
}
.switch-mode-container {
.header-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--ha-space-2);
}
.switch-mode-container .error {
margin-left: var(--ha-space-2);
margin-inline-start: var(--ha-space-2);
margin-inline-end: initial;
.header-title {
flex: 1;
min-width: 0;
}
.secondary {
margin: 0;
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-normal);
line-height: normal;
letter-spacing: normal;
color: var(--secondary-text-color);
}
.card-content {
display: flex;
align-items: stretch;
justify-content: flex-start;
flex-direction: column;
gap: var(--ha-space-4);
margin: var(--ha-space-2);
--service-control-padding: 0;
}
.card-content ha-yaml-editor {
flex: 1;
display: flex;
flex-direction: column;
}
.yaml-mode-toggle {
flex-shrink: 0;
}
.card-actions {
display: flex;
justify-content: flex-end;
align-items: center;
gap: var(--ha-space-2);
}
.card-actions .error {
flex: 1;
color: var(--error-color);
}
.attributes {
width: 100%;

View File

@@ -35,10 +35,7 @@ import {
HOME_SUMMARIES_ICONS,
type HomeSummary,
} from "../strategies/home/helpers/home-summaries";
import {
filterLowBatteryEntities,
filterUnavailableBatteryEntities,
} from "../../maintenance/strategies/maintenance-view-strategy";
import { filterNeedsAttentionEntities } from "../../maintenance/strategies/maintenance-view-strategy";
import type { LovelaceCard, LovelaceGridOptions } from "../types";
import { tileCardStyle } from "./tile/tile-card-style";
import type { HomeSummaryCard } from "./types";
@@ -261,48 +258,19 @@ export class HuiHomeSummaryCard
maintenanceFilters
);
const lowBatteryEntities = filterLowBatteryEntities(
const needsAttentionEntities = filterNeedsAttentionEntities(
this.hass!,
maintenanceEntities
);
const unavailableBatteryEntities = filterUnavailableBatteryEntities(
this.hass!,
maintenanceEntities
);
const lowBatteryText =
lowBatteryEntities.length > 0
? this.hass.localize(
"ui.card.home-summary.count_maintenance_low_battery_issues",
{
count: lowBatteryEntities.length,
}
)
: undefined;
const unavailableText =
unavailableBatteryEntities.length > 0
? this.hass.localize(
"ui.card.home-summary.count_maintenance_issues_unavailable_battery_entities",
{
count: unavailableBatteryEntities.length,
}
)
: undefined;
if (lowBatteryText && unavailableText) {
return `${lowBatteryText}, ${unavailableText}`;
if (needsAttentionEntities.length > 0) {
return this.hass.localize(
"ui.card.home-summary.count_maintenance_issues",
{
count: needsAttentionEntities.length,
}
);
}
if (lowBatteryText) {
return lowBatteryText;
}
if (unavailableText) {
return unavailableText;
}
return this.hass.localize("ui.card.home-summary.all_maintenance_good");
}
case "energy": {

View File

@@ -419,7 +419,6 @@ export class HuiDialogEditBadge
.content {
width: 100%;
max-width: 100%;
gap: var(--ha-space-3);
}
}

View File

@@ -419,7 +419,6 @@ export class HuiDialogEditCard
.content {
width: 100%;
max-width: 100%;
gap: var(--ha-space-3);
}
}

View File

@@ -31,7 +31,7 @@ export const maintenanceEntityFilters: EntityFilter[] = [
const LOW_BATTERY_THRESHOLD = 20;
export const filterLowBatteryEntities = (
export const filterNeedsAttentionEntities = (
hass: HomeAssistant,
entityIds: string[]
): string[] =>
@@ -40,14 +40,6 @@ export const filterLowBatteryEntities = (
return !isNaN(stateValue) && stateValue <= LOW_BATTERY_THRESHOLD;
});
export const filterUnavailableBatteryEntities = (
hass: HomeAssistant,
entityIds: string[]
): string[] =>
entityIds.filter((entityId) => {
return hass.states[entityId]?.state === "unavailable";
});
const computeBatteryTileCard = (entityId: string): TileCardConfig => ({
type: "tile",
entity: entityId,

View File

@@ -218,8 +218,7 @@
"all_secure": "All secure",
"no_media_playing": "No media playing",
"count_media_playing": "{count} {count, plural,\n one {playing}\n other {playing}\n}",
"count_maintenance_low_battery_issues": "{count} {count, plural,\n one {low battery}\n other {low batteries}\n}",
"count_maintenance_issues_unavailable_battery_entities": "{count} {count, plural,\n one {unavailable device}\n other {unavailable devices}\n}",
"count_maintenance_issues": "{count} {count, plural,\n one {issue}\n other {issues}\n}",
"all_maintenance_good": "All good",
"count_persons_home": "{count} {count, plural,\n one {person}\n other {people}\n} home",
"nobody_home": "No one home"
@@ -3567,7 +3566,7 @@
"show_all": "Show all backups"
},
"settings": {
"title": "Automatic backup",
"title": "Backup settings",
"configure": "Configure backup settings",
"schedule": "Automatic backup schedule and retention",
"schedule_copies_all": "and keep all backups",
@@ -3613,10 +3612,6 @@
"sat": "Sa",
"sun": "Su"
}
},
"app_update_backup": {
"title": "App update backup",
"description": "Backup behavior for app updates"
}
},
"backups": {
@@ -3628,15 +3623,13 @@
"new_backup": "[%key:ui::panel::config::backup::overview::new_backup%]"
},
"settings": {
"header": "Automatic backups",
"header": "Backup settings",
"menu": {
"change_default_location": "Change default action location"
},
"schedule": {
"title": "Backup cycle",
"description": "Let Home Assistant take care of your backups by creating a scheduled backup that also removes older backups.",
"error_load": "Error loading Supervisor update config: {error}",
"error_save": "Error saving Supervisor update config: {error}"
"title": "Automatic backups",
"description": "Let Home Assistant take care of your backups by creating a scheduled backup that also removes older backups."
},
"data": {
"title": "Backup data"
@@ -3664,9 +3657,6 @@
"error_save": "Error saving Supervisor update config: {error}"
}
},
"app_update_backups": {
"header": "App update backups"
},
"details": {
"header": "Backup",
"not_found": "Not found",
@@ -3833,8 +3823,8 @@
"column_description": "Description",
"column_example": "Example",
"fill_example_data": "Fill example data",
"yaml_mode": "Go to YAML mode",
"ui_mode": "Go to UI mode",
"yaml_mode": "YAML mode",
"ui_mode": "UI mode",
"yaml_parameters": "Parameters only available in YAML mode",
"all_parameters": "All available parameters",
"accepts_target": "This action accepts a target, for example: `entity_id: light.bed_light`",