Improve more info update release note display (#22502)

* Fix ha-settings-row

* Improve update more info and update available card

* Set actions at the bottom on mobile

* Use update instead of install

* Improve markdown loaded
This commit is contained in:
Paul Bottein 2024-10-29 09:16:02 +01:00 committed by GitHub
parent e55f32ae91
commit 901f736d5f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 306 additions and 198 deletions

View File

@ -1,11 +1,10 @@
import "@material/mwc-list/mwc-list-item";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
html, html,
LitElement, LitElement,
PropertyValues,
nothing, nothing,
PropertyValues,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@ -16,12 +15,12 @@ import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/ha-checkbox"; import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-faded"; import "../../../src/components/ha-faded";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-icon-button"; import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-markdown"; import "../../../src/components/ha-markdown";
import "../../../src/components/ha-settings-row"; import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-svg-icon"; import "../../../src/components/ha-svg-icon";
import "../../../src/components/ha-switch"; import "../../../src/components/ha-switch";
import type { HaSwitch } from "../../../src/components/ha-switch";
import { import {
fetchHassioAddonChangelog, fetchHassioAddonChangelog,
fetchHassioAddonInfo, fetchHassioAddonInfo,
@ -42,6 +41,7 @@ import { updateCore } from "../../../src/data/supervisor/core";
import { StoreAddon } from "../../../src/data/supervisor/store"; import { StoreAddon } from "../../../src/data/supervisor/store";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types"; import { HomeAssistant, Route } from "../../../src/types";
import { addonArchIsSupported, extractChangelog } from "../util/addon"; import { addonArchIsSupported, extractChangelog } from "../util/addon";
@ -149,7 +149,7 @@ class UpdateAvailableCard extends LitElement {
</ha-markdown> </ha-markdown>
</ha-faded> </ha-faded>
` `
: ""} : nothing}
<div class="versions"> <div class="versions">
<p> <p>
${this.supervisor.localize( ${this.supervisor.localize(
@ -164,15 +164,17 @@ class UpdateAvailableCard extends LitElement {
</div> </div>
${["core", "addon"].includes(this._updateType) ${["core", "addon"].includes(this._updateType)
? html` ? html`
<ha-formfield <hr />
.label=${this.supervisor.localize( <ha-settings-row>
"update_available.create_backup" <span slot="heading">
)} ${this.supervisor.localize(
> "update_available.create_backup"
<ha-checkbox checked></ha-checkbox> )}
</ha-formfield> </span>
<ha-switch id="create_backup" checked></ha-switch>
</ha-settings-row>
` `
: ""} : nothing}
` `
: html`<ha-circular-progress : html`<ha-circular-progress
aria-label="Updating" aria-label="Updating"
@ -191,22 +193,24 @@ class UpdateAvailableCard extends LitElement {
? html` ? html`
<div class="card-actions"> <div class="card-actions">
${changelog ${changelog
? html`<a .href=${changelog} target="_blank" rel="noreferrer"> ? html`
<mwc-button <a href=${changelog} target="_blank" rel="noreferrer">
.label=${this.supervisor.localize( <ha-button
"update_available.open_release_notes" .label=${this.supervisor.localize(
)} "update_available.open_release_notes"
> )}
</mwc-button> >
</a>` </ha-button>
: ""} </a>
`
: nothing}
<span></span> <span></span>
<ha-progress-button @click=${this._update} raised> <ha-progress-button @click=${this._update}>
${this.supervisor.localize("common.update")} ${this.supervisor.localize("common.update")}
</ha-progress-button> </ha-progress-button>
</div> </div>
` `
: ""} : nothing}
</ha-card> </ha-card>
`; `;
} }
@ -242,9 +246,11 @@ class UpdateAvailableCard extends LitElement {
if (this._updateType && !["core", "addon"].includes(this._updateType)) { if (this._updateType && !["core", "addon"].includes(this._updateType)) {
return false; return false;
} }
const checkbox = this.shadowRoot?.querySelector("ha-checkbox"); const createBackupSwitch = this.shadowRoot?.getElementById(
if (checkbox) { "create-backup"
return checkbox.checked; ) as HaSwitch;
if (createBackupSwitch) {
return createBackupSwitch.checked;
} }
return true; return true;
} }
@ -397,41 +403,50 @@ class UpdateAvailableCard extends LitElement {
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return [
:host { haStyle,
display: block; css`
} :host {
ha-card { display: block;
margin: auto; }
} ha-card {
a { margin: auto;
text-decoration: none; }
color: var(--primary-text-color); a {
} text-decoration: none;
ha-settings-row { color: var(--primary-text-color);
padding: 0; }
} .card-actions {
.card-actions { display: flex;
display: flex; justify-content: space-between;
justify-content: space-between; }
border-top: none;
padding: 0 8px 8px;
}
ha-circular-progress { ha-circular-progress {
display: block; display: block;
margin: 32px; margin: 32px;
text-align: center; text-align: center;
} }
.progress-text { .progress-text {
text-align: center; text-align: center;
} }
ha-markdown { ha-markdown {
padding-bottom: 8px; padding-bottom: 8px;
} }
`;
ha-settings-row {
padding: 0;
margin-bottom: -16px;
}
hr {
border-color: var(--divider-color);
border-bottom: none;
margin: 16px 0 0 0;
}
`,
];
} }
} }

View File

@ -86,6 +86,11 @@ export class HaMarkdown extends LitElement {
font-size: 1.5em; font-size: 1.5em;
font-weight: bold; font-weight: bold;
} }
hr {
border-color: var(--divider-color);
border-bottom: none;
margin: 16px 0;
}
`; `;
} }
} }

View File

@ -42,14 +42,17 @@ export class HaSettingsRow extends LitElement {
padding-bottom: 8px; padding-bottom: 8px;
padding-left: 0; padding-left: 0;
padding-inline-start: 0; padding-inline-start: 0;
padding-right: 16x; padding-right: 16px;
padding-inline-end: 16px; padding-inline-end: 16px;
overflow: hidden; overflow: hidden;
display: var(--layout-vertical_-_display); display: var(--layout-vertical_-_display, flex);
flex-direction: var(--layout-vertical_-_flex-direction); flex-direction: var(--layout-vertical_-_flex-direction, column);
justify-content: var(--layout-center-justified_-_justify-content); justify-content: var(
flex: var(--layout-flex_-_flex); --layout-center-justified_-_justify-content,
flex-basis: var(--layout-flex_-_flex-basis); center
);
flex: var(--layout-flex_-_flex, 1);
flex-basis: var(--layout-flex_-_flex-basis, 0.000000001px);
} }
.body[three-line] { .body[three-line] {
min-height: var(--paper-item-body-three-line-min-height, 88px); min-height: var(--paper-item-body-three-line-min-height, 88px);

View File

@ -31,6 +31,9 @@ export const DOMAINS_WITH_NEW_MORE_INFO = [
"valve", "valve",
"water_heater", "water_heater",
]; ];
/** Domains with full height more info dialog */
export const DOMAINS_FULL_HEIGHT_MORE_INFO = ["update"];
/** Domains with separate more info dialog. */ /** Domains with separate more info dialog. */
export const DOMAINS_WITH_MORE_INFO = [ export const DOMAINS_WITH_MORE_INFO = [
"alarm_control_panel", "alarm_control_panel",

View File

@ -1,15 +1,18 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-linear-progress/mwc-linear-progress"; import "@material/mwc-linear-progress/mwc-linear-progress";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { BINARY_STATE_OFF } from "../../../common/const"; import { BINARY_STATE_OFF } from "../../../common/const";
import { supportsFeature } from "../../../common/entity/supports-feature"; import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-checkbox"; import "../../../components/ha-checkbox";
import "../../../components/ha-circular-progress"; import "../../../components/ha-circular-progress";
import "../../../components/ha-faded"; import "../../../components/ha-faded";
import "../../../components/ha-formfield"; import "../../../components/ha-formfield";
import "../../../components/ha-markdown"; import "../../../components/ha-markdown";
import "../../../components/ha-settings-row";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import { isUnavailableState } from "../../../data/entity"; import { isUnavailableState } from "../../../data/entity";
import { import {
UpdateEntity, UpdateEntity,
@ -30,6 +33,8 @@ class MoreInfoUpdate extends LitElement {
@state() private _error?: string; @state() private _error?: string;
@state() private _markdownLoading = true;
protected render() { protected render() {
if ( if (
!this.hass || !this.hass ||
@ -45,137 +50,174 @@ class MoreInfoUpdate extends LitElement {
this.stateObj.attributes.latest_version; this.stateObj.attributes.latest_version;
return html` return html`
${this.stateObj.attributes.in_progress <div class="content">
? supportsFeature(this.stateObj, UpdateEntityFeature.PROGRESS) && ${this.stateObj.attributes.in_progress
this.stateObj.attributes.update_percentage !== null ? supportsFeature(this.stateObj, UpdateEntityFeature.PROGRESS) &&
? html`<mwc-linear-progress this.stateObj.attributes.update_percentage !== null
.progress=${this.stateObj.attributes.update_percentage / 100} ? html`<mwc-linear-progress
buffer="" .progress=${this.stateObj.attributes.update_percentage / 100}
></mwc-linear-progress>` buffer=""
: html`<mwc-linear-progress indeterminate></mwc-linear-progress>` ></mwc-linear-progress>`
: ""} : html`<mwc-linear-progress indeterminate></mwc-linear-progress>`
<h3>${this.stateObj.attributes.title}</h3> : nothing}
${this._error <h3>${this.stateObj.attributes.title}</h3>
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ${this._error
: ""} ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
<div class="row"> : nothing}
<div class="key"> <div class="row">
${this.hass.formatEntityAttributeName( <div class="key">
this.stateObj, ${this.hass.formatEntityAttributeName(
"installed_version" this.stateObj,
)} "installed_version"
)}
</div>
<div class="value">
${this.stateObj.attributes.installed_version ??
this.hass.localize("state.default.unavailable")}
</div>
</div> </div>
<div class="value"> <div class="row">
${this.stateObj.attributes.installed_version ?? <div class="key">
this.hass.localize("state.default.unavailable")} ${this.hass.formatEntityAttributeName(
this.stateObj,
"latest_version"
)}
</div>
<div class="value">
${this.stateObj.attributes.latest_version ??
this.hass.localize("state.default.unavailable")}
</div>
</div> </div>
</div>
<div class="row">
<div class="key">
${this.hass.formatEntityAttributeName(
this.stateObj,
"latest_version"
)}
</div>
<div class="value">
${this.stateObj.attributes.latest_version ??
this.hass.localize("state.default.unavailable")}
</div>
</div>
${this.stateObj.attributes.release_url ${this.stateObj.attributes.release_url
? html`<div class="row"> ? html`<div class="row">
<div class="key"> <div class="key">
<a <a
href=${this.stateObj.attributes.release_url} href=${this.stateObj.attributes.release_url}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
${this.hass.localize( ${this.hass.localize(
"ui.dialogs.more_info_control.update.release_announcement" "ui.dialogs.more_info_control.update.release_announcement"
)} )}
</a> </a>
</div> </div>
</div>`
: ""}
${supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES) &&
!this._error
? this._releaseNotes === undefined
? html`<div class="flex center">
<ha-circular-progress indeterminate></ha-circular-progress>
</div>` </div>`
: html`<hr /> : nothing}
<ha-faded> ${supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES) &&
<ha-markdown .content=${this._releaseNotes}></ha-markdown> !this._error
</ha-faded> ` ? this._releaseNotes === undefined
: this.stateObj.attributes.release_summary ? html`
? html`<hr /> <hr />
<ha-markdown ${this._markdownLoading ? this._renderLoader() : nothing}
.content=${this.stateObj.attributes.release_summary} `
></ha-markdown>` : html`
: ""} <hr />
${supportsFeature(this.stateObj, UpdateEntityFeature.BACKUP) <ha-markdown
? html`<hr /> @content-resize=${this._markdownLoaded}
<ha-formfield .content=${this._releaseNotes}
.label=${this.hass.localize( class=${this._markdownLoading ? "hidden" : ""}
"ui.dialogs.more_info_control.update.create_backup" ></ha-markdown>
)} ${this._markdownLoading ? this._renderLoader() : nothing}
> `
<ha-checkbox : this.stateObj.attributes.release_summary
checked ? html`
.disabled=${updateIsInstalling(this.stateObj)} <hr />
></ha-checkbox> <ha-markdown
</ha-formfield> ` @content-resize=${this._markdownLoaded}
: ""} .content=${this.stateObj.attributes.release_summary}
<div class="actions"> class=${this._markdownLoading ? "hidden" : ""}
${this.stateObj.state === BINARY_STATE_OFF && ></ha-markdown>
this.stateObj.attributes.skipped_version ${this._markdownLoading ? this._renderLoader() : nothing}
`
: nothing}
</div>
<div class="footer">
${supportsFeature(this.stateObj, UpdateEntityFeature.BACKUP)
? html` ? html`
<mwc-button @click=${this._handleClearSkipped}> <ha-settings-row>
${this.hass.localize( <span slot="heading">
"ui.dialogs.more_info_control.update.clear_skipped" ${this.hass.localize(
)} "ui.dialogs.more_info_control.update.create_backup"
</mwc-button> )}
</span>
<ha-switch
id="create_backup"
checked
.disabled=${updateIsInstalling(this.stateObj)}
></ha-switch>
</ha-settings-row>
` `
: html` : nothing}
<mwc-button <div class="actions">
@click=${this._handleSkip} ${this.stateObj.state === BINARY_STATE_OFF &&
.disabled=${skippedVersion || this.stateObj.attributes.skipped_version
this.stateObj.state === BINARY_STATE_OFF || ? html`
updateIsInstalling(this.stateObj)} <ha-button @click=${this._handleClearSkipped}>
> ${this.hass.localize(
${this.hass.localize( "ui.dialogs.more_info_control.update.clear_skipped"
"ui.dialogs.more_info_control.update.skip" )}
)} </ha-button>
</mwc-button> `
`} : html`
${supportsFeature(this.stateObj, UpdateEntityFeature.INSTALL) <ha-button
? html` @click=${this._handleSkip}
<mwc-button .disabled=${skippedVersion ||
@click=${this._handleInstall} this.stateObj.state === BINARY_STATE_OFF ||
.disabled=${(this.stateObj.state === BINARY_STATE_OFF && updateIsInstalling(this.stateObj)}
!skippedVersion) || >
updateIsInstalling(this.stateObj)} ${this.hass.localize(
> "ui.dialogs.more_info_control.update.skip"
${this.hass.localize( )}
"ui.dialogs.more_info_control.update.install" </ha-button>
)} `}
</mwc-button> ${supportsFeature(this.stateObj, UpdateEntityFeature.INSTALL)
` ? html`
: ""} <ha-button
@click=${this._handleInstall}
.disabled=${(this.stateObj.state === BINARY_STATE_OFF &&
!skippedVersion) ||
updateIsInstalling(this.stateObj)}
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.update"
)}
</ha-button>
`
: nothing}
</div>
</div>
`;
}
private _renderLoader() {
return html`
<div class="flex center loader">
<ha-circular-progress indeterminate></ha-circular-progress>
</div> </div>
`; `;
} }
protected firstUpdated(): void { protected firstUpdated(): void {
if (supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES)) { if (supportsFeature(this.stateObj!, UpdateEntityFeature.RELEASE_NOTES)) {
updateReleaseNotes(this.hass, this.stateObj!.entity_id) this._fetchReleaseNotes();
.then((result) => { }
this._releaseNotes = result; }
})
.catch((err) => { private async _markdownLoaded() {
this._error = err.message; if (this._markdownLoading) {
}); this._markdownLoading = false;
}
}
private async _fetchReleaseNotes() {
try {
this._releaseNotes = await updateReleaseNotes(
this.hass,
this.stateObj!.entity_id
);
} catch (err: any) {
this._error = err.message;
} }
} }
@ -183,9 +225,11 @@ class MoreInfoUpdate extends LitElement {
if (!supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) { if (!supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
return null; return null;
} }
const checkbox = this.shadowRoot?.querySelector("ha-checkbox"); const createBackupSwitch = this.shadowRoot?.getElementById(
if (checkbox) { "create-backup"
return checkbox.checked; ) as HaSwitch;
if (createBackupSwitch) {
return createBackupSwitch.checked;
} }
return true; return true;
} }
@ -234,6 +278,12 @@ class MoreInfoUpdate extends LitElement {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host {
display: flex;
flex-direction: column;
flex: 1;
justify-content: space-between;
}
hr { hr {
border-color: var(--divider-color); border-color: var(--divider-color);
border-bottom: none; border-bottom: none;
@ -248,26 +298,44 @@ class MoreInfoUpdate extends LitElement {
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
} }
.actions {
.footer {
border-top: 1px solid var(--divider-color); border-top: 1px solid var(--divider-color);
background: var( background: var(
--ha-dialog-surface-background, --ha-dialog-surface-background,
var(--mdc-theme-surface, #fff) var(--mdc-theme-surface, #fff)
); );
margin: 8px 0 0;
display: flex;
flex-wrap: wrap;
justify-content: center;
position: sticky; position: sticky;
bottom: 0; bottom: 0;
padding: 12px 0; margin: 0 -24px -24px -24px;
margin-bottom: -24px; box-sizing: border-box;
z-index: 1; display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
z-index: 10;
} }
.actions mwc-button { ha-settings-row {
margin: 0 4px 4px; width: 100%;
padding: 0 24px;
box-sizing: border-box;
margin-bottom: -16px;
margin-top: -4px;
} }
.actions {
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-end;
box-sizing: border-box;
padding: 12px;
z-index: 1;
gap: 8px;
}
a { a {
color: var(--primary-color); color: var(--primary-color);
} }
@ -282,6 +350,16 @@ class MoreInfoUpdate extends LitElement {
} }
ha-markdown { ha-markdown {
direction: ltr; direction: ltr;
padding-bottom: 16px;
box-sizing: border-box;
}
ha-markdown.hidden {
display: none;
}
.loader {
height: 80px;
box-sizing: border-box;
padding-bottom: 16px;
} }
`; `;
} }

View File

@ -9,6 +9,7 @@ import {
computeShowHistoryComponent, computeShowHistoryComponent,
computeShowLogBookComponent, computeShowLogBookComponent,
computeShowNewMoreInfo, computeShowNewMoreInfo,
DOMAINS_FULL_HEIGHT_MORE_INFO,
DOMAINS_NO_INFO, DOMAINS_NO_INFO,
DOMAINS_WITH_MORE_INFO, DOMAINS_WITH_MORE_INFO,
} from "./const"; } from "./const";
@ -40,6 +41,8 @@ export class MoreInfoInfo extends LitElement {
const entityRegObj = this.hass.entities[entityId]; const entityRegObj = this.hass.entities[entityId];
const domain = computeDomain(entityId); const domain = computeDomain(entityId);
const isNewMoreInfo = stateObj && computeShowNewMoreInfo(stateObj); const isNewMoreInfo = stateObj && computeShowNewMoreInfo(stateObj);
const isFullHeight =
isNewMoreInfo || DOMAINS_FULL_HEIGHT_MORE_INFO.includes(domain);
return html` return html`
<div class="container" data-domain=${domain}> <div class="container" data-domain=${domain}>
@ -89,7 +92,7 @@ export class MoreInfoInfo extends LitElement {
.entityId=${this.entityId} .entityId=${this.entityId}
></ha-more-info-logbook>`} ></ha-more-info-logbook>`}
<more-info-content <more-info-content
?full-height=${isNewMoreInfo} ?full-height=${isFullHeight}
.stateObj=${stateObj} .stateObj=${stateObj}
.hass=${this.hass} .hass=${this.hass}
.entry=${this.entry} .entry=${this.entry}

View File

@ -1191,6 +1191,7 @@
"skip": "Skip", "skip": "Skip",
"clear_skipped": "Clear skipped", "clear_skipped": "Clear skipped",
"install": "Install", "install": "Install",
"update": "Update",
"create_backup": "Create backup before updating", "create_backup": "Create backup before updating",
"auto_update_enabled_title": "Can not skip version", "auto_update_enabled_title": "Can not skip version",
"auto_update_enabled_text": "Automatic updates for this item have been enabled; skipping it is, therefore, unavailable. You can either install this update now or wait for Home Assistant to do it automatically." "auto_update_enabled_text": "Automatic updates for this item have been enabled; skipping it is, therefore, unavailable. You can either install this update now or wait for Home Assistant to do it automatically."