diff --git a/hassio/src/addon-view/info/hassio-addon-info.ts b/hassio/src/addon-view/info/hassio-addon-info.ts index 0be5862955..28343352c7 100644 --- a/hassio/src/addon-view/info/hassio-addon-info.ts +++ b/hassio/src/addon-view/info/hassio-addon-info.ts @@ -9,21 +9,19 @@ import { mdiExclamationThick, mdiFlask, mdiHomeAssistant, - mdiInformation, mdiKey, mdiNetwork, mdiPound, mdiShield, } from "@mdi/js"; -import "@polymer/paper-tooltip/paper-tooltip"; import { css, CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; import { classMap } from "lit-html/directives/class-map"; @@ -35,6 +33,7 @@ import "../../../../src/components/buttons/ha-progress-button"; import "../../../../src/components/ha-card"; import "../../../../src/components/ha-label-badge"; import "../../../../src/components/ha-markdown"; +import "../../../../src/components/ha-settings-row"; import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-switch"; import { @@ -386,67 +385,94 @@ class HassioAddonInfo extends LitElement { ${this.addon.version ? html` -
-
Start on boot
- -
- ${this.addon.auto_update || this.hass.userData?.showAdvanced - ? html` -
-
Auto update
- -
- ` - : ""} - ${this.addon.ingress - ? html` -
-
Show in sidebar
- - ${this._computeCannotIngressSidebar - ? html` - - This option requires Home Assistant 0.92 or - later. - - ` - : ""} -
- ` - : ""} - ${this._computeUsesProtectedOptions - ? html` -
-
- Protection mode - - - - Grant the add-on elevated system access. - +
+ + + Start on boot + + + Make the add-on start during a system boot + + + + + ${this.hass.userData?.showAdvanced + ? html` + + + Watchdog -
- -
- ` - : ""} + + This will start the add-on if it crashes + + + + ` + : ""} + ${this.addon.auto_update || this.hass.userData?.showAdvanced + ? html` + + + Auto update + + + Auto update the add-on when there is a new version + available + + + + ` + : ""} + ${this.addon.ingress + ? html` + + + Show in sidebar + + + ${this._computeCannotIngressSidebar + ? "This option requires Home Assistant 0.92 or later." + : "Add this add-on to your sidebar"} + + + + ` + : ""} + ${this._computeUsesProtectedOptions + ? html` + + + Protection mode + + + Blocks elevated system access from the add-on + + + + ` + : ""} +
` : ""} ${this._error ? html`
${this._error}
` : ""} @@ -552,137 +578,6 @@ class HassioAddonInfo extends LitElement { `; } - static get styles(): CSSResult[] { - return [ - haStyle, - hassioStyle, - css` - :host { - display: block; - } - ha-card { - display: block; - margin-bottom: 16px; - } - ha-card.warning { - background-color: var(--error-color); - color: white; - } - ha-card.warning .card-header { - color: white; - } - ha-card.warning .card-content { - color: white; - } - ha-card.warning mwc-button { - --mdc-theme-primary: white !important; - } - .warning { - color: var(--error-color); - --mdc-theme-primary: var(--error-color); - } - .light-color { - color: var(--secondary-text-color); - } - .addon-header { - padding-left: 8px; - font-size: 24px; - color: var(--ha-card-header-color, --primary-text-color); - } - .addon-version { - float: right; - font-size: 15px; - vertical-align: middle; - } - .errors { - color: var(--error-color); - margin-bottom: 16px; - } - .description { - margin-bottom: 16px; - } - img.logo { - max-height: 60px; - margin: 16px 0; - display: block; - } - .state { - display: flex; - margin: 33px 0; - } - .state div { - width: 180px; - display: inline-block; - } - .state ha-svg-icon { - width: 16px; - height: 16px; - color: var(--secondary-text-color); - } - ha-switch { - display: flex; - } - ha-svg-icon.running { - color: var(--paper-green-400); - } - ha-svg-icon.stopped { - color: var(--google-red-300); - } - ha-call-api-button { - font-weight: 500; - color: var(--primary-color); - } - .right { - float: right; - } - protection-enable mwc-button { - --mdc-theme-primary: white; - } - .description a { - color: var(--primary-color); - } - .red { - --ha-label-badge-color: var(--label-badge-red, #df4c1e); - } - .blue { - --ha-label-badge-color: var(--label-badge-blue, #039be5); - } - .green { - --ha-label-badge-color: var(--label-badge-green, #0da035); - } - .yellow { - --ha-label-badge-color: var(--label-badge-yellow, #f4b400); - } - .security { - margin-bottom: 16px; - } - .card-actions { - display: flow-root; - } - .security h3 { - margin-bottom: 8px; - font-weight: normal; - } - .security ha-label-badge { - cursor: pointer; - margin-right: 4px; - --ha-label-badge-padding: 8px 0 0 0; - } - .changelog { - display: contents; - } - .changelog-link { - color: var(--primary-color); - text-decoration: underline; - cursor: pointer; - } - ha-markdown { - padding: 16px; - } - `, - ]; - } - private get _computeHassioApi(): boolean { return ( this.addon.hassio_api && @@ -771,6 +666,24 @@ class HassioAddonInfo extends LitElement { } } + private async _watchdogToggled(): Promise { + this._error = undefined; + const data: HassioAddonSetOptionParams = { + watchdog: !this.addon.watchdog, + }; + try { + await setHassioAddonOption(this.hass, this.addon.slug, data); + const eventdata = { + success: true, + response: undefined, + path: "option", + }; + fireEvent(this, "hass-api-called", eventdata); + } catch (err) { + this._error = `Failed to set addon option, ${err.body?.message || err}`; + } + } + private async _autoUpdateToggled(): Promise { this._error = undefined; const data: HassioAddonSetOptionParams = { @@ -887,6 +800,146 @@ class HassioAddonInfo extends LitElement { this._error = `Failed to uninstall addon, ${err.body?.message || err}`; } } + + static get styles(): CSSResult[] { + return [ + haStyle, + hassioStyle, + css` + :host { + display: block; + } + ha-card { + display: block; + margin-bottom: 16px; + } + ha-card.warning { + background-color: var(--error-color); + color: white; + } + ha-card.warning .card-header { + color: white; + } + ha-card.warning .card-content { + color: white; + } + ha-card.warning mwc-button { + --mdc-theme-primary: white !important; + } + .warning { + color: var(--error-color); + --mdc-theme-primary: var(--error-color); + } + .light-color { + color: var(--secondary-text-color); + } + .addon-header { + padding-left: 8px; + font-size: 24px; + color: var(--ha-card-header-color, --primary-text-color); + } + .addon-version { + float: right; + font-size: 15px; + vertical-align: middle; + } + .errors { + color: var(--error-color); + margin-bottom: 16px; + } + .description { + margin-bottom: 16px; + } + img.logo { + max-height: 60px; + margin: 16px 0; + display: block; + } + + ha-switch { + display: flex; + } + ha-svg-icon.running { + color: var(--paper-green-400); + } + ha-svg-icon.stopped { + color: var(--google-red-300); + } + ha-call-api-button { + font-weight: 500; + color: var(--primary-color); + } + .right { + float: right; + } + protection-enable mwc-button { + --mdc-theme-primary: white; + } + .description a { + color: var(--primary-color); + } + .red { + --ha-label-badge-color: var(--label-badge-red, #df4c1e); + } + .blue { + --ha-label-badge-color: var(--label-badge-blue, #039be5); + } + .green { + --ha-label-badge-color: var(--label-badge-green, #0da035); + } + .yellow { + --ha-label-badge-color: var(--label-badge-yellow, #f4b400); + } + .security { + margin-bottom: 16px; + } + .card-actions { + display: flow-root; + } + .security h3 { + margin-bottom: 8px; + font-weight: normal; + } + .security ha-label-badge { + cursor: pointer; + margin-right: 4px; + --ha-label-badge-padding: 8px 0 0 0; + } + .changelog { + display: contents; + } + .changelog-link { + color: var(--primary-color); + text-decoration: underline; + cursor: pointer; + } + ha-markdown { + padding: 16px; + } + ha-settings-row { + padding: 0; + height: 54px; + width: 100%; + } + ha-settings-row > span[slot="description"] { + white-space: normal; + color: var(--secondary-text-color); + } + ha-settings-row[three-line] { + height: 74px; + } + + .addon-options { + max-width: 50%; + } + @media (max-width: 720px) { + .addon-options { + max-width: 100%; + } + } + `, + ]; + } } declare global { interface HTMLElementTagNameMap { diff --git a/hassio/src/components/hassio-ansi-to-html.ts b/hassio/src/components/hassio-ansi-to-html.ts index b553367b8a..b957b172b4 100644 --- a/hassio/src/components/hassio-ansi-to-html.ts +++ b/hassio/src/components/hassio-ansi-to-html.ts @@ -21,7 +21,7 @@ interface State { class HassioAnsiToHtml extends LitElement { @property() public content!: string; - public render(): TemplateResult | void { + protected render(): TemplateResult | void { return html`${this._parseTextToColoredPre(this.content)}`; } diff --git a/hassio/src/dashboard/hassio-update.ts b/hassio/src/dashboard/hassio-update.ts index 2fee6b61dc..e4cd4dbb07 100644 --- a/hassio/src/dashboard/hassio-update.ts +++ b/hassio/src/dashboard/hassio-update.ts @@ -10,7 +10,7 @@ import { internalProperty, TemplateResult, } from "lit-element"; -import "../../../src/components/buttons/ha-call-api-button"; +import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/ha-card"; import "../../../src/components/ha-svg-icon"; import { HassioHassOSInfo } from "../../../src/data/hassio/host"; @@ -21,6 +21,11 @@ import { import { haStyle } from "../../../src/resources/styles"; import { HomeAssistant } from "../../../src/types"; import { hassioStyle } from "../resources/hassio-style"; +import { + showConfirmationDialog, + showAlertDialog, +} from "../../../src/dialogs/generic/show-dialog-box"; +import { HassioResponse } from "../../../src/data/hassio/common"; @customElement("hassio-update") export class HassioUpdate extends LitElement { @@ -126,31 +131,50 @@ export class HassioUpdate extends LitElement { Release notes - Update - + `; } - private _apiCalled(ev): void { - if (ev.detail.success) { - this._error = ""; + private async _confirmUpdate(ev): Promise { + const item = ev.target; + item.progress = true; + const confirmed = await showConfirmationDialog(this, { + title: `Update ${item.name}`, + text: `Are you sure you want to upgrade ${item.name} to version ${item.version}?`, + confirmText: "update", + dismissText: "cancel", + }); + + if (!confirmed) { + item.progress = false; return; } - - const response = ev.detail.response; - - if (typeof response.body === "object") { - this._error = response.body.message || "Unknown error"; - } else { - this._error = response.body; + try { + await this.hass.callApi>("POST", item.apiPath); + } catch (err) { + // Only show an error if the status code was not 504 (timeout reported by proxies) + if (err.status_code !== 504) { + showAlertDialog(this, { + title: "Update failed", + text: + typeof err === "object" + ? typeof err.body === "object" + ? err.body.message + : err.body || "Unkown error" + : err, + }); + } } + item.progress = false; } static get styles(): CSSResult[] { diff --git a/hassio/src/dialogs/network/dialog-hassio-network.ts b/hassio/src/dialogs/network/dialog-hassio-network.ts new file mode 100644 index 0000000000..f49d1c1a86 --- /dev/null +++ b/hassio/src/dialogs/network/dialog-hassio-network.ts @@ -0,0 +1,335 @@ +import "@material/mwc-button/mwc-button"; +import "@material/mwc-icon-button"; +import "@material/mwc-tab-bar"; +import "@material/mwc-tab"; +import { PaperInputElement } from "@polymer/paper-input/paper-input"; +import { mdiClose } from "@mdi/js"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + internalProperty, + TemplateResult, +} from "lit-element"; +import { cache } from "lit-html/directives/cache"; + +import { + updateNetworkInterface, + NetworkInterface, +} from "../../../../src/data/hassio/network"; +import { fireEvent } from "../../../../src/common/dom/fire_event"; +import { HassioNetworkDialogParams } from "./show-dialog-network"; +import { haStyleDialog } from "../../../../src/resources/styles"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../../src/dialogs/generic/show-dialog-box"; +import type { HomeAssistant } from "../../../../src/types"; +import type { HaRadio } from "../../../../src/components/ha-radio"; +import { HassDialog } from "../../../../src/dialogs/make-dialog-manager"; + +import "../../../../src/components/ha-circular-progress"; +import "../../../../src/components/ha-dialog"; +import "../../../../src/components/ha-formfield"; +import "../../../../src/components/ha-header-bar"; +import "../../../../src/components/ha-radio"; +import "../../../../src/components/ha-related-items"; +import "../../../../src/components/ha-svg-icon"; + +@customElement("dialog-hassio-network") +export class DialogHassioNetwork extends LitElement implements HassDialog { + @property({ attribute: false }) public hass!: HomeAssistant; + + @internalProperty() private _prosessing = false; + + @internalProperty() private _params?: HassioNetworkDialogParams; + + @internalProperty() private _network!: { + interface: string; + data: NetworkInterface; + }[]; + + @internalProperty() private _curTabIndex = 0; + + @internalProperty() private _device?: { + interface: string; + data: NetworkInterface; + }; + + @internalProperty() private _dirty = false; + + public async showDialog(params: HassioNetworkDialogParams): Promise { + this._params = params; + this._dirty = false; + this._curTabIndex = 0; + this._network = Object.keys(params.network?.interfaces) + .map((device) => ({ + interface: device, + data: params.network.interfaces[device], + })) + .sort((a, b) => { + return a.data.primary > b.data.primary ? -1 : 1; + }); + this._device = this._network[this._curTabIndex]; + this._device.data.nameservers = String(this._device.data.nameservers); + await this.updateComplete; + } + + public closeDialog(): void { + this._params = undefined; + this._prosessing = false; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this._params || !this._network) { + return html``; + } + + return html` + +
+ + + Network settings + + + + + + ${this._network.length > 1 + ? html` ${this._network.map( + (device) => + html` + ` + )} + ` + : ""} +
+ ${cache(this._renderTab())} +
+ `; + } + + private _renderTab() { + return html`
+ + + + + + + + + ${this._device!.data.method !== "dhcp" + ? html` + + + NB!: If you are changing IP or gateway addresses, you might lose + the connection.` + : ""} +
+
+ + + ${this._prosessing + ? html`` + : "Update"} + +
`; + } + + private async _updateNetwork() { + this._prosessing = true; + let options: Partial = { + method: this._device!.data.method, + }; + if (options.method !== "dhcp") { + options = { + ...options, + address: this._device!.data.ip_address, + gateway: this._device!.data.gateway, + dns: String(this._device!.data.nameservers).split(","), + }; + } + try { + await updateNetworkInterface(this.hass, this._device!.interface, options); + } catch (err) { + showAlertDialog(this, { + title: "Failed to change network settings", + text: + typeof err === "object" ? err.body.message || "Unkown error" : err, + }); + this._prosessing = false; + return; + } + this._params?.loadData(); + this.closeDialog(); + } + + private async _handleTabActivated(ev: CustomEvent): Promise { + if (this._dirty) { + const confirm = await showConfirmationDialog(this, { + text: + "You have unsaved changes, these will get lost if you change tabs, do you want to continue?", + confirmText: "yes", + dismissText: "no", + }); + if (!confirm) { + this.requestUpdate("_device"); + return; + } + } + this._curTabIndex = ev.detail.index; + this._device = this._network[ev.detail.index]; + this._device.data.nameservers = String(this._device.data.nameservers); + } + + private _handleRadioValueChanged(ev: CustomEvent): void { + const value = (ev.target as HaRadio).value as "dhcp" | "static"; + + if (!value || !this._device || this._device!.data.method === value) { + return; + } + + this._dirty = true; + + this._device!.data.method = value; + this.requestUpdate("_device"); + } + + private _handleInputValueChanged(ev: CustomEvent): void { + const value: string | null | undefined = (ev.target as PaperInputElement) + .value; + const id = (ev.target as PaperInputElement).id; + + if (!value || !this._device || this._device.data[id] === value) { + return; + } + + this._dirty = true; + + this._device.data[id] = value; + } + + static get styles(): CSSResult[] { + return [ + haStyleDialog, + css` + ha-header-bar { + --mdc-theme-on-primary: var(--primary-text-color); + --mdc-theme-primary: var(--mdc-theme-surface); + flex-shrink: 0; + } + + mwc-tab-bar { + border-bottom: 1px solid + var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12)); + } + + ha-dialog { + --dialog-content-position: static; + --dialog-content-padding: 0; + --dialog-z-index: 6; + } + + @media all and (min-width: 451px) and (min-height: 501px) { + .container { + width: 400px; + } + } + + .content { + display: block; + padding: 20px 24px; + } + + /* overrule the ha-style-dialog max-height on small screens */ + @media all and (max-width: 450px), all and (max-height: 500px) { + ha-header-bar { + --mdc-theme-primary: var(--app-header-background-color); + --mdc-theme-on-primary: var(--app-header-text-color, white); + } + } + + mwc-button.warning { + --mdc-theme-primary: var(--error-color); + } + + :host([rtl]) app-toolbar { + direction: rtl; + text-align: right; + } + .container { + padding: 20px 24px; + } + .form { + margin-bottom: 53px; + } + .buttons { + position: absolute; + bottom: 0; + width: 100%; + box-sizing: border-box; + border-top: 1px solid + var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12)); + display: flex; + justify-content: space-between; + padding: 8px; + padding-bottom: max(env(safe-area-inset-bottom), 8px); + background-color: var(--mdc-theme-surface, #fff); + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-hassio-network": DialogHassioNetwork; + } +} diff --git a/hassio/src/dialogs/network/show-dialog-network.ts b/hassio/src/dialogs/network/show-dialog-network.ts new file mode 100644 index 0000000000..0f59f8a545 --- /dev/null +++ b/hassio/src/dialogs/network/show-dialog-network.ts @@ -0,0 +1,22 @@ +import { fireEvent } from "../../../../src/common/dom/fire_event"; +import { NetworkInfo } from "../../../../src/data/hassio/network"; +import "./dialog-hassio-network"; + +export interface HassioNetworkDialogParams { + network: NetworkInfo; + loadData: () => Promise; +} + +export const showNetworkDialog = ( + element: HTMLElement, + dialogParams: HassioNetworkDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-hassio-network", + dialogImport: () => + import( + /* webpackChunkName: "dialog-hassio-network" */ "./dialog-hassio-network" + ), + dialogParams, + }); +}; diff --git a/hassio/src/system/hassio-host-info.ts b/hassio/src/system/hassio-host-info.ts index 3a53925ddb..2f238a8c00 100644 --- a/hassio/src/system/hassio-host-info.ts +++ b/hassio/src/system/hassio-host-info.ts @@ -1,18 +1,26 @@ import "@material/mwc-button"; +import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; +import "@material/mwc-list/mwc-list-item"; +import { mdiDotsVertical } from "@mdi/js"; +import { safeDump } from "js-yaml"; import { css, CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; -import "../../../src/components/buttons/ha-call-api-button"; +import memoizeOne from "memoize-one"; +import "../../../src/components/ha-button-menu"; +import "../../../src/components/ha-card"; +import "../../../src/components/ha-settings-row"; import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware"; import { changeHostOptions, + configSyncOS, fetchHassioHostInfo, HassioHassOSInfo, HassioHostInfo as HassioHostInfoType, @@ -20,6 +28,10 @@ import { shutdownHost, updateOS, } from "../../../src/data/hassio/host"; +import { + fetchNetworkInfo, + NetworkInfo, +} from "../../../src/data/hassio/network"; import { HassioInfo } from "../../../src/data/hassio/supervisor"; import { showAlertDialog, @@ -29,6 +41,7 @@ import { import { haStyle } from "../../../src/resources/styles"; import { HomeAssistant } from "../../../src/types"; import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown"; +import { showNetworkDialog } from "../dialogs/network/show-dialog-network"; import { hassioStyle } from "../resources/hassio-style"; @customElement("hassio-host-info") @@ -41,86 +54,130 @@ class HassioHostInfo extends LitElement { @property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo; - @internalProperty() private _errors?: string; + @internalProperty() public _networkInfo?: NetworkInfo; - public render(): TemplateResult | void { + protected render(): TemplateResult | void { + const primaryIpAddress = this.hostInfo.features.includes("network") + ? this._primaryIpAddress(this._networkInfo!) + : ""; return html` - +
-

Host system

- - - - - - - - - - - ${!this.hostInfo.features.includes("hassos") - ? html` - - - ` - : ""} - ${this.hostInfo.deployment - ? html` - - - - - ` - : ""} - -
Hostname${this.hostInfo.hostname}
System${this.hostInfo.operating_system}
Docker version${this.hassioInfo.docker}
Deployment${this.hostInfo.deployment}
- - Hardware - ${this.hostInfo.features.includes("hostname") - ? html` + ? html` + + Hostname + + + ${this.hostInfo.hostname} + - Change hostname - ` + ` : ""} - ${this._errors - ? html`
Error: ${this._errors}
` + ${this.hostInfo.features.includes("network") + ? html` + + IP address + + + ${primaryIpAddress} + + + + ` + : ""} + + + + Operating system + + + ${this.hostInfo.operating_system} + + ${this.hostInfo.version !== this.hostInfo.version_latest && + this.hostInfo.features.includes("hassos") + ? html` + + + ` + : ""} + + ${!this.hostInfo.features.includes("hassos") + ? html` + + Docker version + + + ${this.hassioInfo.docker} + + ` + : ""} + ${this.hostInfo.deployment + ? html` + + Deployment + + + ${this.hostInfo.deployment} + + ` : ""}
${this.hostInfo.features.includes("reboot") ? html` - Reboot + ` : ""} ${this.hostInfo.features.includes("shutdown") ? html` - Shutdown - ` - : ""} - ${this.hostInfo.features.includes("hassos") - ? html` - Import from USB + ` : ""} - ${this.hostInfo.version !== this.hostInfo.version_latest - ? html` Update ` - : ""} + + + + + + + Hardware + + ${this.hostInfo.features.includes("hassos") + ? html` + Import from USB + ` + : ""} +
`; @@ -133,72 +190,96 @@ class HassioHostInfo extends LitElement { css` ha-card { height: 100%; + justify-content: space-between; + flex-direction: column; + display: flex; + } + .card-actions { + height: 48px; + border-top: none; + display: flex; + justify-content: space-between; + align-items: center; + } + ha-settings-row { + padding: 0; + height: 54px; width: 100%; } - .card-content { - color: var(--primary-text-color); - box-sizing: border-box; - height: calc(100% - 47px); + ha-settings-row[three-line] { + height: 74px; } - .info { - width: 100%; - } - .info td:nth-child(2) { - text-align: right; - } - .errors { - color: var(--error-color); - margin-top: 16px; - } - mwc-button.info { - max-width: calc(50% - 12px); - } - table.info { - margin-bottom: 10px; + ha-settings-row > span[slot="description"] { + white-space: normal; + color: var(--secondary-text-color); } + .warning { --mdc-theme-primary: var(--error-color); } + + ha-button-menu { + color: var(--secondary-text-color); + --mdc-menu-min-width: 200px; + } + @media (min-width: 563px) { + paper-listbox { + max-height: 150px; + overflow: auto; + } + } + paper-item { + cursor: pointer; + min-height: 35px; + } + mwc-list-item ha-svg-icon { + color: var(--secondary-text-color); + } `, ]; } protected firstUpdated(): void { - this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev)); + this._loadData(); } - private _apiCalled(ev): void { - if (ev.detail.success) { - this._errors = undefined; - return; + private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => { + if (!network_info) { + return ""; } + return Object.keys(network_info?.interfaces) + .map((device) => network_info.interfaces[device]) + .find((device) => device.primary)?.ip_address; + }); - const response = ev.detail.response; - - this._errors = - typeof response.body === "object" - ? response.body.message || "Unknown error" - : response.body; + private async _handleMenuAction(ev: CustomEvent) { + switch (ev.detail.index) { + case 0: + await this._showHardware(); + break; + case 1: + await this._importFromUSB(); + break; + } } private async _showHardware(): Promise { try { - const content = this._objectToMarkdown( - await fetchHassioHardwareInfo(this.hass) - ); + const content = await fetchHassioHardwareInfo(this.hass); showHassioMarkdownDialog(this, { title: "Hardware", - content, + content: `
${safeDump(content, { indent: 2 })}
`, }); } catch (err) { - showHassioMarkdownDialog(this, { - title: "Hardware", - content: "Error getting hardware info", + showAlertDialog(this, { + title: "Failed to get Hardware list", + text: + typeof err === "object" ? err.body?.message || "Unkown error" : err, }); } } - private async _rebootHost(): Promise { + private async _hostReboot(): Promise { const confirmed = await showConfirmationDialog(this, { title: "Reboot", text: "Are you sure you want to reboot the host?", @@ -215,12 +296,13 @@ class HassioHostInfo extends LitElement { } catch (err) { showAlertDialog(this, { title: "Failed to reboot", - text: err.body.message, + text: + typeof err === "object" ? err.body?.message || "Unkown error" : err, }); } } - private async _shutdownHost(): Promise { + private async _hostShutdown(): Promise { const confirmed = await showConfirmationDialog(this, { title: "Shutdown", text: "Are you sure you want to shutdown the host?", @@ -237,12 +319,13 @@ class HassioHostInfo extends LitElement { } catch (err) { showAlertDialog(this, { title: "Failed to shutdown", - text: err.body.message, + text: + typeof err === "object" ? err.body?.message || "Unkown error" : err, }); } } - private async _updateOS(): Promise { + private async _osUpdate(): Promise { const confirmed = await showConfirmationDialog(this, { title: "Update", text: "Are you sure you want to update the OS?", @@ -259,30 +342,17 @@ class HassioHostInfo extends LitElement { } catch (err) { showAlertDialog(this, { title: "Failed to update", - text: err.body.message, + text: + typeof err === "object" ? err.body?.message || "Unkown error" : err, }); } } - private _objectToMarkdown(obj, indent = ""): string { - let data = ""; - Object.keys(obj).forEach((key) => { - if (typeof obj[key] !== "object") { - data += `${indent}- ${key}: ${obj[key]}\n`; - } else { - data += `${indent}- ${key}:\n`; - if (Array.isArray(obj[key])) { - if (obj[key].length) { - data += - `${indent} - ` + obj[key].join(`\n${indent} - `) + "\n"; - } - } else { - data += this._objectToMarkdown(obj[key], ` ${indent}`); - } - } + private async _changeNetworkClicked(): Promise { + showNetworkDialog(this, { + network: this._networkInfo!, + loadData: () => this._loadData(), }); - - return data; } private async _changeHostnameClicked(): Promise { @@ -301,11 +371,29 @@ class HassioHostInfo extends LitElement { } catch (err) { showAlertDialog(this, { title: "Setting hostname failed", - text: err.body.message, + text: + typeof err === "object" ? err.body?.message || "Unkown error" : err, }); } } } + + private async _importFromUSB(): Promise { + try { + await configSyncOS(this.hass); + this.hostInfo = await fetchHassioHostInfo(this.hass); + } catch (err) { + showAlertDialog(this, { + title: "Failed to import from USB", + text: + typeof err === "object" ? err.body?.message || "Unkown error" : err, + }); + } + } + + private async _loadData(): Promise { + this._networkInfo = await fetchNetworkInfo(this.hass); + } } declare global { diff --git a/hassio/src/system/hassio-supervisor-info.ts b/hassio/src/system/hassio-supervisor-info.ts index b76f791c4d..560a04e534 100644 --- a/hassio/src/system/hassio-supervisor-info.ts +++ b/hassio/src/system/hassio-supervisor-info.ts @@ -6,24 +6,23 @@ import { html, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; -import { fireEvent } from "../../../src/common/dom/fire_event"; -import "../../../src/components/buttons/ha-call-api-button"; import "../../../src/components/ha-card"; +import "../../../src/components/ha-settings-row"; +import "../../../src/components/ha-switch"; import { HassioHostInfo as HassioHostInfoType } from "../../../src/data/hassio/host"; import { HassioSupervisorInfo as HassioSupervisorInfoType, + reloadSupervisor, setSupervisorOption, SupervisorOptions, + updateSupervisor, } from "../../../src/data/hassio/supervisor"; -import "../../../src/components/ha-switch"; import { - showConfirmationDialog, showAlertDialog, + showConfirmationDialog, } from "../../../src/dialogs/generic/show-dialog-box"; -import "../../../src/components/ha-settings-row"; import { haStyle } from "../../../src/resources/styles"; import { HomeAssistant } from "../../../src/types"; import { hassioStyle } from "../resources/hassio-style"; @@ -36,104 +35,108 @@ class HassioSupervisorInfo extends LitElement { @property() public hostInfo!: HassioHostInfoType; - @internalProperty() private _errors?: string; - - public render(): TemplateResult | void { + protected render(): TemplateResult | void { return html` - +
-

Supervisor

- - - - - - - - - - - ${this.supervisorInfo.channel !== "stable" - ? html` - - - - - ` - : ""} - -
Version${this.supervisorInfo.version}
Latest version${this.supervisorInfo.version_latest}
Channel${this.supervisorInfo.channel}
-
- ${this.supervisorInfo?.supported - ? html` - - Share Diagnostics - -
- Share crash reports and diagnostic information. - -
- -
` - : html`
- You are running an unsupported installation. - Learn More + + Version + + + ${this.supervisorInfo.version} + + + + + Newest version + + + ${this.supervisorInfo.version_latest} + + ${this.supervisorInfo.version !== this.supervisorInfo.version_latest + ? html` + -
`} -
- ${this._errors - ? html`
Error: ${this._errors}
` - : ""} + + ` + : ""} + + + + Channel + + + ${this.supervisorInfo.channel} + + ${this.supervisorInfo.channel === "beta" + ? html` + + + ` + : this.supervisorInfo.channel === "stable" + ? html` + + + ` + : ""} + + + ${this.supervisorInfo?.supported + ? html` + + Share diagnostics + +
+ Share crash reports and diagnostic information. + +
+ +
` + : html`
+ You are running an unsupported installation. + + Learn More + +
`}
- Reload - ${this.supervisorInfo.version !== this.supervisorInfo.version_latest - ? html` - Update - ` - : ""} - ${this.supervisorInfo.channel === "beta" - ? html` - Leave beta channel - ` - : ""} - ${this.supervisorInfo.channel === "stable" - ? html` - Join beta channel - ` - : ""} +
`; @@ -146,93 +149,103 @@ class HassioSupervisorInfo extends LitElement { css` ha-card { height: 100%; - width: 100%; + justify-content: space-between; + flex-direction: column; + display: flex; } - .card-content { - color: var(--primary-text-color); - box-sizing: border-box; - height: calc(100% - 47px); - } - .info, - .options { - width: 100%; - } - .info td:nth-child(2) { - text-align: right; - } - ha-settings-row { - padding: 0; + .card-actions { + height: 48px; + border-top: none; + display: flex; + justify-content: space-between; + align-items: center; } button.link { color: var(--primary-color); } - .diagnostics-description { - white-space: normal; + ha-settings-row { padding: 0; + height: 54px; + width: 100%; + } + ha-settings-row[three-line] { + height: 74px; + } + ha-settings-row > span[slot="description"] { + white-space: normal; color: var(--secondary-text-color); } `, ]; } - protected firstUpdated(): void { - this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev)); - } + private async _toggleBeta(): Promise { + if (this.supervisorInfo.channel === "stable") { + const confirmed = await showConfirmationDialog(this, { + title: "WARNING", + text: html` Beta releases are for testers and early adopters and can + contain unstable code changes. +
+ + Make sure you have backups of your data before you activate this + feature. + +

+ This includes beta releases for: +
  • Home Assistant Core
  • +
  • Home Assistant Supervisor
  • +
  • Home Assistant Operating System
  • +
    + Do you want to join the beta channel?`, + confirmText: "join beta", + dismissText: "no", + }); - private _apiCalled(ev): void { - if (ev.detail.success) { - this._errors = undefined; - return; - } - - const response = ev.detail.response; - - this._errors = - typeof response.body === "object" - ? response.body.message || "Unknown error" - : response.body; - } - - private async _joinBeta() { - const confirmed = await showConfirmationDialog(this, { - title: "WARNING", - text: html` Beta releases are for testers and early adopters and can - contain unstable code changes. -
    - - Make sure you have backups of your data before you activate this - feature. - -

    - This includes beta releases for: -
  • Home Assistant Core
  • -
  • Home Assistant Supervisor
  • -
  • Home Assistant Operating System
  • -
    - Do you want to join the beta channel?`, - confirmText: "join beta", - dismissText: "no", - }); - - if (!confirmed) { - return; + if (!confirmed) { + return; + } } try { - const data: SupervisorOptions = { channel: "beta" }; - await setSupervisorOption(this.hass, data); - const eventdata = { - success: true, - response: undefined, - path: "option", + const data: Partial = { + channel: this.supervisorInfo.channel !== "stable" ? "beta" : "stable", }; - fireEvent(this, "hass-api-called", eventdata); + await setSupervisorOption(this.hass, data); + await reloadSupervisor(this.hass); } catch (err) { - this._errors = `Error joining beta channel, ${err.body?.message || err}`; + showAlertDialog(this, { + title: "Failed to set supervisor option", + text: + typeof err === "object" ? err.body?.message || "Unkown error" : err, + }); } } - private async _diagnosticsInformationDialog() { + private async _supervisorReload(): Promise { + try { + await reloadSupervisor(this.hass); + } catch (err) { + showAlertDialog(this, { + title: "Failed to reload the supervisor", + text: + typeof err === "object" ? err.body?.message || "Unkown error" : err, + }); + } + } + + private async _supervisorUpdate(): Promise { + try { + await updateSupervisor(this.hass); + } catch (err) { + showAlertDialog(this, { + title: "Failed to update the supervisor", + text: + typeof err === "object" ? err.body.message || "Unkown error" : err, + }); + } + } + + private async _diagnosticsInformationDialog(): Promise { await showAlertDialog(this, { title: "Help Improve Home Assistant", text: html`Would you want to automatically share crash reports and @@ -247,22 +260,18 @@ class HassioSupervisorInfo extends LitElement { }); } - private async _toggleDiagnostics() { + private async _toggleDiagnostics(): Promise { try { const data: SupervisorOptions = { diagnostics: !this.supervisorInfo?.diagnostics, }; await setSupervisorOption(this.hass, data); - const eventdata = { - success: true, - response: undefined, - path: "option", - }; - fireEvent(this, "hass-api-called", eventdata); } catch (err) { - this._errors = `Error changing supervisor setting, ${ - err.body?.message || err - }`; + showAlertDialog(this, { + title: "Failed to set supervisor option", + text: + typeof err === "object" ? err.body.message || "Unkown error" : err, + }); } } } diff --git a/hassio/src/system/hassio-supervisor-log.ts b/hassio/src/system/hassio-supervisor-log.ts index ec2dbe7031..68fba0f1ab 100644 --- a/hassio/src/system/hassio-supervisor-log.ts +++ b/hassio/src/system/hassio-supervisor-log.ts @@ -7,18 +7,20 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; -import "../../../src/components/ha-card"; + import { fetchHassioLogs } from "../../../src/data/hassio/supervisor"; -import "../../../src/layouts/hass-loading-screen"; +import { hassioStyle } from "../resources/hassio-style"; import { haStyle } from "../../../src/resources/styles"; import { HomeAssistant } from "../../../src/types"; + +import "../../../src/components/ha-card"; +import "../../../src/layouts/hass-loading-screen"; import "../components/hassio-ansi-to-html"; -import { hassioStyle } from "../resources/hassio-style"; interface LogProvider { key: string; @@ -67,7 +69,7 @@ class HassioSupervisorLog extends LitElement { await this._loadData(); } - public render(): TemplateResult | void { + protected render(): TemplateResult | void { return html` ${this._error ? html`
    ${this._error}
    ` : ""} @@ -102,7 +104,7 @@ class HassioSupervisorLog extends LitElement { : html``}
    - Refresh + Refresh
    `; @@ -114,6 +116,7 @@ class HassioSupervisorLog extends LitElement { hassioStyle, css` ha-card { + margin-top: 8px; width: 100%; } pre { @@ -127,9 +130,6 @@ class HassioSupervisorLog extends LitElement { color: var(--error-color); margin-bottom: 16px; } - .card-content { - padding-top: 0px; - } `, ]; } @@ -142,7 +142,6 @@ class HassioSupervisorLog extends LitElement { private async _loadData(): Promise { this._error = undefined; - this._content = undefined; try { this._content = await fetchHassioLogs( @@ -151,14 +150,10 @@ class HassioSupervisorLog extends LitElement { ); } catch (err) { this._error = `Failed to get supervisor logs, ${ - err.body?.message || err + typeof err === "object" ? err.body?.message || "Unkown error" : err }`; } } - - private async _refresh(): Promise { - await this._loadData(); - } } declare global { diff --git a/hassio/src/system/hassio-system.ts b/hassio/src/system/hassio-system.ts index 3a52347a08..f2ffc2cc62 100644 --- a/hassio/src/system/hassio-system.ts +++ b/hassio/src/system/hassio-system.ts @@ -12,8 +12,8 @@ import { HassioHostInfo, } from "../../../src/data/hassio/host"; import { - HassioSupervisorInfo, HassioInfo, + HassioSupervisorInfo, } from "../../../src/data/hassio/supervisor"; import "../../../src/layouts/hass-tabs-subpage"; import { haStyle } from "../../../src/resources/styles"; @@ -40,7 +40,7 @@ class HassioSystem extends LitElement { @property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo; - public render(): TemplateResult | void { + protected render(): TemplateResult | void { return html` System
    -

    Information

    -

    System log

    diff --git a/package.json b/package.json index 5be452e00b..1410d6bf3b 100644 --- a/package.json +++ b/package.json @@ -114,6 +114,7 @@ "regenerator-runtime": "^0.13.2", "resize-observer-polyfill": "^1.5.1", "roboto-fontface": "^0.10.0", + "sortablejs": "^1.10.2", "superstruct": "^0.10.12", "unfetch": "^4.1.0", "vue": "^2.6.11", diff --git a/setup.py b/setup.py index 901c39ac1d..8ca7a0f6d6 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20200824.0", + version="20200901.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors", diff --git a/src/common/decorators/local-storage.ts b/src/common/decorators/local-storage.ts new file mode 100644 index 0000000000..99cdebdf3d --- /dev/null +++ b/src/common/decorators/local-storage.ts @@ -0,0 +1,68 @@ +import type { ClassElement } from "../../types"; + +class Storage { + private _storage: any = {}; + + public addFromStorage(storageKey: any): void { + if (!this._storage[storageKey]) { + const data = window.localStorage.getItem(storageKey); + if (data) { + this._storage[storageKey] = JSON.parse(data); + } + } + } + + public hasKey(storageKey: string): any { + return storageKey in this._storage; + } + + public getValue(storageKey: string): any { + return this._storage[storageKey]; + } + + public setValue(storageKey: string, value: any): any { + this._storage[storageKey] = value; + try { + window.localStorage.setItem(storageKey, JSON.stringify(value)); + } catch (err) { + // Safari in private mode doesn't allow localstorage + } + } +} + +const storage = new Storage(); + +export const LocalStorage = (key?: string) => { + return (element: ClassElement, propName: string) => { + const storageKey = key || propName; + const initVal = element.initializer ? element.initializer() : undefined; + + storage.addFromStorage(storageKey); + + const getValue = (): any => { + return storage.hasKey(storageKey) + ? storage.getValue(storageKey) + : initVal; + }; + + const setValue = (val: any) => { + storage.setValue(storageKey, val); + }; + + return { + kind: "method", + placement: "own", + key: element.key, + descriptor: { + set(value) { + setValue(value); + }, + get() { + return getValue(); + }, + enumerable: true, + configurable: true, + }, + }; + }; +}; diff --git a/src/common/dom/load_resource.ts b/src/common/dom/load_resource.ts index c4933ed8e5..f5288b886d 100644 --- a/src/common/dom/load_resource.ts +++ b/src/common/dom/load_resource.ts @@ -22,9 +22,6 @@ const _load = ( (element as HTMLScriptElement).async = true; if (type) { (element as HTMLScriptElement).type = type; - // https://github.com/home-assistant/frontend/pull/6328 - (element as HTMLScriptElement).crossOrigin = - url.substr(0, 1) === "/" ? "use-credentials" : "anonymous"; } break; case "link": diff --git a/src/components/buttons/ha-progress-button.js b/src/components/buttons/ha-progress-button.js deleted file mode 100644 index ea4f90184c..0000000000 --- a/src/components/buttons/ha-progress-button.js +++ /dev/null @@ -1,110 +0,0 @@ -import "@material/mwc-button"; -import "../ha-circular-progress"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -class HaProgressButton extends PolymerElement { - static get template() { - return html` - -
    - - - - -
    - `; - } - - static get properties() { - return { - hass: { - type: Object, - }, - - progress: { - type: Boolean, - value: false, - }, - - disabled: { - type: Boolean, - value: false, - }, - }; - } - - tempClass(className) { - const classList = this.$.container.classList; - classList.add(className); - setTimeout(() => { - classList.remove(className); - }, 1000); - } - - ready() { - super.ready(); - this.addEventListener("click", (ev) => this.buttonTapped(ev)); - } - - buttonTapped(ev) { - if (this.progress) ev.stopPropagation(); - } - - actionSuccess() { - this.tempClass("success"); - } - - actionError() { - this.tempClass("error"); - } - - computeDisabled(disabled, progress) { - return disabled || progress; - } -} - -customElements.define("ha-progress-button", HaProgressButton); diff --git a/src/components/buttons/ha-progress-button.ts b/src/components/buttons/ha-progress-button.ts new file mode 100644 index 0000000000..a446d456fc --- /dev/null +++ b/src/components/buttons/ha-progress-button.ts @@ -0,0 +1,114 @@ +import "@material/mwc-button"; +import type { Button } from "@material/mwc-button"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, + query, +} from "lit-element"; + +import "../ha-circular-progress"; + +@customElement("ha-progress-button") +class HaProgressButton extends LitElement { + @property({ type: Boolean }) public disabled = false; + + @property({ type: Boolean }) public progress = false; + + @property({ type: Boolean }) public raised = false; + + @query("mwc-button") private _button?: Button; + + public render(): TemplateResult { + return html` + + + + ${this.progress + ? html`
    + +
    ` + : ""} + `; + } + + public actionSuccess(): void { + this._tempClass("success"); + } + + public actionError(): void { + this._tempClass("error"); + } + + private _tempClass(className: string): void { + this._button!.classList.add(className); + setTimeout(() => { + this._button!.classList.remove(className); + }, 1000); + } + + private _buttonTapped(ev: Event): void { + if (this.progress) { + ev.stopPropagation(); + } + } + + static get styles(): CSSResult { + return css` + :host { + outline: none; + display: inline-block; + position: relative; + } + + mwc-button { + transition: all 1s; + } + + mwc-button.success { + --mdc-theme-primary: white; + background-color: var(--success-color); + transition: none; + } + + mwc-button[raised].success { + --mdc-theme-primary: var(--success-color); + --mdc-theme-on-primary: white; + } + + mwc-button.error { + --mdc-theme-primary: white; + background-color: var(--error-color); + transition: none; + } + + mwc-button[raised].error { + --mdc-theme-primary: var(--error-color); + --mdc-theme-on-primary: white; + } + + .progress { + bottom: 0; + margin-top: 4px; + position: absolute; + text-align: center; + top: 0; + width: 100%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-progress-button": HaProgressButton; + } +} diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index 69c30633b6..db83db5a6d 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -70,6 +70,7 @@ export interface DataTableColumnData extends DataTableSortColumnData { maxWidth?: string; grows?: boolean; forceLTR?: boolean; + hidden?: boolean; } export interface DataTableRowData { @@ -214,13 +215,15 @@ export class HaDataTable extends LitElement { class="mdc-data-table__table ${classMap({ "auto-height": this.autoHeight, })}" + role="table" + aria-rowcount=${this._filteredData.length} style=${styleMap({ height: this.autoHeight ? `${(this._filteredData.length || 1) * 53 + 57}px` : `calc(100% - ${this._header?.clientHeight}px)`, })} > -
    +
    ${this.selectable ? html`
    ` : ""} - ${Object.entries(this.columns).map((columnEntry) => { - const [key, column] = columnEntry; + ${Object.entries(this.columns).map(([key, column]) => { + if (column.hidden) { + return ""; + } const sorted = key === this._sortColumn; const classes = { "mdc-data-table__header-cell--numeric": Boolean( @@ -288,8 +293,8 @@ export class HaDataTable extends LitElement { ${!this._filteredData.length ? html`
    -
    -
    +
    +
    ${this.noDataText || "No data"}
    @@ -304,12 +309,14 @@ export class HaDataTable extends LitElement { items: !this.hasFab ? this._filteredData : [...this._filteredData, ...[{ empty: true }]], - renderItem: (row: DataTableRowData) => { + renderItem: (row: DataTableRowData, index) => { if (row.empty) { return html`
    `; } return html`
    ` : ""} - ${Object.entries(this.columns).map((columnEntry) => { - const [key, column] = columnEntry; - return html` -
    - ${column.template - ? column.template(row[key], row) - : row[key]} -
    - `; - })} + ${Object.entries(this.columns).map( + ([key, column]) => { + if (column.hidden) { + return ""; + } + return html` +
    + ${column.template + ? column.template(row[key], row) + : row[key]} +
    + `; + } + )}
    `; }, diff --git a/src/components/data-table/sort_filter_worker.ts b/src/components/data-table/sort_filter_worker.ts index 4c0d6d1987..1c04338a3c 100644 --- a/src/components/data-table/sort_filter_worker.ts +++ b/src/components/data-table/sort_filter_worker.ts @@ -1,11 +1,11 @@ // To use comlink under ES5 -import "proxy-polyfill"; import { expose } from "comlink"; +import "proxy-polyfill"; import type { - DataTableSortColumnData, DataTableRowData, - SortingDirection, + DataTableSortColumnData, SortableColumnContainer, + SortingDirection, } from "./ha-data-table"; const filterData = ( @@ -19,7 +19,7 @@ const filterData = ( const [key, column] = columnEntry; if (column.filterable) { if ( - (column.filterKey ? row[key][column.filterKey] : row[key]) + String(column.filterKey ? row[key][column.filterKey] : row[key]) .toUpperCase() .includes(filter) ) { diff --git a/src/components/ha-bar.ts b/src/components/ha-bar.ts new file mode 100644 index 0000000000..b9d160bf13 --- /dev/null +++ b/src/components/ha-bar.ts @@ -0,0 +1,67 @@ +import { + css, + CSSResult, + customElement, + LitElement, + property, + svg, + TemplateResult, +} from "lit-element"; + +import { + getValueInPercentage, + normalize, + roundWithOneDecimal, +} from "../util/calculate"; + +@customElement("ha-bar") +export class HaBar extends LitElement { + @property({ type: Number }) public min = 0; + + @property({ type: Number }) public max = 100; + + @property({ type: Number }) public value!: number; + + protected render(): TemplateResult { + const valuePrecentage = roundWithOneDecimal( + getValueInPercentage( + normalize(this.value, this.min, this.max), + this.min, + this.max + ) + ); + + return svg` + + + + + + + `; + } + + static get styles(): CSSResult { + return css` + rect:first-child { + width: 100%; + fill: var(--ha-bar-background-color, var(--secondary-background-color)); + } + rect:last-child { + fill: var(--ha-bar-primary-color, var(--primary-color)); + rx: var(--ha-bar-border-radius, 4px); + } + svg { + border-radius: var(--ha-bar-border-radius, 4px); + height: 12px; + width: 100%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-bar": HaBar; + } +} diff --git a/src/components/ha-circular-progress.ts b/src/components/ha-circular-progress.ts index e219f52e47..d28837872e 100644 --- a/src/components/ha-circular-progress.ts +++ b/src/components/ha-circular-progress.ts @@ -1,16 +1,16 @@ -import { - LitElement, - TemplateResult, - property, - svg, - html, - customElement, - unsafeCSS, - SVGTemplateResult, - css, -} from "lit-element"; // @ts-ignore import progressStyles from "@material/circular-progress/dist/mdc.circular-progress.min.css"; +import { + css, + customElement, + html, + LitElement, + property, + svg, + SVGTemplateResult, + TemplateResult, + unsafeCSS, +} from "lit-element"; import { classMap } from "lit-html/directives/class-map"; @customElement("ha-circular-progress") @@ -24,7 +24,7 @@ export class HaCircularProgress extends LitElement { @property() public size: "small" | "medium" | "large" = "medium"; - protected render(): TemplateResult | void { + protected render(): TemplateResult { let indeterminatePart: SVGTemplateResult; if (this.size === "small") { diff --git a/src/components/ha-code-editor.ts b/src/components/ha-code-editor.ts index d4d74adef5..452e75dffd 100644 --- a/src/components/ha-code-editor.ts +++ b/src/components/ha-code-editor.ts @@ -1,8 +1,8 @@ import { Editor } from "codemirror"; import { customElement, - property, internalProperty, + property, PropertyValues, UpdatingElement, } from "lit-element"; @@ -123,7 +123,7 @@ export class HaCodeEditor extends UpdatingElement { transition: 0.2s ease border-right; } .cm-s-default.CodeMirror { - background-color: var(--card-background-color); + background-color: var(--code-editor-background-color, var(--card-background-color)); color: var(--primary-text-color); } .cm-s-default .CodeMirror-cursor { diff --git a/src/components/ha-dialog.ts b/src/components/ha-dialog.ts index f18163ea7a..04dcbb1155 100644 --- a/src/components/ha-dialog.ts +++ b/src/components/ha-dialog.ts @@ -1,11 +1,11 @@ import "@material/mwc-dialog"; import type { Dialog } from "@material/mwc-dialog"; import { style } from "@material/mwc-dialog/mwc-dialog-css"; -import "./ha-icon-button"; -import { css, CSSResult, customElement, html } from "lit-element"; -import type { Constructor, HomeAssistant } from "../types"; import { mdiClose } from "@mdi/js"; +import { css, CSSResult, customElement, html } from "lit-element"; import { computeRTLDirection } from "../common/util/compute_rtl"; +import type { Constructor, HomeAssistant } from "../types"; +import "./ha-icon-button"; const MwcDialog = customElements.get("mwc-dialog") as Constructor; @@ -23,6 +23,10 @@ export const createCloseHeading = (hass: HomeAssistant, title: string) => html` @customElement("ha-dialog") export class HaDialog extends MwcDialog { + public scrollToPos(x: number, y: number) { + this.contentElement.scrollTo(x, y); + } + protected renderHeading() { return html` ${super.renderHeading()} @@ -62,6 +66,10 @@ export class HaDialog extends MwcDialog { position: var(--dialog-surface-position, relative); min-height: var(--mdc-dialog-min-height, auto); } + :host([flexContent]) .mdc-dialog .mdc-dialog__content { + display: flex; + flex-direction: column; + } .header_button { position: absolute; right: 16px; diff --git a/src/components/ha-gauge.ts b/src/components/ha-gauge.ts index 6cb3391f2e..5e1adbf351 100644 --- a/src/components/ha-gauge.ts +++ b/src/components/ha-gauge.ts @@ -11,23 +11,13 @@ import { styleMap } from "lit-html/directives/style-map"; import { afterNextRender } from "../common/util/render-status"; import { ifDefined } from "lit-html/directives/if-defined"; +import { getValueInPercentage, normalize } from "../util/calculate"; + const getAngle = (value: number, min: number, max: number) => { const percentage = getValueInPercentage(normalize(value, min, max), min, max); return (percentage * 180) / 100; }; -const normalize = (value: number, min: number, max: number) => { - if (value > max) return max; - if (value < min) return min; - return value; -}; - -const getValueInPercentage = (value: number, min: number, max: number) => { - const newMax = max - min; - const newVal = value - min; - return (100 * newVal) / newMax; -}; - // Workaround for https://github.com/home-assistant/frontend/issues/6467 const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); diff --git a/src/components/ha-settings-row.ts b/src/components/ha-settings-row.ts index 43c886ab99..0d53c58d29 100644 --- a/src/components/ha-settings-row.ts +++ b/src/components/ha-settings-row.ts @@ -25,7 +25,7 @@ export class HaSettingsRow extends LitElement {
    diff --git a/src/components/ha-sidebar-sort-styles.ts b/src/components/ha-sidebar-sort-styles.ts new file mode 100644 index 0000000000..b87b3c92ba --- /dev/null +++ b/src/components/ha-sidebar-sort-styles.ts @@ -0,0 +1,77 @@ +import { html } from "lit-element"; + +export const sortStyles = html` + +`; diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 4454ac122d..575cb4cc2f 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -1,9 +1,12 @@ +import "@material/mwc-button/mwc-button"; import "@material/mwc-icon-button"; import { mdiBell, mdiCellphoneCog, - mdiMenuOpen, + mdiClose, mdiMenu, + mdiMenuOpen, + mdiPlus, mdiViewDashboard, } from "@mdi/js"; import "@polymer/paper-item/paper-icon-item"; @@ -13,20 +16,24 @@ import "@polymer/paper-listbox/paper-listbox"; import { css, CSSResult, + customElement, eventOptions, html, - customElement, + internalProperty, LitElement, property, - internalProperty, PropertyValues, + TemplateResult, } from "lit-element"; import { classMap } from "lit-html/directives/class-map"; +import { guard } from "lit-html/directives/guard"; +import memoizeOne from "memoize-one"; +import { LocalStorage } from "../common/decorators/local-storage"; import { fireEvent } from "../common/dom/fire_event"; import { computeDomain } from "../common/entity/compute_domain"; import { compare } from "../common/string/compare"; import { computeRTL } from "../common/util/compute_rtl"; -import { getDefaultPanel } from "../data/panel"; +import { ActionHandlerDetail } from "../data/lovelace"; import { PersistentNotification, subscribeNotifications, @@ -35,6 +42,7 @@ import { ExternalConfig, getExternalConfig, } from "../external_app/external_config"; +import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; import type { HomeAssistant, PanelInfo } from "../types"; import "./ha-icon"; import "./ha-menu-button"; @@ -54,11 +62,39 @@ const SORT_VALUE_URL_PATHS = { config: 11, }; -const panelSorter = (a: PanelInfo, b: PanelInfo) => { +const panelSorter = ( + reverseSort: string[], + defaultPanel: string, + a: PanelInfo, + b: PanelInfo +) => { + const indexA = reverseSort.indexOf(a.url_path); + const indexB = reverseSort.indexOf(b.url_path); + if (indexA !== indexB) { + if (indexA < indexB) { + return 1; + } + return -1; + } + return defaultPanelSorter(defaultPanel, a, b); +}; + +const defaultPanelSorter = ( + defaultPanel: string, + a: PanelInfo, + b: PanelInfo +) => { // Put all the Lovelace at the top. const aLovelace = a.component_name === "lovelace"; const bLovelace = b.component_name === "lovelace"; + if (a.url_path === defaultPanel) { + return -1; + } + if (b.url_path === defaultPanel) { + return 1; + } + if (aLovelace && bLovelace) { return compare(a.title!, b.title!); } @@ -85,30 +121,45 @@ const panelSorter = (a: PanelInfo, b: PanelInfo) => { return compare(a.title!, b.title!); }; -const computePanels = (hass: HomeAssistant): [PanelInfo[], PanelInfo[]] => { - const panels = hass.panels; - if (!panels) { - return [[], []]; - } - - const beforeSpacer: PanelInfo[] = []; - const afterSpacer: PanelInfo[] = []; - - Object.values(panels).forEach((panel) => { - if (!panel.title || panel.url_path === hass.defaultPanel) { - return; +const computePanels = memoizeOne( + ( + panels: HomeAssistant["panels"], + defaultPanel: HomeAssistant["defaultPanel"], + panelsOrder: string[], + hiddenPanels: string[] + ): [PanelInfo[], PanelInfo[]] => { + if (!panels) { + return [[], []]; } - (SHOW_AFTER_SPACER.includes(panel.url_path) - ? afterSpacer - : beforeSpacer - ).push(panel); - }); - beforeSpacer.sort(panelSorter); - afterSpacer.sort(panelSorter); + const beforeSpacer: PanelInfo[] = []; + const afterSpacer: PanelInfo[] = []; - return [beforeSpacer, afterSpacer]; -}; + Object.values(panels).forEach((panel) => { + if ( + hiddenPanels.includes(panel.url_path) || + (!panel.title && panel.url_path !== defaultPanel) + ) { + return; + } + (SHOW_AFTER_SPACER.includes(panel.url_path) + ? afterSpacer + : beforeSpacer + ).push(panel); + }); + + const reverseSort = [...panelsOrder].reverse(); + + beforeSpacer.sort((a, b) => panelSorter(reverseSort, defaultPanel, a, b)); + afterSpacer.sort((a, b) => panelSorter(reverseSort, defaultPanel, a, b)); + + return [beforeSpacer, afterSpacer]; + } +); + +let Sortable; + +let sortStyles: TemplateResult; @customElement("ha-sidebar") class HaSidebar extends LitElement { @@ -124,16 +175,30 @@ class HaSidebar extends LitElement { @internalProperty() private _notifications?: PersistentNotification[]; + @internalProperty() private _editMode = false; + // property used only in css // @ts-ignore @property({ type: Boolean, reflect: true }) public rtl = false; + @internalProperty() private _renderEmptySortable = false; + private _mouseLeaveTimeout?: number; private _tooltipHideTimeout?: number; private _recentKeydownActiveUntil = 0; + // @ts-ignore + @LocalStorage("sidebarPanelOrder") + private _panelOrder: string[] = []; + + // @ts-ignore + @LocalStorage("sidebarHiddenPanels") + private _hiddenPanels: string[] = []; + + private _sortable?; + protected render() { const hass = this.hass; @@ -141,7 +206,12 @@ class HaSidebar extends LitElement { return html``; } - const [beforeSpacer, afterSpacer] = computePanels(hass); + const [beforeSpacer, afterSpacer] = computePanels( + hass.panels, + hass.defaultPanel, + this._panelOrder, + this._hiddenPanels + ); let notificationCount = this._notifications ? this._notifications.length @@ -152,9 +222,8 @@ class HaSidebar extends LitElement { } } - const defaultPanel = getDefaultPanel(hass); - return html` + ${this._editMode ? sortStyles : ""} - ${this._renderPanel( - defaultPanel.url_path, - defaultPanel.title || hass.localize("panel.states"), - defaultPanel.icon, - !defaultPanel.icon ? mdiViewDashboard : undefined - )} - ${beforeSpacer.map((panel) => - this._renderPanel( - panel.url_path, - hass.localize(`panel.${panel.title}`) || panel.title, - panel.icon, - undefined - ) - )} + ${this._editMode + ? html`
    + ${guard([this._hiddenPanels, this._renderEmptySortable], () => + this._renderEmptySortable + ? "" + : this._renderPanels(beforeSpacer) + )} +
    ` + : this._renderPanels(beforeSpacer)}
    - - ${afterSpacer.map((panel) => - this._renderPanel( - panel.url_path, - hass.localize(`panel.${panel.title}`) || panel.title, - panel.icon, - undefined - ) - )} + ${this._editMode && this._hiddenPanels.length + ? html` + ${this._hiddenPanels.map((url) => { + const panel = this.hass.panels[url]; + return html` + + ${panel.url_path === "lovelace" + ? hass.localize("panel.states") + : hass.localize(`panel.${panel.title}`) || + panel.title} + + `; + })} +
    + ` + : ""} + ${this._renderPanels(afterSpacer)} ${this._externalConfig && this._externalConfig.hasSettingsScreen ? html` ) { + if (ev.detail.action !== "hold") { + return; + } + + if (!Sortable) { + const [sortableImport, sortStylesImport] = await Promise.all([ + import("sortablejs/modular/sortable.core.esm"), + import("./ha-sidebar-sort-styles"), + ]); + + sortStyles = sortStylesImport.sortStyles; + + Sortable = sortableImport.Sortable; + Sortable.mount(sortableImport.OnSpill); + Sortable.mount(sortableImport.AutoScroll()); + } + this._editMode = true; + + await this.updateComplete; + + this._createSortable(); + } + + private _createSortable() { + this._sortable = new Sortable(this.shadowRoot!.getElementById("sortable"), { + animation: 150, + fallbackClass: "sortable-fallback", + dataIdAttr: "data-panel", + onSort: async () => { + this._panelOrder = this._sortable.toArray(); + }, + }); + } + + private _closeEditMode() { + this._sortable?.destroy(); + this._sortable = undefined; + this._editMode = false; + } + + private async _hidePanel(ev: Event) { + ev.preventDefault(); + const panel = (ev.target as any).panel; + if (this._hiddenPanels.includes(panel)) { + return; + } + // Make a copy for Memoize + this._hiddenPanels = [...this._hiddenPanels, panel]; + this._renderEmptySortable = true; + await this.updateComplete; + this._renderEmptySortable = false; + } + + private async _unhidePanel(ev: Event) { + ev.preventDefault(); + const index = this._hiddenPanels.indexOf((ev.target as any).panel); + if (index < 0) { + return; + } + this._hiddenPanels.splice(index, 1); + // Make a copy for Memoize + this._hiddenPanels = [...this._hiddenPanels]; + this._renderEmptySortable = true; + await this.updateComplete; + this._renderEmptySortable = false; + } + private _itemMouseEnter(ev: MouseEvent) { // On keypresses on the listbox, we're going to ignore mouse enter events // for 100ms so that we ignore it when pressing down arrow scrolls the @@ -457,6 +624,19 @@ class HaSidebar extends LitElement { fireEvent(this, "hass-toggle-menu"); } + private _renderPanels(panels: PanelInfo[]) { + return panels.map((panel) => + this._renderPanel( + panel.url_path, + panel.url_path === "lovelace" + ? this.hass.localize("panel.states") + : this.hass.localize(`panel.${panel.title}`) || panel.title, + panel.url_path === "lovelace" ? undefined : panel.icon, + panel.url_path === "lovelace" ? mdiViewDashboard : undefined + ) + ); + } + private _renderPanel( urlPath: string, title: string | null, @@ -480,6 +660,14 @@ class HaSidebar extends LitElement { >` : html``} ${title} + ${this._editMode + ? html`` + : ""} `; @@ -542,11 +730,15 @@ class HaSidebar extends LitElement { } .title { + width: 100%; display: none; } :host([expanded]) .title { display: initial; } + .title mwc-button { + width: 100%; + } paper-listbox::-webkit-scrollbar { width: 0.4rem; diff --git a/src/components/media-player/dialog-media-player-browse.ts b/src/components/media-player/dialog-media-player-browse.ts index c686f5ef94..5f4d14649a 100644 --- a/src/components/media-player/dialog-media-player-browse.ts +++ b/src/components/media-player/dialog-media-player-browse.ts @@ -15,7 +15,7 @@ import type { } from "../../data/media-player"; import { haStyleDialog } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; -import { createCloseHeading } from "../ha-dialog"; +import "../ha-dialog"; import "./ha-media-player-browse"; import { MediaPlayerBrowseDialogParams } from "./show-media-browser-dialog"; @@ -56,18 +56,17 @@ class DialogMediaPlayerBrowse extends LitElement { scrimClickAction escapeKeyAction hideActions - .heading=${createCloseHeading( - this.hass, - this.hass.localize("ui.components.media-browser.media-player-browser") - )} + flexContent @closed=${this._closeDialog} > @@ -94,13 +93,20 @@ class DialogMediaPlayerBrowse extends LitElement { --dialog-content-padding: 0; } + ha-header-bar { + --mdc-theme-on-primary: var(--primary-text-color); + --mdc-theme-primary: var(--mdc-theme-surface); + flex-shrink: 0; + border-bottom: 1px solid + var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12)); + } + @media (min-width: 800px) { ha-dialog { --mdc-dialog-max-width: 800px; } ha-media-player-browse { width: 700px; - padding: 20px 24px; } } `, diff --git a/src/components/media-player/ha-media-player-browse.ts b/src/components/media-player/ha-media-player-browse.ts index f6f2f08df3..c485a96453 100644 --- a/src/components/media-player/ha-media-player-browse.ts +++ b/src/components/media-player/ha-media-player-browse.ts @@ -2,7 +2,7 @@ import "@material/mwc-button/mwc-button"; import "@material/mwc-fab/mwc-fab"; import "@material/mwc-list/mwc-list"; import "@material/mwc-list/mwc-list-item"; -import { mdiArrowLeft, mdiFolder, mdiPlay, mdiPlus } from "@mdi/js"; +import { mdiArrowLeft, mdiClose, mdiFolder, mdiPlay, mdiPlus } from "@mdi/js"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-listbox/paper-listbox"; import { @@ -16,8 +16,11 @@ import { PropertyValues, TemplateResult, } from "lit-element"; +import { classMap } from "lit-html/directives/class-map"; +import { ifDefined } from "lit-html/directives/if-defined"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../common/dom/fire_event"; +import { computeRTLDirection } from "../../common/util/compute_rtl"; import { debounce } from "../../common/util/debounce"; import { browseMediaPlayer, MediaPickedEvent } from "../../data/media-player"; import type { MediaPlayerItem } from "../../data/media-player"; @@ -49,6 +52,12 @@ export class HaMediaPlayerBrowse extends LitElement { @property() public action: "pick" | "play" = "play"; + @property({ type: Boolean }) public hideBack = false; + + @property({ type: Boolean }) public hideTitle = false; + + @property({ type: Boolean }) public dialog = false; + @property({ type: Boolean, attribute: "narrow", reflect: true }) private _narrow = false; @@ -69,6 +78,15 @@ export class HaMediaPlayerBrowse extends LitElement { } } + public navigateBack() { + this._mediaPlayerItems!.pop(); + const item = this._mediaPlayerItems!.pop(); + if (!item) { + return; + } + this._navigate(item); + } + protected render(): TemplateResult { if (!this._mediaPlayerItems.length) { return html``; @@ -90,8 +108,20 @@ export class HaMediaPlayerBrowse extends LitElement { | MediaPlayerItem | undefined = this._hasExpandableChildren(mostRecentItem.children); + const showImages = mostRecentItem.children?.some( + (child) => child.thumbnail && child.thumbnail !== mostRecentItem.thumbnail + ); + + const mediaType = this.hass.localize( + `ui.components.media-browser.content-type.${mostRecentItem.media_content_type}` + ); + return html` -
    +
    ${mostRecentItem.thumbnail ? html` @@ -123,56 +153,65 @@ export class HaMediaPlayerBrowse extends LitElement { ` : html``}
    - - ${mostRecentItem?.can_play && - (!this._narrow || (this._narrow && !mostRecentItem.thumbnail)) - ? html` -
    - - - ${this.hass.localize( - `ui.components.media-browser.${this.action}` - )} - + ${this.hideTitle && (this._narrow || !mostRecentItem.thumbnail) + ? "" + : html``} + ${mostRecentItem?.can_play && + (!mostRecentItem.thumbnail || !this._narrow) + ? html` + + + ${this.hass.localize( + `ui.components.media-browser.${this.action}` + )} + ` : ""}
    + ${this.dialog + ? html` + + + + ` + : ""}
    -
    ${mostRecentItem.children?.length ? hasExpandableChildren ? html` @@ -184,7 +223,7 @@ export class HaMediaPlayerBrowse extends LitElement {
    ${mostRecentItem.children.map( - (child) => html` html` + - ${child.title} - -
  • ` + slot="graphic" + > + + + +
    + ${child.title} + +
  • + ` )} ` @@ -260,6 +319,11 @@ export class HaMediaPlayerBrowse extends LitElement { protected firstUpdated(): void { this._measureCard(); this._attachObserver(); + + this.addEventListener("scroll", this._scroll, { passive: true }); + this.addEventListener("touchmove", this._scroll, { + passive: true, + }); } protected updated(changedProps: PropertyValues): void { @@ -295,25 +359,23 @@ export class HaMediaPlayerBrowse extends LitElement { }); } - private async _navigate(ev: MouseEvent): Promise { + private async _navigateForward(ev: MouseEvent): Promise { const target = ev.currentTarget as any; - let item: MediaPlayerItem | undefined; - - if (target.previous) { - this._mediaPlayerItems!.pop(); - item = this._mediaPlayerItems!.pop(); - } - - item = target.item; + const item: MediaPlayerItem = target.item; if (!item) { return; } + this._navigate(item); + } + private async _navigate(item: MediaPlayerItem) { const itemData = await this._fetchData( item.media_content_id, item.media_content_type ); + + this.scrollTo(0, 0); this._mediaPlayerItems = [...this._mediaPlayerItems, itemData]; } @@ -332,7 +394,15 @@ export class HaMediaPlayerBrowse extends LitElement { } private _measureCard(): void { - this._narrow = this.offsetWidth < 500; + this._narrow = (this.dialog ? window.innerWidth : this.offsetWidth) < 450; + } + + private _scroll(): void { + if (this.scrollTop > (this._narrow ? 224 : 125)) { + this.setAttribute("scroll", ""); + } else if (this.scrollTop === 0) { + this.removeAttribute("scroll"); + } } private async _attachObserver(): Promise { @@ -350,22 +420,40 @@ export class HaMediaPlayerBrowse extends LitElement { children.find((item: MediaPlayerItem) => item.can_expand) ); + private _closeDialogAction(): void { + fireEvent(this, "close-dialog"); + } + static get styles(): CSSResultArray { return [ haStyle, css` :host { display: block; + overflow-y: auto; + display: flex; + padding: 0px 0px 20px; + flex-direction: column; } .header { display: flex; justify-content: space-between; + border-bottom: 1px solid var(--divider-color); } - .breadcrumb-overflow { - display: flex; - justify-content: space-between; + .header_button { + position: relative; + top: 14px; + right: -8px; + } + + .header { + background-color: var(--card-background-color); + position: sticky; + top: 0; + z-index: 5; + padding: 20px 24px 10px; } .header-content { @@ -380,6 +468,8 @@ export class HaMediaPlayerBrowse extends LitElement { width: 200px; margin-right: 16px; background-size: cover; + border-radius: 4px; + transition: width 0.4s, height 0.4s; } .header-info { @@ -391,9 +481,14 @@ export class HaMediaPlayerBrowse extends LitElement { flex: 1; } - .header-info .actions { - padding-top: 24px; - --mdc-theme-primary: var(--primary-color); + .header-info mwc-button { + display: block; + } + + .breadcrumb-overflow { + display: flex; + flex-grow: 1; + justify-content: space-between; } .breadcrumb { @@ -404,7 +499,7 @@ export class HaMediaPlayerBrowse extends LitElement { } .breadcrumb .title { - font-size: 48px; + font-size: 32px; line-height: 1.2; font-weight: bold; margin: 0; @@ -412,6 +507,7 @@ export class HaMediaPlayerBrowse extends LitElement { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; + padding-right: 8px; } .breadcrumb .previous-title { @@ -428,17 +524,8 @@ export class HaMediaPlayerBrowse extends LitElement { font-size: 16px; overflow: hidden; text-overflow: ellipsis; - } - - .divider { - padding: 10px 0; - } - - .divider::before { - height: 1px; - display: block; - background-color: var(--divider-color); - content: " "; + margin-bottom: 0; + transition: height 0.5s, margin 0.5s; } /* ============= CHILDREN ============= */ @@ -446,8 +533,7 @@ export class HaMediaPlayerBrowse extends LitElement { mwc-list { --mdc-list-vertical-padding: 0; --mdc-theme-text-icon-on-background: var(--secondary-text-color); - border: 1px solid var(--divider-color); - border-radius: 4px; + margin-top: 10px; } mwc-list li:last-child { @@ -468,6 +554,10 @@ export class HaMediaPlayerBrowse extends LitElement { margin: 8px 0px; } + :host(:not([narrow])) .children { + padding: 0px 24px; + } + .child { display: flex; flex-direction: column; @@ -483,7 +573,9 @@ export class HaMediaPlayerBrowse extends LitElement { width: 100%; padding-bottom: 100%; position: relative; + box-sizing: border-box; background-size: cover; + background-repeat: no-repeat; background-position: center; } @@ -503,7 +595,7 @@ export class HaMediaPlayerBrowse extends LitElement { bottom: 4px; right: 4px; transition: all 0.5s; - background-color: rgba(255, 255, 255, 0.5); + background-color: rgba(var(--rgb-card-background-color), 0.5); border-radius: 50%; } @@ -529,22 +621,46 @@ export class HaMediaPlayerBrowse extends LitElement { color: var(--secondary-text-color); } + mwc-list-item .graphic { + background-size: cover; + } + + mwc-list-item .graphic .play { + opacity: 0; + transition: all 0.5s; + background-color: rgba(var(--rgb-card-background-color), 0.5); + border-radius: 50%; + --mdc-icon-button-size: 40px; + } + + mwc-list-item:hover .graphic .play { + opacity: 1; + color: var(--primary-color); + } + + mwc-list-item .graphic .play.show { + opacity: 1; + background-color: transparent; + } + /* ============= Narrow ============= */ :host([narrow]) { padding: 0; } - :host([narrow]) mwc-list { - border: 0; - } - :host([narrow]) .breadcrumb .title { - font-size: 38px; + font-size: 24px; } - :host([narrow]) .breadcrumb-overflow { - align-items: flex-end; + :host([narrow]) .header { + padding: 0; + } + + :host([narrow]) .header_button { + position: absolute; + top: 14px; + right: 8px; } :host([narrow]) .header-content { @@ -556,26 +672,100 @@ export class HaMediaPlayerBrowse extends LitElement { height: auto; width: 100%; margin-right: 0; - padding-bottom: 100%; + padding-bottom: 50%; margin-bottom: 8px; position: relative; + background-position: center; + border-radius: 0; + transition: width 0.4s, height 0.4s, padding-bottom 0.4s; } - :host([narrow]) .header-content .img mwc-fab { + mwc-fab { position: absolute; --mdc-theme-secondary: var(--primary-color); bottom: -20px; right: 20px; } - :host([narrow]) .header-info, + :host([narrow]) .header-info mwc-button { + margin-top: 16px; + margin-bottom: 8px; + } + + :host([narrow]) .header-info { + padding: 20px 24px 10px; + } + :host([narrow]) .media-source, :host([narrow]) .children { padding: 0 24px; } :host([narrow]) .children { - grid-template-columns: 1fr 1fr !important; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) !important; + } + + /* ============= Scroll ============= */ + + :host([scroll]) .breadcrumb .subtitle { + height: 0; + margin: 0; + } + + :host([scroll]) .breadcrumb .title { + -webkit-line-clamp: 1; + } + + :host([scroll]) .header-info mwc-button, + .no-img .header-info mwc-button { + padding-right: 4px; + } + + :host([scroll][narrow]) .no-img .header-info mwc-button { + padding-right: 16px; + } + + :host([scroll]) .header-info { + flex-direction: row; + } + + :host([scroll]) .header-info mwc-button { + align-self: center; + margin-top: 0; + margin-bottom: 0; + } + + :host([scroll][narrow]) .no-img .header-info { + flex-direction: row-reverse; + } + + :host([scroll][narrow]) .header-info { + padding: 20px 24px 10px 24px; + align-items: center; + } + + :host([scroll]) .header-content { + align-items: flex-end; + flex-direction: row; + } + + :host([scroll]) .header-content .img { + height: 75px; + width: 75px; + } + + :host([scroll][narrow]) .header-content .img { + height: 100px; + width: 100px; + padding-bottom: initial; + margin-bottom: 0; + } + + :host([scroll]) mwc-fab { + bottom: 4px; + right: 4px; + --mdc-fab-box-shadow: none; + --mdc-theme-secondary: rgba(var(--rgb-primary-color), 0.5); } `, ]; diff --git a/src/data/auth.ts b/src/data/auth.ts index 162ade2fbd..aea90dd335 100644 --- a/src/data/auth.ts +++ b/src/data/auth.ts @@ -44,3 +44,14 @@ export const createAuthForUser = async ( username, password, }); + +export const adminChangePassword = async ( + hass: HomeAssistant, + userId: string, + password: string +) => + hass.callWS({ + type: "config/auth_provider/homeassistant/admin_change_password", + user_id: userId, + password, + }); diff --git a/src/data/automation.ts b/src/data/automation.ts index d4eaa843c7..97395806db 100644 --- a/src/data/automation.ts +++ b/src/data/automation.ts @@ -3,7 +3,7 @@ import { HassEntityBase, } from "home-assistant-js-websocket"; import { navigate } from "../common/navigate"; -import { HomeAssistant } from "../types"; +import { HomeAssistant, Context } from "../types"; import { DeviceCondition, DeviceTrigger } from "./device_automation"; import { Action } from "./script"; @@ -206,3 +206,31 @@ export const getAutomationEditorInitData = () => { inititialAutomationEditorData = undefined; return data; }; + +export const subscribeTrigger = ( + hass: HomeAssistant, + onChange: (result: { + variables: { + trigger: {}; + }; + context: Context; + }) => void, + trigger: Trigger | Trigger[], + variables?: {} +) => + hass.connection.subscribeMessage(onChange, { + type: "subscribe_trigger", + trigger, + variables, + }); + +export const testCondition = ( + hass: HomeAssistant, + condition: Condition | Condition[], + variables?: {} +) => + hass.callWS<{ result: boolean }>({ + type: "test_condition", + condition, + variables, + }); diff --git a/src/data/cloud.ts b/src/data/cloud.ts index c642841584..e261805f50 100644 --- a/src/data/cloud.ts +++ b/src/data/cloud.ts @@ -9,14 +9,14 @@ interface CloudStatusBase { } export interface GoogleEntityConfig { - should_expose?: boolean; + should_expose?: boolean | null; override_name?: string; aliases?: string[]; disable_2fa?: boolean; } export interface AlexaEntityConfig { - should_expose?: boolean; + should_expose?: boolean | null; } export interface CertificateInformation { @@ -31,9 +31,11 @@ export interface CloudPreferences { remote_enabled: boolean; google_secure_devices_pin: string | undefined; cloudhooks: { [webhookId: string]: CloudWebhook }; + google_default_expose: string[] | null; google_entity_configs: { [entityId: string]: GoogleEntityConfig; }; + alexa_default_expose: string[] | null; alexa_entity_configs: { [entityId: string]: AlexaEntityConfig; }; @@ -106,8 +108,10 @@ export const updateCloudPref = ( prefs: { google_enabled?: CloudPreferences["google_enabled"]; alexa_enabled?: CloudPreferences["alexa_enabled"]; + alexa_default_expose?: CloudPreferences["alexa_default_expose"]; alexa_report_state?: CloudPreferences["alexa_report_state"]; google_report_state?: CloudPreferences["google_report_state"]; + google_default_expose?: CloudPreferences["google_default_expose"]; google_secure_devices_pin?: CloudPreferences["google_secure_devices_pin"]; } ) => diff --git a/src/data/config_entries.ts b/src/data/config_entries.ts index 527838ff3b..925d9cc9b1 100644 --- a/src/data/config_entries.ts +++ b/src/data/config_entries.ts @@ -8,6 +8,7 @@ export interface ConfigEntry { state: string; connection_class: string; supports_options: boolean; + supports_unload: boolean; } export interface ConfigEntryMutableParams { @@ -37,6 +38,11 @@ export const deleteConfigEntry = (hass: HomeAssistant, configEntryId: string) => require_restart: boolean; }>("DELETE", `config/config_entries/entry/${configEntryId}`); +export const reloadConfigEntry = (hass: HomeAssistant, configEntryId: string) => + hass.callApi<{ + require_restart: boolean; + }>("POST", `config/config_entries/entry/${configEntryId}/reload`); + export const getConfigEntrySystemOptions = ( hass: HomeAssistant, configEntryId: string diff --git a/src/data/hassio/addon.ts b/src/data/hassio/addon.ts index a6c20faf54..baa2e95f29 100644 --- a/src/data/hassio/addon.ts +++ b/src/data/hassio/addon.ts @@ -72,6 +72,7 @@ export interface HassioAddonDetails extends HassioAddonInfo { ingress_panel: boolean; ingress_entry: null | string; ingress_url: null | string; + watchdog: null | boolean; } export interface HassioAddonsInfo { @@ -99,6 +100,7 @@ export interface HassioAddonSetOptionParams { auto_update?: boolean; ingress_panel?: boolean; network?: object | null; + watchdog?: boolean; } export const reloadHassioAddons = async (hass: HomeAssistant) => { diff --git a/src/data/hassio/host.ts b/src/data/hassio/host.ts index bdefe66dde..a8db6dbc71 100644 --- a/src/data/hassio/host.ts +++ b/src/data/hassio/host.ts @@ -40,6 +40,10 @@ export const updateOS = async (hass: HomeAssistant) => { return hass.callApi>("POST", "hassio/os/update"); }; +export const configSyncOS = async (hass: HomeAssistant) => { + return hass.callApi>("POST", "hassio/os/config/sync"); +}; + export const changeHostOptions = async (hass: HomeAssistant, options: any) => { return hass.callApi>( "POST", diff --git a/src/data/hassio/network.ts b/src/data/hassio/network.ts new file mode 100644 index 0000000000..5e2e7c20e1 --- /dev/null +++ b/src/data/hassio/network.ts @@ -0,0 +1,43 @@ +import { HomeAssistant } from "../../types"; +import { hassioApiResultExtractor, HassioResponse } from "./common"; + +export interface NetworkInterface { + gateway: string; + id: string; + ip_address: string; + address?: string; + method: "static" | "dhcp"; + nameservers: string[] | string; + dns?: string[]; + primary: boolean; + type: string; +} + +export interface NetworkInterfaces { + [key: string]: NetworkInterface; +} + +export interface NetworkInfo { + interfaces: NetworkInterfaces; +} + +export const fetchNetworkInfo = async (hass: HomeAssistant) => { + return hassioApiResultExtractor( + await hass.callApi>( + "GET", + "hassio/network/info" + ) + ); +}; + +export const updateNetworkInterface = async ( + hass: HomeAssistant, + network_interface: string, + options: Partial +) => { + await hass.callApi>( + "POST", + `hassio/network/interface/${network_interface}/update`, + options + ); +}; diff --git a/src/data/hassio/supervisor.ts b/src/data/hassio/supervisor.ts index aaf854ac81..992425b8bc 100644 --- a/src/data/hassio/supervisor.ts +++ b/src/data/hassio/supervisor.ts @@ -35,6 +35,14 @@ export interface SupervisorOptions { addons_repositories?: string[]; } +export const reloadSupervisor = async (hass: HomeAssistant) => { + await hass.callApi>("POST", `hassio/supervisor/reload`); +}; + +export const updateSupervisor = async (hass: HomeAssistant) => { + await hass.callApi>("POST", `hassio/supervisor/update`); +}; + export const fetchHassioHomeAssistantInfo = async (hass: HomeAssistant) => { return hassioApiResultExtractor( await hass.callApi>( diff --git a/src/data/lovelace.ts b/src/data/lovelace.ts index 234825d018..4431730218 100644 --- a/src/data/lovelace.ts +++ b/src/data/lovelace.ts @@ -318,10 +318,11 @@ export interface WindowWithLovelaceProm extends Window { export interface ActionHandlerOptions { hasHold?: boolean; hasDoubleClick?: boolean; + disabled?: boolean; } export interface ActionHandlerDetail { - action: string; + action: "hold" | "tap" | "double_tap"; } export type ActionHandlerEvent = HASSDomEvent; diff --git a/src/data/weather.ts b/src/data/weather.ts index 199a5e68e9..369a64ec34 100644 --- a/src/data/weather.ts +++ b/src/data/weather.ts @@ -2,6 +2,7 @@ import { SVGTemplateResult, svg, html, TemplateResult, css } from "lit-element"; import { styleMap } from "lit-html/directives/style-map"; import type { HomeAssistant, WeatherEntity } from "../types"; +import { roundWithOneDecimal } from "../util/calculate"; export const weatherSVGs = new Set([ "clear-night", @@ -135,7 +136,7 @@ export const getSecondaryWeatherAttribute = ( return ` ${hass!.localize( `ui.card.weather.attributes.${attribute}` - )} ${value} ${getWeatherUnit(hass!, attribute)} + )} ${roundWithOneDecimal(value)} ${getWeatherUnit(hass!, attribute)} `; }; diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index 976a98d301..79a95c2bc2 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -1,21 +1,22 @@ import "@material/mwc-button"; import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; -import "../../components/ha-icon-button"; -import "../../components/ha-circular-progress"; -import "@polymer/paper-tooltip/paper-tooltip"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultArray, customElement, html, - LitElement, internalProperty, + LitElement, PropertyValues, TemplateResult, } from "lit-element"; +import { fireEvent } from "../../common/dom/fire_event"; +import { computeRTL } from "../../common/util/compute_rtl"; +import "../../components/ha-circular-progress"; import "../../components/ha-dialog"; import "../../components/ha-form/ha-form"; +import "../../components/ha-icon-button"; import "../../components/ha-markdown"; import { AreaRegistryEntry, @@ -35,8 +36,6 @@ import "./step-flow-external"; import "./step-flow-form"; import "./step-flow-loading"; import "./step-flow-pick-handler"; -import { fireEvent } from "../../common/dom/fire_event"; -import { computeRTL } from "../../common/util/compute_rtl"; let instance = 0; diff --git a/src/dialogs/config-flow/step-flow-form.ts b/src/dialogs/config-flow/step-flow-form.ts index a0d13a98e6..660595b2f3 100644 --- a/src/dialogs/config-flow/step-flow-form.ts +++ b/src/dialogs/config-flow/step-flow-form.ts @@ -1,5 +1,4 @@ import "@material/mwc-button"; -import "../../components/ha-circular-progress"; import "@polymer/paper-tooltip/paper-tooltip"; import { css, @@ -12,6 +11,7 @@ import { TemplateResult, } from "lit-element"; import { fireEvent } from "../../common/dom/fire_event"; +import "../../components/ha-circular-progress"; import "../../components/ha-form/ha-form"; import type { HaFormSchema } from "../../components/ha-form/ha-form"; import "../../components/ha-markdown"; @@ -91,7 +91,7 @@ class StepFlowForm extends LitElement { ${!allRequiredInfoFilledIn ? html` - ${this.hass.localize( "ui.panel.config.integrations.config_flow.not_all_required_fields" )} diff --git a/src/dialogs/domain-toggler/dialog-domain-toggler.ts b/src/dialogs/domain-toggler/dialog-domain-toggler.ts index 326fb40c01..4b61bac3ab 100644 --- a/src/dialogs/domain-toggler/dialog-domain-toggler.ts +++ b/src/dialogs/domain-toggler/dialog-domain-toggler.ts @@ -4,27 +4,35 @@ import { CSSResultArray, customElement, html, - LitElement, internalProperty, + LitElement, TemplateResult, } from "lit-element"; -import "../../components/dialog/ha-paper-dialog"; +import { fireEvent } from "../../common/dom/fire_event"; +import { createCloseHeading } from "../../components/ha-dialog"; +import "../../components/ha-switch"; +import "../../components/ha-formfield"; import { domainToName } from "../../data/integration"; -import { PolymerChangedEvent } from "../../polymer-types"; import { haStyleDialog } from "../../resources/styles"; import { HomeAssistant } from "../../types"; +import { HassDialog } from "../make-dialog-manager"; import { HaDomainTogglerDialogParams } from "./show-dialog-domain-toggler"; @customElement("dialog-domain-toggler") -class DomainTogglerDialog extends LitElement { +class DomainTogglerDialog extends LitElement implements HassDialog { public hass!: HomeAssistant; @internalProperty() private _params?: HaDomainTogglerDialogParams; - public async showDialog(params: HaDomainTogglerDialogParams): Promise { + public showDialog(params: HaDomainTogglerDialogParams): void { this._params = params; } + public closeDialog() { + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + protected render(): TemplateResult { if (!this._params) { return html``; @@ -35,46 +43,47 @@ class DomainTogglerDialog extends LitElement { .sort(); return html` - -

    - ${this.hass.localize("ui.dialogs.domain_toggler.title")} -

    ${domains.map( (domain) => html` -
    ${domain[0]}
    - - ${this.hass.localize("state.default.off")} - - - ${this.hass.localize("state.default.on")} + + + + + + ${this.hass.localize("ui.dialogs.domain_toggler.reset_entities")} ` )}
    -
    + `; } - private _openedChanged(ev: PolymerChangedEvent): void { - // Closed dialog by clicking on the overlay - if (!ev.detail.value) { - this._params = undefined; - } - } - - private _handleOff(ev) { - this._params!.toggleDomain(ev.currentTarget.domain, false); + private _handleSwitch(ev) { + this._params!.toggleDomain(ev.currentTarget.domain, ev.target.checked); ev.currentTarget.blur(); } - private _handleOn(ev) { - this._params!.toggleDomain(ev.currentTarget.domain, true); + private _handleReset(ev) { + this._params!.resetDomain(ev.currentTarget.domain); ev.currentTarget.blur(); } @@ -82,8 +91,8 @@ class DomainTogglerDialog extends LitElement { return [ haStyleDialog, css` - ha-paper-dialog { - max-width: 500px; + ha-dialog { + --mdc-dialog-max-width: 500px; } div { display: grid; diff --git a/src/dialogs/domain-toggler/show-dialog-domain-toggler.ts b/src/dialogs/domain-toggler/show-dialog-domain-toggler.ts index 509075c4d5..e5ee6bad5b 100644 --- a/src/dialogs/domain-toggler/show-dialog-domain-toggler.ts +++ b/src/dialogs/domain-toggler/show-dialog-domain-toggler.ts @@ -2,7 +2,9 @@ import { fireEvent } from "../../common/dom/fire_event"; export interface HaDomainTogglerDialogParams { domains: string[]; + exposedDomains: string[] | null; toggleDomain: (domain: string, turnOn: boolean) => void; + resetDomain: (domain: string) => void; } export const loadDomainTogglerDialog = () => diff --git a/src/dialogs/generic/dialog-box.ts b/src/dialogs/generic/dialog-box.ts index 0663b6bfe7..b8fcea9a70 100644 --- a/src/dialogs/generic/dialog-box.ts +++ b/src/dialogs/generic/dialog-box.ts @@ -55,9 +55,9 @@ class DialogBox extends LitElement { return html`
    `; diff --git a/src/dialogs/more-info/controls/more-info-media_player.ts b/src/dialogs/more-info/controls/more-info-media_player.ts index 0bdcc97fd8..0e7a445c0b 100644 --- a/src/dialogs/more-info/controls/more-info-media_player.ts +++ b/src/dialogs/more-info/controls/more-info-media_player.ts @@ -174,7 +174,7 @@ class MoreInfoMediaPlayer extends LitElement { > ${stateObj.attributes.sound_mode_list.map( (mode) => html` - ${mode} + ${mode} ` )} @@ -352,21 +352,27 @@ class MoreInfoMediaPlayer extends LitElement { } private _handleSourceChanged(e: CustomEvent) { - const newVal = e.detail.value; + const newVal = e.detail.item.itemName; - if (!newVal || this.stateObj!.attributes.source === newVal) return; + if (!newVal || this.stateObj!.attributes.source === newVal) { + return; + } this.hass.callService("media_player", "select_source", { + entity_id: this.stateObj!.entity_id, source: newVal, }); } private _handleSoundModeChanged(e: CustomEvent) { - const newVal = e.detail.value; + const newVal = e.detail.item.itemName; - if (!newVal || this.stateObj?.attributes.sound_mode === newVal) return; + if (!newVal || this.stateObj?.attributes.sound_mode === newVal) { + return; + } this.hass.callService("media_player", "select_sound_mode", { + entity_id: this.stateObj!.entity_id, sound_mode: newVal, }); } diff --git a/src/dialogs/more-info/controls/more-info-vacuum.ts b/src/dialogs/more-info/controls/more-info-vacuum.ts index 0c131333f1..ce947e9929 100644 --- a/src/dialogs/more-info/controls/more-info-vacuum.ts +++ b/src/dialogs/more-info/controls/more-info-vacuum.ts @@ -68,7 +68,7 @@ const VACUUM_COMMANDS: VacuumCommand[] = [ }, { translationKey: "clean_spot", - icon: "hass:broom", + icon: "hass:target-variant", serviceName: "clean_spot", isVisible: (stateObj) => supportsFeature(stateObj, VACUUM_SUPPORT_CLEAN_SPOT), diff --git a/src/dialogs/notifications/persistent-notification-item.ts b/src/dialogs/notifications/persistent-notification-item.ts index a8f2a20777..cc5359f306 100644 --- a/src/dialogs/notifications/persistent-notification-item.ts +++ b/src/dialogs/notifications/persistent-notification-item.ts @@ -43,12 +43,9 @@ export class HuiPersistentNotificationItem extends LitElement { .hass=${this.hass} .datetime="${this.notification.created_at}" > - ${this._computeTooltip( - this.hass, - this.notification - )} + + ${this._computeTooltip(this.hass, this.notification)} +
    diff --git a/src/layouts/hass-tabs-subpage-data-table.ts b/src/layouts/hass-tabs-subpage-data-table.ts index dce430e320..58ea90fb99 100644 --- a/src/layouts/hass-tabs-subpage-data-table.ts +++ b/src/layouts/hass-tabs-subpage-data-table.ts @@ -11,6 +11,7 @@ import { TemplateResult, } from "lit-element"; import { navigate } from "../common/navigate"; +import { computeRTLDirection } from "../common/util/compute_rtl"; import "../components/data-table/ha-data-table"; import type { DataTableColumnContainer, @@ -20,7 +21,6 @@ import type { import type { HomeAssistant, Route } from "../types"; import "./hass-tabs-subpage"; import type { PageNavigation } from "./hass-tabs-subpage"; -import { computeRTLDirection } from "../common/util/compute_rtl"; @customElement("hass-tabs-subpage-data-table") export class HaTabsSubpageDataTable extends LitElement { @@ -136,7 +136,7 @@ export class HaTabsSubpageDataTable extends LitElement { ? html`
    - + ${this.hass.localize( "ui.panel.config.filtering.filtering_by" )} diff --git a/src/panels/config/areas/ha-config-area-page.ts b/src/panels/config/areas/ha-config-area-page.ts index 41bd615b2d..7f3d7cc2d1 100644 --- a/src/panels/config/areas/ha-config-area-page.ts +++ b/src/panels/config/areas/ha-config-area-page.ts @@ -4,9 +4,9 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; import { ifDefined } from "lit-html/directives/if-defined"; @@ -175,8 +175,8 @@ class HaConfigAreaPage extends LitElement { ${!state.attributes.id ? html` - ${this.hass.localize( + + ${this.hass.localize( "ui.panel.config.devices.cant_edit" )} @@ -228,8 +228,8 @@ class HaConfigAreaPage extends LitElement { ${!state.attributes.id ? html` - ${this.hass.localize( + + ${this.hass.localize( "ui.panel.config.devices.cant_edit" )} diff --git a/src/panels/config/areas/ha-config-areas-dashboard.ts b/src/panels/config/areas/ha-config-areas-dashboard.ts index d5c776baa7..3b2262ee75 100644 --- a/src/panels/config/areas/ha-config-areas-dashboard.ts +++ b/src/panels/config/areas/ha-config-areas-dashboard.ts @@ -1,3 +1,5 @@ +import "@material/mwc-fab"; +import { mdiPlus } from "@mdi/js"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item-body"; import { @@ -16,7 +18,8 @@ import { DataTableColumnContainer, RowClickedEvent, } from "../../../components/data-table/ha-data-table"; -import "@material/mwc-fab"; +import "../../../components/ha-icon-button"; +import "../../../components/ha-svg-icon"; import { AreaRegistryEntry, createAreaRegistryEntry, @@ -26,8 +29,6 @@ import { devicesInArea, } from "../../../data/device_registry"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; -import "../../../components/ha-icon-button"; -import "../../../components/ha-svg-icon"; import "../../../layouts/hass-loading-screen"; import "../../../layouts/hass-tabs-subpage-data-table"; import { HomeAssistant, Route } from "../../../types"; @@ -37,7 +38,6 @@ import { loadAreaRegistryDetailDialog, showAreaRegistryDetailDialog, } from "./show-dialog-area-registry-detail"; -import { mdiPlus } from "@mdi/js"; @customElement("ha-config-areas-dashboard") export class HaConfigAreasDashboard extends LitElement { diff --git a/src/panels/config/automation/action/types/ha-automation-action-condition.ts b/src/panels/config/automation/action/types/ha-automation-action-condition.ts index 8849117902..03eb234832 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-condition.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-condition.ts @@ -15,7 +15,7 @@ export class HaConditionAction extends LitElement implements ActionElement { return { condition: "state" }; } - public render() { + protected render() { return html` ${this.hass.localize( diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index 57286eccd4..e56b62fe2b 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -1,12 +1,13 @@ -import "../../../components/ha-icon-button"; +import "@material/mwc-fab"; +import { mdiPlus } from "@mdi/js"; import "@polymer/paper-tooltip/paper-tooltip"; import { + CSSResult, customElement, html, LitElement, property, TemplateResult, - CSSResult, } from "lit-element"; import { ifDefined } from "lit-html/directives/if-defined"; import memoizeOne from "memoize-one"; @@ -16,7 +17,8 @@ import { fireEvent } from "../../../common/dom/fire_event"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table"; import "../../../components/entity/ha-entity-toggle"; -import "@material/mwc-fab"; +import "../../../components/ha-icon-button"; +import "../../../components/ha-svg-icon"; import { AutomationConfig, AutomationEntity, @@ -28,8 +30,6 @@ import { haStyle } from "../../../resources/styles"; import { HomeAssistant, Route } from "../../../types"; import { configSections } from "../ha-panel-config"; import { showThingtalkDialog } from "./show-dialog-thingtalk"; -import "../../../components/ha-svg-icon"; -import { mdiPlus } from "@mdi/js"; @customElement("ha-automation-picker") class HaAutomationPicker extends LitElement { @@ -138,7 +138,7 @@ class HaAutomationPicker extends LitElement { ${!automation.attributes.id ? html` - + ${this.hass.localize( "ui.panel.config.automation.picker.only_editable" )} diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-event.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-event.ts index fd1555c598..cb7bd866ca 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-event.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-event.ts @@ -19,7 +19,7 @@ export class HaEventTrigger extends LitElement implements TriggerElement { return { event_type: "", event_data: {} }; } - public render() { + protected render() { const { event_type, event_data } = this.trigger; return html` diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state.ts index 6d10f83c2d..068dbd6856 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state.ts @@ -1,8 +1,8 @@ import "@polymer/paper-input/paper-input"; +import "@polymer/paper-input/paper-textarea"; import { customElement, html, LitElement, property } from "lit-element"; import { fireEvent } from "../../../../../common/dom/fire_event"; import "../../../../../components/entity/ha-entity-picker"; -import "@polymer/paper-input/paper-textarea"; import { ForDict, NumericStateTrigger } from "../../../../../data/automation"; import { HomeAssistant } from "../../../../../types"; import { handleChangeEvent } from "../ha-automation-trigger-row"; @@ -19,7 +19,7 @@ export default class HaNumericStateTrigger extends LitElement { }; } - public render() { + protected render() { const { value_template, entity_id, below, above } = this.trigger; let trgFor = this.trigger.for; diff --git a/src/panels/config/cloud/alexa/cloud-alexa.ts b/src/panels/config/cloud/alexa/cloud-alexa.ts index 3dd625cc6c..5614318886 100644 --- a/src/panels/config/cloud/alexa/cloud-alexa.ts +++ b/src/panels/config/cloud/alexa/cloud-alexa.ts @@ -1,14 +1,22 @@ -import "../../../../components/ha-icon-button"; +import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; +import "@material/mwc-list/mwc-list-item"; +import { + mdiCheckboxMarked, + mdiCheckboxMultipleMarked, + mdiCloseBox, + mdiCloseBoxMultiple, +} from "@mdi/js"; import { css, CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; +import { classMap } from "lit-html/directives/class-map"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../common/dom/fire_event"; import { computeDomain } from "../../../../common/entity/compute_domain"; @@ -20,31 +28,28 @@ import { } from "../../../../common/entity/entity_filter"; import { compare } from "../../../../common/string/compare"; import "../../../../components/entity/state-info"; +import "../../../../components/ha-button-menu"; import "../../../../components/ha-card"; +import "../../../../components/ha-formfield"; +import "../../../../components/ha-icon-button"; import "../../../../components/ha-switch"; -import type { HaSwitch } from "../../../../components/ha-switch"; import { AlexaEntity, fetchCloudAlexaEntities } from "../../../../data/alexa"; import { AlexaEntityConfig, CloudPreferences, CloudStatusLoggedIn, updateCloudAlexaEntityConfig, + updateCloudPref, } from "../../../../data/cloud"; import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show-dialog-domain-toggler"; import "../../../../layouts/hass-loading-screen"; import "../../../../layouts/hass-subpage"; +import { haStyle } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; -import "../../../../components/ha-formfield"; -import { computeRTLDirection } from "../../../../common/util/compute_rtl"; const DEFAULT_CONFIG_EXPOSE = true; const IGNORE_INTERFACES = ["Alexa.EndpointHealth"]; -const configIsExposed = (config: AlexaEntityConfig) => - config.should_expose === undefined - ? DEFAULT_CONFIG_EXPOSE - : config.should_expose; - @customElement("cloud-alexa") class CloudAlexa extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -100,7 +105,10 @@ class CloudAlexa extends LitElement { const stateObj = this.hass.states[entity.entity_id]; const config = this._entityConfigs[entity.entity_id] || {}; const isExposed = emptyFilter - ? configIsExposed(config) + ? this._configIsExposed(entity.entity_id, config) + : filterFunc(entity.entity_id); + const isDomainExposed = emptyFilter + ? this._configIsDomainExposed(entity.entity_id) : filterFunc(entity.entity_id); if (isExposed) { selected++; @@ -117,33 +125,80 @@ class CloudAlexa extends LitElement { target.push(html`
    - - ${entity.interfaces - .filter((ifc) => !IGNORE_INTERFACES.includes(ifc)) - .map((ifc) => - ifc.replace("Alexa.", "").replace("Controller", "") - ) - .join(", ")} - - - + - - + ${entity.interfaces + .filter((ifc) => !IGNORE_INTERFACES.includes(ifc)) + .map((ifc) => ifc.replace(/(Alexa.|Controller)/g, "")) + .join(", ")} + + + + + + + ${this.hass!.localize( + "ui.panel.config.cloud.alexa.expose_entity" + )} + + + + ${this.hass!.localize( + "ui.panel.config.cloud.alexa.dont_expose_entity" + )} + + + + ${this.hass!.localize( + "ui.panel.config.cloud.alexa.follow_domain" + )} + + + +
    `); @@ -157,17 +212,16 @@ class CloudAlexa extends LitElement { - - ${selected}${!this.narrow ? html` selected ` : ""} - ${ emptyFilter ? html` - + >${this.hass!.localize( + "ui.panel.config.cloud.alexa.manage_domains" + )} ` : "" } @@ -183,11 +237,20 @@ class CloudAlexa extends LitElement { ${ exposedCards.length > 0 ? html` -

    - ${this.hass!.localize( - "ui.panel.config.cloud.alexa.exposed_entities" - )} -

    +
    +

    + ${this.hass!.localize( + "ui.panel.config.cloud.alexa.exposed_entities" + )} +

    + ${!this.narrow + ? this.hass!.localize( + "ui.panel.config.cloud.alexa.exposed", + "selected", + selected + ) + : selected} +
    ${exposedCards}
    ` : "" @@ -195,11 +258,20 @@ class CloudAlexa extends LitElement { ${ notExposedCards.length > 0 ? html` -

    - ${this.hass!.localize( - "ui.panel.config.cloud.alexa.not_exposed_entities" - )} -

    +
    +

    + ${this.hass!.localize( + "ui.panel.config.cloud.alexa.not_exposed_entities" + )} +

    + ${!this.narrow + ? this.hass!.localize( + "ui.panel.config.cloud.alexa.not_exposed", + "selected", + this._entities.length - selected + ) + : this._entities.length - selected} +
    ${notExposedCards}
    ` : "" @@ -239,17 +311,37 @@ class CloudAlexa extends LitElement { fireEvent(this, "hass-more-info", { entityId }); } - private async _exposeChanged(ev: Event) { - const entityId = (ev.currentTarget as any).entityId; - const newExposed = (ev.target as HaSwitch).checked; - await this._updateExposed(entityId, newExposed); + private _configIsDomainExposed(entityId: string) { + const domain = computeDomain(entityId); + return this.cloudStatus.prefs.alexa_default_expose + ? this.cloudStatus.prefs.alexa_default_expose.includes(domain) + : DEFAULT_CONFIG_EXPOSE; } - private async _updateExposed(entityId: string, newExposed: boolean) { - const curExposed = configIsExposed(this._entityConfigs[entityId] || {}); - if (newExposed === curExposed) { - return; + private _configIsExposed(entityId: string, config: AlexaEntityConfig) { + return config.should_expose === null + ? this._configIsDomainExposed(entityId) + : config.should_expose; + } + + private async _exposeChanged(ev: CustomEvent) { + const entityId = (ev.currentTarget as any).entityId; + let newVal: boolean | null = null; + switch (ev.detail.index) { + case 0: + newVal = true; + break; + case 1: + newVal = false; + break; + case 2: + newVal = null; + break; } + await this._updateExposed(entityId, newVal); + } + + private async _updateExposed(entityId: string, newExposed: boolean | null) { await this._updateConfig(entityId, { should_expose: newExposed, }); @@ -274,16 +366,46 @@ class CloudAlexa extends LitElement { domains: this._entities!.map((entity) => computeDomain(entity.entity_id) ).filter((value, idx, self) => self.indexOf(value) === idx), - toggleDomain: (domain, turnOn) => { + exposedDomains: this.cloudStatus.prefs.alexa_default_expose, + toggleDomain: (domain, expose) => { + this._updateDomainExposed(domain, expose); + }, + resetDomain: (domain) => { this._entities!.forEach((entity) => { if (computeDomain(entity.entity_id) === domain) { - this._updateExposed(entity.entity_id, turnOn); + this._updateExposed(entity.entity_id, null); } }); }, }); } + private async _updateDomainExposed(domain: string, expose: boolean) { + const defaultExpose = + this.cloudStatus.prefs.alexa_default_expose || + this._entities!.map((entity) => computeDomain(entity.entity_id)).filter( + (value, idx, self) => self.indexOf(value) === idx + ); + + if ( + (expose && defaultExpose.includes(domain)) || + (!expose && !defaultExpose.includes(domain)) + ) { + return; + } + + if (expose) { + defaultExpose.push(domain); + } else { + defaultExpose.splice(defaultExpose.indexOf(domain), 1); + } + + await updateCloudPref(this.hass!, { + alexa_default_expose: defaultExpose, + }); + fireEvent(this, "ha-refresh-cloud-status"); + } + private _ensureStatusReload() { if (this._popstateReloadStatusAttached) { return; @@ -306,61 +428,75 @@ class CloudAlexa extends LitElement { this._popstateSyncAttached = true; // Cache parent because by the time popstate happens, // this element is detached - // const parent = this.parentElement!; window.addEventListener( "popstate", () => { // We don't have anything yet. - // showToast(parent, { message: "Synchronizing changes to Google." }); - // cloudSyncGoogleAssistant(this.hass); }, { once: true } ); } - static get styles(): CSSResult { - return css` - .banner { - color: var(--primary-text-color); - background-color: var( - --ha-card-background, - var(--card-background-color, white) - ); - padding: 16px 8px; - text-align: center; - } - h1 { - color: var(--primary-text-color); - font-size: 24px; - letter-spacing: -0.012em; - margin-bottom: 0; - padding: 0 8px; - } - .content { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - grid-gap: 8px 8px; - padding: 8px; - } - ha-switch { - clear: both; - } - .card-content { - padding-bottom: 12px; - } - state-info { - cursor: pointer; - } - ha-switch { - padding: 8px 0; - } - - @media all and (max-width: 450px) { - ha-card { - max-width: 100%; + static get styles(): CSSResult[] { + return [ + haStyle, + css` + mwc-list-item > [slot="meta"] { + margin-left: 4px; } - } - `; + .banner { + color: var(--primary-text-color); + background-color: var( + --ha-card-background, + var(--card-background-color, white) + ); + padding: 16px 8px; + text-align: center; + } + .content { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + grid-gap: 8px 8px; + padding: 8px; + } + .card-content { + padding-bottom: 12px; + } + state-info { + cursor: pointer; + } + ha-switch { + padding: 8px 0; + } + .top-line { + display: flex; + align-items: center; + justify-content: space-between; + } + .header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 16px; + border-bottom: 1px solid var(--divider-color); + background: var(--app-header-background-color); + } + .header.second { + border-top: 1px solid var(--divider-color); + } + .exposed { + color: var(--success-color); + } + .not-exposed { + color: var(--error-color); + } + @media all and (max-width: 450px) { + ha-card { + max-width: 100%; + } + } + `, + ]; } } diff --git a/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts b/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts index bdf23ec5f5..c29e22eaa0 100644 --- a/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts +++ b/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts @@ -1,14 +1,22 @@ -import "../../../../components/ha-icon-button"; +import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; +import "@material/mwc-list/mwc-list-item"; +import { + mdiCheckboxMarked, + mdiCheckboxMultipleMarked, + mdiCloseBox, + mdiCloseBoxMultiple, +} from "@mdi/js"; import { css, CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; +import { classMap } from "lit-html/directives/class-map"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../common/dom/fire_event"; import { computeDomain } from "../../../../common/entity/compute_domain"; @@ -19,8 +27,12 @@ import { isEmptyFilter, } from "../../../../common/entity/entity_filter"; import { compare } from "../../../../common/string/compare"; +import { computeRTLDirection } from "../../../../common/util/compute_rtl"; import "../../../../components/entity/state-info"; +import "../../../../components/ha-button-menu"; import "../../../../components/ha-card"; +import "../../../../components/ha-formfield"; +import "../../../../components/ha-icon-button"; import "../../../../components/ha-switch"; import type { HaSwitch } from "../../../../components/ha-switch"; import { @@ -29,6 +41,7 @@ import { cloudSyncGoogleAssistant, GoogleEntityConfig, updateCloudGoogleEntityConfig, + updateCloudPref, } from "../../../../data/cloud"; import { fetchCloudGoogleEntities, @@ -37,18 +50,12 @@ import { import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show-dialog-domain-toggler"; import "../../../../layouts/hass-loading-screen"; import "../../../../layouts/hass-subpage"; +import { haStyle } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; import { showToast } from "../../../../util/toast"; -import "../../../../components/ha-formfield"; -import { computeRTLDirection } from "../../../../common/util/compute_rtl"; const DEFAULT_CONFIG_EXPOSE = true; -const configIsExposed = (config: GoogleEntityConfig) => - config.should_expose === undefined - ? DEFAULT_CONFIG_EXPOSE - : config.should_expose; - @customElement("cloud-google-assistant") class CloudGoogleAssistant extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -104,7 +111,10 @@ class CloudGoogleAssistant extends LitElement { const stateObj = this.hass.states[entity.entity_id]; const config = this._entityConfigs[entity.entity_id] || {}; const isExposed = emptyFilter - ? configIsExposed(config) + ? this._configIsExposed(entity.entity_id, config) + : filterFunc(entity.entity_id); + const isDomainExposed = emptyFilter + ? this._configIsDomainExposed(entity.entity_id) : filterFunc(entity.entity_id); if (isExposed) { selected++; @@ -121,31 +131,78 @@ class CloudGoogleAssistant extends LitElement { target.push(html`
    - - ${entity.traits - .map((trait) => trait.substr(trait.lastIndexOf(".") + 1)) - .join(", ")} - -
    - + - trait.substr(trait.lastIndexOf(".") + 1)) + .join(", ")} + + + - - + + + + ${this.hass!.localize( + "ui.panel.config.cloud.google.expose_entity" + )} + + + + ${this.hass!.localize( + "ui.panel.config.cloud.google.dont_expose_entity" + )} + + + + ${this.hass!.localize( + "ui.panel.config.cloud.google.follow_domain" + )} + + +
    ${entity.might_2fa ? html` @@ -178,17 +235,16 @@ class CloudGoogleAssistant extends LitElement { - - ${selected}${!this.narrow ? html` selected ` : ""} - ${ emptyFilter ? html` - + >${this.hass!.localize( + "ui.panel.config.cloud.google.manage_domains" + )} ` : "" } @@ -204,11 +260,20 @@ class CloudGoogleAssistant extends LitElement { ${ exposedCards.length > 0 ? html` -

    - ${this.hass!.localize( - "ui.panel.config.cloud.google.exposed_entities" - )} -

    +
    +

    + ${this.hass!.localize( + "ui.panel.config.cloud.google.exposed_entities" + )} +

    + ${!this.narrow + ? this.hass!.localize( + "ui.panel.config.cloud.alexa.exposed", + "selected", + selected + ) + : selected} +
    ${exposedCards}
    ` : "" @@ -216,11 +281,20 @@ class CloudGoogleAssistant extends LitElement { ${ notExposedCards.length > 0 ? html` -

    - ${this.hass!.localize( - "ui.panel.config.cloud.google.not_exposed_entities" - )} -

    +
    +

    + ${this.hass!.localize( + "ui.panel.config.cloud.google.not_exposed_entities" + )} +

    + ${!this.narrow + ? this.hass!.localize( + "ui.panel.config.cloud.alexa.not_exposed", + "selected", + this._entities.length - selected + ) + : this._entities.length - selected} +
    ${notExposedCards}
    ` : "" @@ -242,6 +316,19 @@ class CloudGoogleAssistant extends LitElement { } } + private _configIsDomainExposed(entityId: string) { + const domain = computeDomain(entityId); + return this.cloudStatus.prefs.google_default_expose + ? this.cloudStatus.prefs.google_default_expose.includes(domain) + : DEFAULT_CONFIG_EXPOSE; + } + + private _configIsExposed(entityId: string, config: GoogleEntityConfig) { + return config.should_expose === null + ? this._configIsDomainExposed(entityId) + : config.should_expose; + } + private async _fetchData() { const entities = await fetchCloudGoogleEntities(this.hass); entities.sort((a, b) => { @@ -260,17 +347,24 @@ class CloudGoogleAssistant extends LitElement { fireEvent(this, "hass-more-info", { entityId }); } - private async _exposeChanged(ev: Event) { + private async _exposeChanged(ev: CustomEvent) { const entityId = (ev.currentTarget as any).entityId; - const newExposed = (ev.target as HaSwitch).checked; - await this._updateExposed(entityId, newExposed); + let newVal: boolean | null = null; + switch (ev.detail.index) { + case 0: + newVal = true; + break; + case 1: + newVal = false; + break; + case 2: + newVal = null; + break; + } + await this._updateExposed(entityId, newVal); } - private async _updateExposed(entityId: string, newExposed: boolean) { - const curExposed = configIsExposed(this._entityConfigs[entityId] || {}); - if (newExposed === curExposed) { - return; - } + private async _updateExposed(entityId: string, newExposed: boolean | null) { await this._updateConfig(entityId, { should_expose: newExposed, }); @@ -309,16 +403,46 @@ class CloudGoogleAssistant extends LitElement { domains: this._entities!.map((entity) => computeDomain(entity.entity_id) ).filter((value, idx, self) => self.indexOf(value) === idx), - toggleDomain: (domain, turnOn) => { + exposedDomains: this.cloudStatus.prefs.google_default_expose, + toggleDomain: (domain, expose) => { + this._updateDomainExposed(domain, expose); + }, + resetDomain: (domain) => { this._entities!.forEach((entity) => { if (computeDomain(entity.entity_id) === domain) { - this._updateExposed(entity.entity_id, turnOn); + this._updateExposed(entity.entity_id, null); } }); }, }); } + private async _updateDomainExposed(domain: string, expose: boolean) { + const defaultExpose = + this.cloudStatus.prefs.google_default_expose || + this._entities!.map((entity) => computeDomain(entity.entity_id)).filter( + (value, idx, self) => self.indexOf(value) === idx + ); + + if ( + (expose && defaultExpose.includes(domain)) || + (!expose && !defaultExpose.includes(domain)) + ) { + return; + } + + if (expose) { + defaultExpose.push(domain); + } else { + defaultExpose.splice(defaultExpose.indexOf(domain), 1); + } + + await updateCloudPref(this.hass!, { + google_default_expose: defaultExpose, + }); + fireEvent(this, "ha-refresh-cloud-status"); + } + private _ensureStatusReload() { if (this._popstateReloadStatusAttached) { return; @@ -356,46 +480,66 @@ class CloudGoogleAssistant extends LitElement { ); } - static get styles(): CSSResult { - return css` - .banner { - color: var(--primary-text-color); - background-color: var( - --ha-card-background, - var(--card-background-color, white) - ); - padding: 16px 8px; - text-align: center; - } - h1 { - color: var(--primary-text-color); - font-size: 24px; - letter-spacing: -0.012em; - margin-bottom: 0; - padding: 0 8px; - } - .content { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - grid-gap: 8px 8px; - padding: 8px; - } - .card-content { - padding-bottom: 12px; - } - state-info { - cursor: pointer; - } - ha-switch { - padding: 8px 0; - } - - @media all and (max-width: 450px) { - ha-card { - max-width: 100%; + static get styles(): CSSResult[] { + return [ + haStyle, + css` + mwc-list-item > [slot="meta"] { + margin-left: 4px; } - } - `; + .banner { + color: var(--primary-text-color); + background-color: var( + --ha-card-background, + var(--card-background-color, white) + ); + padding: 16px 8px; + text-align: center; + } + .content { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + grid-gap: 8px 8px; + padding: 8px; + } + .card-content { + padding-bottom: 12px; + } + state-info { + cursor: pointer; + } + ha-switch { + padding: 8px 0; + } + .top-line { + display: flex; + align-items: center; + justify-content: space-between; + } + .header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 16px; + border-bottom: 1px solid var(--divider-color); + background: var(--app-header-background-color); + } + .header.second { + border-top: 1px solid var(--divider-color); + } + .exposed { + color: var(--success-color); + } + .not-exposed { + color: var(--error-color); + } + @media all and (max-width: 450px) { + ha-card { + max-width: 100%; + } + } + `, + ]; } } diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index e3577eb5dd..0287b084cd 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -4,9 +4,9 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; import { ifDefined } from "lit-html/directives/if-defined"; @@ -14,6 +14,7 @@ import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { compare } from "../../../common/string/compare"; +import { slugify } from "../../../common/string/slugify"; import "../../../components/entity/ha-battery-icon"; import "../../../components/ha-icon-next"; import { AreaRegistryEntry } from "../../../data/area_registry"; @@ -25,8 +26,8 @@ import { } from "../../../data/device_registry"; import { EntityRegistryEntry, - findBatteryEntity, findBatteryChargingEntity, + findBatteryEntity, updateEntityRegistryEntry, } from "../../../data/entity_registry"; import { SceneEntities, showSceneEditor } from "../../../data/scene"; @@ -35,6 +36,7 @@ import { loadDeviceRegistryDetailDialog, showDeviceRegistryDetailDialog, } from "../../../dialogs/device-registry-detail/show-dialog-device-registry-detail"; +import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/hass-error-screen"; import "../../../layouts/hass-tabs-subpage"; import { HomeAssistant, Route } from "../../../types"; @@ -43,8 +45,6 @@ import { configSections } from "../ha-panel-config"; import "./device-detail/ha-device-entities-card"; import "./device-detail/ha-device-info-card"; import { showDeviceAutomationDialog } from "./device-detail/show-dialog-device-automation"; -import { slugify } from "../../../common/string/slugify"; -import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; export interface EntityRegistryStateEntry extends EntityRegistryEntry { stateName?: string | null; @@ -296,8 +296,8 @@ export class HaConfigDevicePage extends LitElement { ${!state.attributes.id ? html` - ${this.hass.localize( + + ${this.hass.localize( "ui.panel.config.devices.cant_edit" )} @@ -369,7 +369,9 @@ export class HaConfigDevicePage extends LitElement { ${!state.attributes.id ? html` ${this.hass.localize( + animation-delay="0" + > + ${this.hass.localize( "ui.panel.config.devices.cant_edit" )} diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts index 884c7c16dc..274eecf5e0 100644 --- a/src/panels/config/devices/ha-config-devices-dashboard.ts +++ b/src/panels/config/devices/ha-config-devices-dashboard.ts @@ -1,9 +1,9 @@ import { customElement, html, + internalProperty, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; import memoizeOne from "memoize-one"; @@ -25,8 +25,8 @@ import { } from "../../../data/device_registry"; import { EntityRegistryEntry, - findBatteryEntity, findBatteryChargingEntity, + findBatteryEntity, } from "../../../data/entity_registry"; import { domainToName } from "../../../data/integration"; import "../../../layouts/hass-tabs-subpage-data-table"; @@ -181,8 +181,8 @@ export class HaConfigDeviceDashboard extends LitElement { ); private _columns = memoizeOne( - (narrow: boolean): DataTableColumnContainer => - narrow + (narrow: boolean): DataTableColumnContainer => { + const columns: DataTableColumnContainer = narrow ? { name: { title: "Device", @@ -199,36 +199,6 @@ export class HaConfigDeviceDashboard extends LitElement { `; }, }, - battery_entity: { - title: this.hass.localize( - "ui.panel.config.devices.data_table.battery" - ), - sortable: true, - type: "numeric", - width: "90px", - template: ( - batteryEntityPair: DeviceRowData["battery_entity"] - ) => { - const battery = - batteryEntityPair && batteryEntityPair[0] - ? this.hass.states[batteryEntityPair[0]] - : undefined; - const batteryCharging = - batteryEntityPair && batteryEntityPair[1] - ? this.hass.states[batteryEntityPair[1]] - : undefined; - return battery - ? html` - ${isNaN(battery.state as any) ? "-" : battery.state}% - - ` - : html` - `; - }, - }, } : { name: { @@ -240,70 +210,69 @@ export class HaConfigDeviceDashboard extends LitElement { grows: true, direction: "asc", }, - manufacturer: { - title: this.hass.localize( - "ui.panel.config.devices.data_table.manufacturer" - ), - sortable: true, - filterable: true, - width: "15%", - }, - model: { - title: this.hass.localize( - "ui.panel.config.devices.data_table.model" - ), - sortable: true, - filterable: true, - width: "15%", - }, - area: { - title: this.hass.localize( - "ui.panel.config.devices.data_table.area" - ), - sortable: true, - filterable: true, - width: "15%", - }, - integration: { - title: this.hass.localize( - "ui.panel.config.devices.data_table.integration" - ), - sortable: true, - filterable: true, - width: "15%", - }, - battery_entity: { - title: this.hass.localize( - "ui.panel.config.devices.data_table.battery" - ), - sortable: true, - type: "numeric", - width: "15%", - maxWidth: "90px", - template: ( - batteryEntityPair: DeviceRowData["battery_entity"] - ) => { - const battery = - batteryEntityPair && batteryEntityPair[0] - ? this.hass.states[batteryEntityPair[0]] - : undefined; - const batteryCharging = - batteryEntityPair && batteryEntityPair[1] - ? this.hass.states[batteryEntityPair[1]] - : undefined; - return battery && !isNaN(battery.state as any) - ? html` - ${battery.state}% - - ` - : html` - `; - }, - }, - } + }; + + columns.manufacturer = { + title: this.hass.localize( + "ui.panel.config.devices.data_table.manufacturer" + ), + sortable: true, + hidden: narrow, + filterable: true, + width: "15%", + }; + columns.model = { + title: this.hass.localize("ui.panel.config.devices.data_table.model"), + sortable: true, + hidden: narrow, + filterable: true, + width: "15%", + }; + columns.area = { + title: this.hass.localize("ui.panel.config.devices.data_table.area"), + sortable: true, + hidden: narrow, + filterable: true, + width: "15%", + }; + columns.integration = { + title: this.hass.localize( + "ui.panel.config.devices.data_table.integration" + ), + sortable: true, + hidden: narrow, + filterable: true, + width: "15%", + }; + columns.battery_entity = { + title: this.hass.localize("ui.panel.config.devices.data_table.battery"), + sortable: true, + type: "numeric", + width: narrow ? "90px" : "15%", + maxWidth: "90px", + template: (batteryEntityPair: DeviceRowData["battery_entity"]) => { + const battery = + batteryEntityPair && batteryEntityPair[0] + ? this.hass.states[batteryEntityPair[0]] + : undefined; + const batteryCharging = + batteryEntityPair && batteryEntityPair[1] + ? this.hass.states[batteryEntityPair[1]] + : undefined; + return battery && !isNaN(battery.state as any) + ? html` + ${battery.state}% + + ` + : html` - `; + }, + }; + return columns; + } ); public constructor() { diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index 9d3ad306d6..ad7c3d9e3d 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -1,4 +1,6 @@ import "@material/mwc-list/mwc-list-item"; +import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item"; +import { mdiFilterVariant } from "@mdi/js"; import "@polymer/paper-checkbox/paper-checkbox"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@polymer/paper-item/paper-icon-item"; @@ -10,9 +12,9 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, query, TemplateResult, } from "lit-element"; @@ -56,8 +58,6 @@ import { loadEntityEditorDialog, showEntityEditorDialog, } from "./show-dialog-entity-editor"; -import { mdiFilterVariant } from "@mdi/js"; -import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item"; export interface StateEntity extends EntityRegistryEntry { readonly?: boolean; @@ -192,7 +192,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { ? "hass:cancel" : "hass:pencil-off"} > - + ${entity.restored ? this.hass.localize( "ui.panel.config.entities.picker.status.restored" @@ -390,7 +390,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { icon="hass:undo" @click=${this._enableSelected} > - + ${this.hass.localize( "ui.panel.config.entities.picker.enable_selected.button" )} @@ -400,7 +400,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { icon="hass:cancel" @click=${this._disableSelected} > - + ${this.hass.localize( "ui.panel.config.entities.picker.disable_selected.button" )} @@ -410,7 +410,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { icon="hass:delete" @click=${this._removeSelected} > - + ${this.hass.localize( "ui.panel.config.entities.picker.remove_selected.button" )} @@ -433,7 +433,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { ${this.narrow ? html`
    - + ${this.hass.localize( "ui.panel.config.filtering.filtering_by" )} diff --git a/src/panels/config/helpers/dialog-helper-detail.ts b/src/panels/config/helpers/dialog-helper-detail.ts index b752a5037d..a7320a5e0e 100644 --- a/src/panels/config/helpers/dialog-helper-detail.ts +++ b/src/panels/config/helpers/dialog-helper-detail.ts @@ -6,9 +6,9 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, query, TemplateResult, } from "lit-element"; @@ -136,7 +136,7 @@ export class DialogHelperDetail extends LitElement { ${!isLoaded ? html` - ${this.hass.localize( "ui.dialogs.helper_settings.platform_not_loaded", "platform", diff --git a/src/panels/config/helpers/ha-config-helpers.ts b/src/panels/config/helpers/ha-config-helpers.ts index e03b5463ad..fe96b3389e 100644 --- a/src/panels/config/helpers/ha-config-helpers.ts +++ b/src/panels/config/helpers/ha-config-helpers.ts @@ -1,3 +1,5 @@ +import "@material/mwc-fab"; +import { mdiPlus } from "@mdi/js"; import "@polymer/paper-checkbox/paper-checkbox"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@polymer/paper-item/paper-icon-item"; @@ -7,9 +9,9 @@ import { HassEntity } from "home-assistant-js-websocket"; import { customElement, html, + internalProperty, LitElement, property, - internalProperty, PropertyValues, TemplateResult, } from "lit-element"; @@ -21,8 +23,8 @@ import { DataTableColumnContainer, RowClickedEvent, } from "../../../components/data-table/ha-data-table"; -import "@material/mwc-fab"; import "../../../components/ha-icon"; +import "../../../components/ha-svg-icon"; import "../../../layouts/hass-loading-screen"; import "../../../layouts/hass-tabs-subpage-data-table"; import { HomeAssistant, Route } from "../../../types"; @@ -30,8 +32,6 @@ import { showEntityEditorDialog } from "../entities/show-dialog-entity-editor"; import { configSections } from "../ha-panel-config"; import { HELPER_DOMAINS } from "./const"; import { showHelperDetailDialog } from "./show-dialog-helper-detail"; -import "../../../components/ha-svg-icon"; -import { mdiPlus } from "@mdi/js"; @customElement("ha-config-helpers") export class HaConfigHelpers extends LitElement { @@ -110,7 +110,7 @@ export class HaConfigHelpers extends LitElement { style="display:inline-block; position: relative;" > - + ${this.hass.localize( "ui.panel.config.entities.picker.status.readonly" )} diff --git a/src/panels/config/integrations/ha-integration-card.ts b/src/panels/config/integrations/ha-integration-card.ts index fc03a16ddd..dddeaf6754 100644 --- a/src/panels/config/integrations/ha-integration-card.ts +++ b/src/panels/config/integrations/ha-integration-card.ts @@ -14,6 +14,7 @@ import { ConfigEntry, updateConfigEntry, deleteConfigEntry, + reloadConfigEntry, } from "../../../data/config_entries"; import { EntityRegistryEntry } from "../../../data/entity_registry"; import { DeviceRegistryEntry } from "../../../data/device_registry"; @@ -28,7 +29,8 @@ import { haStyle } from "../../../resources/styles"; import "../../../components/ha-icon-next"; import { fireEvent } from "../../../common/dom/fire_event"; import { mdiDotsVertical, mdiOpenInNew } from "@mdi/js"; -import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; +import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item"; +import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event"; export interface ConfigEntryUpdatedEvent { entry: ConfigEntry; @@ -228,7 +230,7 @@ export class HaIntegrationCard extends LitElement { ` : ""}
    - + - + ${this.hass.localize( "ui.panel.config.integrations.config_entry.system_options" )} @@ -259,7 +261,17 @@ export class HaIntegrationCard extends LitElement { `} - + ${item.state === "loaded" && item.supports_unload + ? html` + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.reload" + )} + ` + : ""} + ${this.hass.localize( "ui.panel.config.integrations.config_entry.delete" )} @@ -309,17 +321,31 @@ export class HaIntegrationCard extends LitElement { showOptionsFlowDialog(this, ev.target.closest("ha-card").configEntry); } - private _handleAction(ev: CustomEvent) { - const configEntry = ((ev.target as HTMLElement).closest("ha-card") as any) - .configEntry; - switch (ev.detail.index) { - case 0: - this._showSystemOptions(configEntry); - break; - case 1: - this._removeIntegration(configEntry); - break; + private _handleReload(ev: CustomEvent): void { + if (!shouldHandleRequestSelectedEvent(ev)) { + return; } + this._reloadIntegration( + ((ev.target as HTMLElement).closest("ha-card") as any).configEntry + ); + } + + private _handleDelete(ev: CustomEvent): void { + if (!shouldHandleRequestSelectedEvent(ev)) { + return; + } + this._removeIntegration( + ((ev.target as HTMLElement).closest("ha-card") as any).configEntry + ); + } + + private _handleSystemOptions(ev: CustomEvent): void { + if (!shouldHandleRequestSelectedEvent(ev)) { + return; + } + this._showSystemOptions( + ((ev.target as HTMLElement).closest("ha-card") as any).configEntry + ); } private _showSystemOptions(configEntry: ConfigEntry) { @@ -353,6 +379,21 @@ export class HaIntegrationCard extends LitElement { }); } + private async _reloadIntegration(configEntry: ConfigEntry) { + const entryId = configEntry.entry_id; + + reloadConfigEntry(this.hass, entryId).then((result) => { + const locale_key = result.require_restart + ? "reload_restart_confirm" + : "reload_confirm"; + showAlertDialog(this, { + text: this.hass.localize( + `ui.panel.config.integrations.config_entry.${locale_key}` + ), + }); + }); + } + private async _editEntryName(ev) { const configEntry = ev.target.closest("ha-card").configEntry; const newName = await showPromptDialog(this, { diff --git a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts index 2edb4d0b8e..90205aa8a7 100644 --- a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts +++ b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts @@ -1,11 +1,12 @@ import "@material/mwc-fab"; +import { mdiPlus } from "@mdi/js"; import "@polymer/paper-tooltip/paper-tooltip"; import { customElement, html, + internalProperty, LitElement, property, - internalProperty, PropertyValues, TemplateResult, } from "lit-element"; @@ -18,6 +19,7 @@ import { } from "../../../../components/data-table/ha-data-table"; import "../../../../components/ha-icon"; import "../../../../components/ha-icon-button"; +import "../../../../components/ha-svg-icon"; import { createDashboard, deleteDashboard, @@ -33,8 +35,6 @@ import "../../../../layouts/hass-tabs-subpage-data-table"; import { HomeAssistant, Route } from "../../../../types"; import { lovelaceTabs } from "../ha-config-lovelace"; import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail"; -import "../../../../components/ha-svg-icon"; -import { mdiPlus } from "@mdi/js"; @customElement("ha-config-lovelace-dashboards") export class HaConfigLovelaceDashboards extends LitElement { @@ -76,7 +76,7 @@ export class HaConfigLovelaceDashboards extends LitElement { style="padding-left: 10px;" icon="hass:check-circle-outline" > - + ${this.hass.localize( `ui.panel.config.lovelace.dashboards.default_dashboard` )} diff --git a/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts b/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts index 0bfb4d68ca..5d35134956 100644 --- a/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts +++ b/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts @@ -1,15 +1,15 @@ +import "@material/mwc-fab"; +import { mdiPlus } from "@mdi/js"; import "@polymer/paper-checkbox/paper-checkbox"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-listbox/paper-listbox"; -import "@polymer/paper-tooltip/paper-tooltip"; -import "@material/mwc-fab"; import { customElement, html, + internalProperty, LitElement, property, - internalProperty, PropertyValues, TemplateResult, } from "lit-element"; @@ -20,6 +20,7 @@ import { RowClickedEvent, } from "../../../../components/data-table/ha-data-table"; import "../../../../components/ha-icon"; +import "../../../../components/ha-svg-icon"; import { createResource, deleteResource, @@ -37,8 +38,6 @@ import { HomeAssistant, Route } from "../../../../types"; import { loadLovelaceResources } from "../../../lovelace/common/load-resources"; import { lovelaceTabs } from "../ha-config-lovelace"; import { showResourceDetailDialog } from "./show-dialog-lovelace-resource-detail"; -import "../../../../components/ha-svg-icon"; -import { mdiPlus } from "@mdi/js"; @customElement("ha-config-lovelace-resources") export class HaConfigLovelaceRescources extends LitElement { diff --git a/src/panels/config/scene/ha-scene-dashboard.ts b/src/panels/config/scene/ha-scene-dashboard.ts index f2835f405b..e8b46223f7 100644 --- a/src/panels/config/scene/ha-scene-dashboard.ts +++ b/src/panels/config/scene/ha-scene-dashboard.ts @@ -1,4 +1,5 @@ -import "../../../components/ha-icon-button"; +import "@material/mwc-fab"; +import { mdiPlus } from "@mdi/js"; import "@polymer/paper-tooltip/paper-tooltip"; import { css, @@ -13,8 +14,11 @@ import { ifDefined } from "lit-html/directives/if-defined"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../common/dom/fire_event"; import { computeStateName } from "../../../common/entity/compute_state_name"; +import { stateIcon } from "../../../common/entity/state_icon"; import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table"; -import "@material/mwc-fab"; +import "../../../components/ha-icon"; +import "../../../components/ha-icon-button"; +import "../../../components/ha-svg-icon"; import { forwardHaptic } from "../../../data/haptics"; import { activateScene, SceneEntity } from "../../../data/scene"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; @@ -23,10 +27,6 @@ import { haStyle } from "../../../resources/styles"; import { HomeAssistant, Route } from "../../../types"; import { showToast } from "../../../util/toast"; import { configSections } from "../ha-panel-config"; -import "../../../components/ha-icon"; -import "../../../components/ha-svg-icon"; -import { mdiPlus } from "@mdi/js"; -import { stateIcon } from "../../../common/entity/state_icon"; @customElement("ha-scene-dashboard") class HaSceneDashboard extends LitElement { @@ -117,7 +117,7 @@ class HaSceneDashboard extends LitElement { ${!scene.attributes.id ? html` - + ${this.hass.localize( "ui.panel.config.scene.picker.only_editable" )} diff --git a/src/panels/config/server_control/ha-config-server-control.ts b/src/panels/config/server_control/ha-config-server-control.ts index fe19018906..def3ad5492 100644 --- a/src/panels/config/server_control/ha-config-server-control.ts +++ b/src/panels/config/server_control/ha-config-server-control.ts @@ -36,6 +36,19 @@ const reloadableDomains = [ "input_datetime", "input_select", "template", + "universal", + "rest", + "command_line", + "filter", + "statistics", + "generic", + "generic_thermostat", + "homekit", + "min_max", + "history_stats", + "trend", + "ping", + "filesize", ]; @customElement("ha-config-server-control") diff --git a/src/panels/config/users/dialog-add-user.ts b/src/panels/config/users/dialog-add-user.ts index f9f83566fb..6d6fd0ce76 100644 --- a/src/panels/config/users/dialog-add-user.ts +++ b/src/panels/config/users/dialog-add-user.ts @@ -1,20 +1,21 @@ import "@material/mwc-button"; import "@polymer/paper-input/paper-input"; -import "../../../components/ha-circular-progress"; import { css, CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, PropertyValues, TemplateResult, } from "lit-element"; +import { computeRTLDirection } from "../../../common/util/compute_rtl"; +import "../../../components/ha-circular-progress"; import "../../../components/ha-dialog"; -import "../../../components/ha-switch"; import "../../../components/ha-formfield"; +import "../../../components/ha-switch"; import { createAuthForUser } from "../../../data/auth"; import { createUser, @@ -27,7 +28,6 @@ import { PolymerChangedEvent } from "../../../polymer-types"; import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import { AddUserDialogParams } from "./show-dialog-add-user"; -import { computeRTLDirection } from "../../../common/util/compute_rtl"; @customElement("dialog-add-user") export class DialogAddUser extends LitElement { @@ -46,6 +46,8 @@ export class DialogAddUser extends LitElement { @internalProperty() private _password?: string; + @internalProperty() private _passwordConfirm?: string; + @internalProperty() private _isAdmin?: boolean; public showDialog(params: AddUserDialogParams) { @@ -53,6 +55,7 @@ export class DialogAddUser extends LitElement { this._name = ""; this._username = ""; this._password = ""; + this._passwordConfirm = ""; this._isAdmin = false; this._error = undefined; this._loading = false; @@ -83,17 +86,20 @@ export class DialogAddUser extends LitElement { ${this._error ? html`
    ${this._error}
    ` : ""} + + + + + ${this.hass.localize("ui.panel.config.users.add_user.create")} @@ -173,19 +202,10 @@ export class DialogAddUser extends LitElement { } } - private _nameChanged(ev: PolymerChangedEvent) { + private _handleValueChanged(ev: PolymerChangedEvent): void { this._error = undefined; - this._name = ev.detail.value; - } - - private _usernameChanged(ev: PolymerChangedEvent) { - this._error = undefined; - this._username = ev.detail.value; - } - - private _passwordChanged(ev: PolymerChangedEvent) { - this._error = undefined; - this._password = ev.detail.value; + const name = (ev.target as any).name; + this[`_${name}`] = ev.detail.value; } private async _adminChanged(ev): Promise { diff --git a/src/panels/config/users/dialog-user-detail.ts b/src/panels/config/users/dialog-user-detail.ts index 504ef0a4b5..d662248f02 100644 --- a/src/panels/config/users/dialog-user-detail.ts +++ b/src/panels/config/users/dialog-user-detail.ts @@ -6,23 +6,28 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; +import { computeRTLDirection } from "../../../common/util/compute_rtl"; import { createCloseHeading } from "../../../components/ha-dialog"; -import "../../../components/ha-switch"; import "../../../components/ha-formfield"; +import "../../../components/ha-switch"; +import { adminChangePassword } from "../../../data/auth"; import { SYSTEM_GROUP_ID_ADMIN, SYSTEM_GROUP_ID_USER, } from "../../../data/user"; +import { + showAlertDialog, + showPromptDialog, +} from "../../../dialogs/generic/show-dialog-box"; import { PolymerChangedEvent } from "../../../polymer-types"; import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import { UserDetailDialogParams } from "./show-dialog-user-detail"; -import { computeRTLDirection } from "../../../common/util/compute_rtl"; @customElement("dialog-user-detail") class DialogUserDetail extends LitElement { @@ -134,14 +139,22 @@ class DialogUserDetail extends LitElement { ${user.system_generated ? html` - + ${this.hass.localize( "ui.panel.config.users.editor.system_generated_users_not_removable" )} ` : ""} + ${!user.system_generated && this.hass.user?.is_owner + ? html` + ${this.hass.localize( + "ui.panel.config.users.editor.change_password" + )} + ` + : ""}
    +
    ${user.system_generated ? html` - + ${this.hass.localize( "ui.panel.config.users.editor.system_generated_users_not_editable" )} @@ -202,6 +215,52 @@ class DialogUserDetail extends LitElement { } } + private async _changePassword() { + const credential = this._params?.entry.credentials.find( + (cred) => cred.type === "homeassistant" + ); + if (!credential) { + showAlertDialog(this, { + title: "No Home Assistant credentials found.", + }); + return; + } + const newPassword = await showPromptDialog(this, { + title: this.hass.localize("ui.panel.config.users.editor.change_password"), + inputType: "password", + inputLabel: this.hass.localize( + "ui.panel.config.users.editor.new_password" + ), + }); + if (!newPassword) { + return; + } + const confirmPassword = await showPromptDialog(this, { + title: this.hass.localize("ui.panel.config.users.editor.change_password"), + inputType: "password", + inputLabel: this.hass.localize( + "ui.panel.config.users.add_user.password_confirm" + ), + }); + if (!confirmPassword) { + return; + } + if (newPassword !== confirmPassword) { + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.users.add_user.password_not_match" + ), + }); + return; + } + await adminChangePassword(this.hass, this._params!.entry.id, newPassword); + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.users.add_user.password_changed" + ), + }); + } + private _close(): void { this._params = undefined; } diff --git a/src/panels/config/zone/ha-config-zone.ts b/src/panels/config/zone/ha-config-zone.ts index b1690c6930..83910d3c15 100644 --- a/src/panels/config/zone/ha-config-zone.ts +++ b/src/panels/config/zone/ha-config-zone.ts @@ -11,9 +11,9 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, PropertyValues, query, TemplateResult, @@ -198,7 +198,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) { : mdiPencilOff} > - + ${state.entity_id === "zone.home" ? this.hass.localize( `ui.panel.config.zone.${ diff --git a/src/panels/developer-tools/service/developer-tools-service.js b/src/panels/developer-tools/service/developer-tools-service.js index 51db6e7645..a0478ac55d 100644 --- a/src/panels/developer-tools/service/developer-tools-service.js +++ b/src/panels/developer-tools/service/developer-tools-service.js @@ -1,8 +1,9 @@ -import "@material/mwc-button"; import { html } from "@polymer/polymer/lib/utils/html-tag"; /* eslint-plugin-disable lit */ import { PolymerElement } from "@polymer/polymer/polymer-element"; import { safeDump, safeLoad } from "js-yaml"; +import { computeRTL } from "../../../common/util/compute_rtl"; +import "../../../components/buttons/ha-progress-button"; import "../../../components/entity/ha-entity-picker"; import "../../../components/ha-code-editor"; import "../../../components/ha-service-picker"; @@ -11,7 +12,6 @@ import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import LocalizeMixin from "../../../mixins/localize-mixin"; import "../../../styles/polymer-ha-style"; import "../../../util/app-localstorage-document"; -import { computeRTL } from "../../../common/util/compute_rtl"; const ERROR_SENTINEL = {}; /* @@ -34,7 +34,7 @@ class HaPanelDevService extends LocalizeMixin(PolymerElement) { max-width: 400px; } - mwc-button { + ha-progress-button { margin-top: 8px; } @@ -136,9 +136,13 @@ class HaPanelDevService extends LocalizeMixin(PolymerElement) { error="[[!validJSON]]" on-value-changed="_yamlChanged" > - + [[localize('ui.panel.developer-tools.tabs.services.call_service')]] - +