diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md new file mode 100644 index 0000000000..f088f4c019 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.md @@ -0,0 +1,85 @@ +--- +name: Report a bug with the UI, Frontend or Lovelace +about: Report an issue related to the Home Assistant frontend. +labels: bug +--- + +## Checklist + +- [ ] I have updated to the latest available Home Assistant version. +- [ ] I have cleared the cache of my browser. +- [ ] I have tried a different browser to see if it is related to my browser. + +## The problem + + + +## Expected behavior + + + +## Steps to reproduce + + + +## Environment + + +- Home Assistant release with the issue: +- Last working Home Assistant release (if known): +- UI Type (States or Lovelace): +- Browser and browser version: +- Operating system: + +## Problem-relevant configuration + + +```yaml + +``` + +## Javascript errors shown in your browser console/inspector + + +```txt + +``` + +## Additional information + diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md new file mode 100644 index 0000000000..634724fcbe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md @@ -0,0 +1,25 @@ +--- +name: Request a feature for the UI, Frontend or Lovelace +about: Request an new feature for the Home Assistant frontend. +labels: feature request +--- + +## The request + + + +## The alternatives + + + +## Additional information + diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index a293b99f4a..0000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: "" -labels: bug -assignees: "" ---- - - - -**Checklist:** - -- [ ] I updated to the latest version available -- [ ] I cleared the cache of my browser - -**Home Assistant release with the issue:** - - - -**Last working Home Assistant release (if known):** - -**UI (States or Lovelace UI?):** - - - -**Browser and Operating System:** - - - -**Description of problem:** - - - -**Expected behaviour:** - - - -**Relevant config:** - - - -**Steps to reproduce this problem:** - - - -**Javascript errors shown in the web inspector (if applicable):** - -``` - -``` - -**Additional information:** diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..7581c03f17 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +blank_issues_enabled: false +contact_links: + - name: Report a bug that is NOT related to the UI, Frontend or Lovelace + url: https://github.com/home-assistant/home-assistant/issues + about: This is the issue tracker for our frontend. Please report other issues with the backend repository. + - name: Report incorrect or missing information on our website + url: https://github.com/home-assistant/home-assistant.io/issues + about: Our documentation has its own issue tracker. Please report issues with the website there. + - name: I have a question or need support + url: https://www.home-assistant.io/help + about: We use GitHub for tracking bugs, check our website for resources on getting help. + - name: I'm unsure where to go + url: https://www.home-assistant.io/join-chat + about: If you are unsure where to go, then joining our chat is recommended; Just ask! diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 51d1465c16..0000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: "" -labels: feature request -assignees: "" ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..f73933940c --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,77 @@ + +## Breaking change + + + +## Proposed change + + + +## Type of change + + +- [ ] Dependency upgrade +- [ ] Bugfix (non-breaking change which fixes an issue) +- [ ] New feature (thank you!) +- [ ] Breaking change (fix/feature causing existing functionality to break) +- [ ] Code quality improvements to existing code or addition of tests + +## Example configuration + + +```yaml + +``` + +## Additional information + + +- This PR fixes or closes issue: fixes # +- This PR is related to issue: +- Link to documentation pull request: + +## Checklist + + +- [ ] The code change is tested and works locally. +- [ ] There is no commented out code in this PR. +- [ ] Tests have been added to verify that the new code works. + +If user exposed functionality or configuration variables are added/changed: + +- [ ] Documentation added/updated for [www.home-assistant.io][docs-repository] + + +[docs-repository]: https://github.com/home-assistant/home-assistant.io diff --git a/.github/lock.yml b/.github/lock.yml new file mode 100644 index 0000000000..63db6533f2 --- /dev/null +++ b/.github/lock.yml @@ -0,0 +1,27 @@ +# Configuration for Lock Threads - https://github.com/dessant/lock-threads + +# Number of days of inactivity before a closed issue or pull request is locked +daysUntilLock: 1 + +# Skip issues and pull requests created before a given timestamp. Timestamp must +# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable +skipCreatedBefore: 2020-01-01 + +# Issues and pull requests with these labels will be ignored. Set to `[]` to disable +exemptLabels: [] + +# Label to add before locking, such as `outdated`. Set to `false` to disable +lockLabel: false + +# Comment to post before locking. Set to `false` to disable +lockComment: false + +# Assign `resolved` as the reason for locking. Set to `false` to disable +setLockReason: false + +# Limit to only `issues` or `pulls` +only: pulls + +# Optionally, specify configuration settings just for `issues` or `pulls` +issues: + daysUntilLock: 30 diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000000..dc0896c22c --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,56 @@ +# Configuration for probot-stale - https://github.com/probot/stale + +# Number of days of inactivity before an Issue or Pull Request becomes stale +daysUntilStale: 90 + +# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. +# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. +daysUntilClose: 7 + +# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) +onlyLabels: [] + +# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable +exemptLabels: + - feature request + - Help wanted + - to do + +# Set to true to ignore issues in a project (defaults to false) +exemptProjects: true + +# Set to true to ignore issues in a milestone (defaults to false) +exemptMilestones: true + +# Set to true to ignore issues with an assignee (defaults to false) +exemptAssignees: false + +# Label to use when marking as stale +staleLabel: stale + +# Comment to post when marking as stale. Set to `false` to disable +markComment: > + There hasn't been any activity on this issue recently. Due to the high number + of incoming GitHub notifications, we have to clean some of the old issues, + as many of them have already been resolved with the latest updates. + + Please make sure to update to the latest Home Assistant version and check + if that solves the issue. Let us know if that works for you by adding a + comment 👍 + + This issue now has been marked as stale and will be closed if no further + activity occurs. Thank you for your contributions. + +# Comment to post when removing the stale label. +# unmarkComment: > +# Your comment here. + +# Comment to post when closing a stale Issue or Pull Request. +# closeComment: > +# Your comment here. + +# Limit the number of actions per hour, from 1-30. Default is 30 +limitPerRun: 30 + +# Limit to only `issues` or `pulls` +only: issues diff --git a/azure-pipelines-netlify.yml b/azure-pipelines-netlify.yml new file mode 100644 index 0000000000..83cf89dcf9 --- /dev/null +++ b/azure-pipelines-netlify.yml @@ -0,0 +1,27 @@ +# https://dev.azure.com/home-assistant + +trigger: none +pr: none +schedules: + - cron: "0 0 * * *" + displayName: "build preview" + branches: + include: + - dev + always: false +variables: + - group: netlify + +jobs: + +- job: 'Netlify_preview' + pool: + vmImage: 'ubuntu-latest' + steps: + - script: | + # Cast + curl -X POST -d {} https://api.netlify.com/build_hooks/${NETLIFY_CAST} + + # Demo + curl -X POST -d {} https://api.netlify.com/build_hooks/${NETLIFY_DEMO} + displayName: 'Trigger netlify build preview' diff --git a/build-scripts/webpack.js b/build-scripts/webpack.js index d3c57b3c2a..042d978461 100644 --- a/build-scripts/webpack.js +++ b/build-scripts/webpack.js @@ -22,7 +22,11 @@ const createWebpackConfig = ({ isProdBuild, latestBuild, isStatsBuild, + dontHash, }) => { + if (!dontHash) { + dontHash = new Set(); + } return { mode: isProdBuild ? "production" : "development", devtool: isProdBuild ? "source-map" : "inline-cheap-module-source-map", @@ -103,8 +107,6 @@ const createWebpackConfig = ({ }, output: { filename: ({ chunk }) => { - const dontHash = new Set(); - if (!isProdBuild || dontHash.has(chunk.name)) { return `${chunk.name}.js`; } @@ -222,11 +224,12 @@ const createHassioConfig = ({ isProdBuild, latestBuild }) => { } const config = createWebpackConfig({ entry: { - entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.js"), + entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.ts"), }, outputRoot: "", isProdBuild, latestBuild, + dontHash: new Set(["entrypoint"]), }); config.output.path = paths.hassio_root; diff --git a/cast/src/launcher/layout/hc-cast.ts b/cast/src/launcher/layout/hc-cast.ts index a63ad71787..859b2cb911 100644 --- a/cast/src/launcher/layout/hc-cast.ts +++ b/cast/src/launcher/layout/hc-cast.ts @@ -39,7 +39,7 @@ class HcCast extends LitElement { @property() private askWrite = false; @property() private lovelaceConfig?: LovelaceConfig | null; - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (this.lovelaceConfig === undefined) { return html` > diff --git a/cast/src/launcher/layout/hc-connect.ts b/cast/src/launcher/layout/hc-connect.ts index a2a56403d1..a2ab3fb179 100644 --- a/cast/src/launcher/layout/hc-connect.ts +++ b/cast/src/launcher/layout/hc-connect.ts @@ -70,7 +70,7 @@ export class HcConnect extends LitElement { @property() private castManager?: CastManager | null; private openDemo = false; - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (this.cannotConnect) { const tokens = loadTokens(); return html` diff --git a/cast/src/launcher/layout/hc-layout.ts b/cast/src/launcher/layout/hc-layout.ts index 5decc615a4..aaf9d8c17a 100644 --- a/cast/src/launcher/layout/hc-layout.ts +++ b/cast/src/launcher/layout/hc-layout.ts @@ -22,7 +22,7 @@ class HcLayout extends LitElement { @property() public connection?: Connection; @property() public user?: HassUser; - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html`
@@ -50,13 +50,12 @@ class HcLayout extends LitElement {
`; } diff --git a/cast/src/receiver/layout/hc-demo.ts b/cast/src/receiver/layout/hc-demo.ts index 878026e555..4b597d6e1f 100644 --- a/cast/src/receiver/layout/hc-demo.ts +++ b/cast/src/receiver/layout/hc-demo.ts @@ -16,7 +16,7 @@ class HcDemo extends HassElement { @property() public lovelacePath!: string; @property() private _lovelaceConfig?: LovelaceConfig; - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this._lovelaceConfig) { return html``; } diff --git a/cast/src/receiver/layout/hc-launch-screen.ts b/cast/src/receiver/layout/hc-launch-screen.ts index a838ae3e7a..ee2586e391 100644 --- a/cast/src/receiver/layout/hc-launch-screen.ts +++ b/cast/src/receiver/layout/hc-launch-screen.ts @@ -14,7 +14,7 @@ class HcLaunchScreen extends LitElement { @property() public hass?: HomeAssistant; @property() public error?: string; - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html`
diff --git a/demo/src/custom-cards/cast-demo-row.ts b/demo/src/custom-cards/cast-demo-row.ts index fd852f9739..4a19eaf373 100644 --- a/demo/src/custom-cards/cast-demo-row.ts +++ b/demo/src/custom-cards/cast-demo-row.ts @@ -10,7 +10,7 @@ import { import "../../../src/components/ha-icon"; import { - EntityRow, + LovelaceRow, CastConfig, } from "../../../src/panels/lovelace/entity-rows/types"; import { HomeAssistant } from "../../../src/types"; @@ -18,7 +18,7 @@ import { CastManager } from "../../../src/cast/cast_manager"; import { castSendShowDemo } from "../../../src/cast/receiver_messages"; @customElement("cast-demo-row") -class CastDemoRow extends LitElement implements EntityRow { +class CastDemoRow extends LitElement implements LovelaceRow { public hass!: HomeAssistant; @property() private _castManager?: CastManager | null; @@ -27,7 +27,7 @@ class CastDemoRow extends LitElement implements EntityRow { // No config possible. } - protected render(): TemplateResult | void { + protected render(): TemplateResult { if ( !this._castManager || this._castManager.castState === "NO_DEVICES_AVAILABLE" diff --git a/gallery/src/components/demo-card.js b/gallery/src/components/demo-card.js index b782aaf882..e52cf02890 100644 --- a/gallery/src/components/demo-card.js +++ b/gallery/src/components/demo-card.js @@ -2,7 +2,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; import { safeLoad } from "js-yaml"; -import { createCardElement } from "../../../src/panels/lovelace/common/create-card-element"; +import { createCardElement } from "../../../src/panels/lovelace/create-element/create-card-element"; class DemoCard extends PolymerElement { static get template() { diff --git a/gallery/src/demos/demo-util-long-press.ts b/gallery/src/demos/demo-util-long-press.ts index 77cf1e83c0..b1da49043e 100644 --- a/gallery/src/demos/demo-util-long-press.ts +++ b/gallery/src/demos/demo-util-long-press.ts @@ -6,7 +6,7 @@ import { actionHandler } from "../../../src/panels/lovelace/common/directives/ac import { ActionHandlerEvent } from "../../../src/data/lovelace"; export class DemoUtilLongPress extends LitElement { - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html` ${this.renderStyle()} ${[1, 2, 3].map( diff --git a/hassio/src/addon-store/hassio-addon-repository.ts b/hassio/src/addon-store/hassio-addon-repository.ts index 5427ae8fe1..722697ecd2 100644 --- a/hassio/src/addon-store/hassio-addon-repository.ts +++ b/hassio/src/addon-store/hassio-addon-repository.ts @@ -15,7 +15,7 @@ import { HomeAssistant } from "../../../src/types"; import { HassioAddonInfo, HassioAddonRepository, -} from "../../../src/data/hassio"; +} from "../../../src/data/hassio/addon"; import { navigate } from "../../../src/common/navigate"; import { filterAndSort } from "../components/hassio-filter-addons"; @@ -36,7 +36,7 @@ class HassioAddonRepositoryEl extends LitElement { } ); - protected render(): TemplateResult | void { + protected render(): TemplateResult { const repo = this.repo; const addons = this._getAddons(this.addons, this.filter); diff --git a/hassio/src/addon-store/hassio-addon-store.ts b/hassio/src/addon-store/hassio-addon-store.ts index 2035a5b16f..628ef2bef9 100644 --- a/hassio/src/addon-store/hassio-addon-store.ts +++ b/hassio/src/addon-store/hassio-addon-store.ts @@ -14,7 +14,7 @@ import { HassioAddonInfo, fetchHassioAddonsInfo, reloadHassioAddons, -} from "../../../src/data/hassio"; +} from "../../../src/data/hassio/addon"; import "../../../src/layouts/loading-screen"; import "../components/hassio-search-input"; @@ -48,7 +48,7 @@ class HassioAddonStore extends LitElement { await this._loadData(); } - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this._addons || !this._repos) { return html` diff --git a/hassio/src/addon-store/hassio-repositories-editor.ts b/hassio/src/addon-store/hassio-repositories-editor.ts index 55832c00ef..5110a77c45 100644 --- a/hassio/src/addon-store/hassio-repositories-editor.ts +++ b/hassio/src/addon-store/hassio-repositories-editor.ts @@ -17,7 +17,7 @@ import "../../../src/components/buttons/ha-call-api-button"; import "../components/hassio-card-content"; import { hassioStyle } from "../resources/hassio-style"; import { HomeAssistant } from "../../../src/types"; -import { HassioAddonRepository } from "../../../src/data/hassio"; +import { HassioAddonRepository } from "../../../src/data/hassio/addon"; import { PolymerChangedEvent } from "../../../src/polymer-types"; import { repeat } from "lit-html/directives/repeat"; @@ -33,7 +33,7 @@ class HassioRepositoriesEditor extends LitElement { .sort((a, b) => (a.name < b.name ? -1 : 1)) ); - protected render(): TemplateResult | void { + protected render(): TemplateResult { const repos = this._sortedRepos(this.repos); return html`
diff --git a/hassio/src/addon-view/hassio-addon-audio.js b/hassio/src/addon-view/hassio-addon-audio.js deleted file mode 100644 index 84ec925d80..0000000000 --- a/hassio/src/addon-view/hassio-addon-audio.js +++ /dev/null @@ -1,138 +0,0 @@ -import "web-animations-js/web-animations-next-lite.min"; - -import "@material/mwc-button"; -import "@polymer/paper-card/paper-card"; -import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -import "../../../src/resources/ha-style"; -import { EventsMixin } from "../../../src/mixins/events-mixin"; - -class HassioAddonAudio extends EventsMixin(PolymerElement) { - static get template() { - return html` - - -
- - - - - - - - - - - - -
-
- Save -
-
- `; - } - - static get properties() { - return { - hass: Object, - addon: { - type: Object, - observer: "addonChanged", - }, - inputDevices: Array, - outputDevices: Array, - selectedInput: String, - selectedOutput: String, - error: String, - }; - } - - addonChanged(addon) { - this.setProperties({ - selectedInput: addon.audio_input || "null", - selectedOutput: addon.audio_output || "null", - }); - if (this.outputDevices) return; - - const noDevice = [{ device: "null", name: "-" }]; - this.hass.callApi("get", "hassio/hardware/audio").then( - (resp) => { - const dev = resp.data.audio; - const input = Object.keys(dev.input).map((key) => ({ - device: key, - name: dev.input[key], - })); - const output = Object.keys(dev.output).map((key) => ({ - device: key, - name: dev.output[key], - })); - this.setProperties({ - inputDevices: noDevice.concat(input), - outputDevices: noDevice.concat(output), - }); - }, - () => { - this.setProperties({ - inputDevices: noDevice, - outputDevices: noDevice, - }); - } - ); - } - - _saveSettings() { - this.error = null; - const path = `hassio/addons/${this.addon.slug}/options`; - this.hass - .callApi("post", path, { - audio_input: this.selectedInput === "null" ? null : this.selectedInput, - audio_output: - this.selectedOutput === "null" ? null : this.selectedOutput, - }) - .then( - () => { - this.fire("hass-api-called", { success: true, path: path }); - }, - (resp) => { - this.error = resp.body.message; - } - ); - } -} - -customElements.define("hassio-addon-audio", HassioAddonAudio); diff --git a/hassio/src/addon-view/hassio-addon-audio.ts b/hassio/src/addon-view/hassio-addon-audio.ts new file mode 100644 index 0000000000..c0718a4c9a --- /dev/null +++ b/hassio/src/addon-view/hassio-addon-audio.ts @@ -0,0 +1,188 @@ +import "web-animations-js/web-animations-next-lite.min"; + +import "@material/mwc-button"; +import "@polymer/paper-card/paper-card"; +import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; +import "@polymer/paper-item/paper-item"; +import "@polymer/paper-listbox/paper-listbox"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + PropertyValues, + TemplateResult, +} from "lit-element"; + +import { HomeAssistant } from "../../../src/types"; +import { + HassioAddonDetails, + setHassioAddonOption, + HassioAddonSetOptionParams, +} from "../../../src/data/hassio/addon"; +import { + HassioHardwareAudioDevice, + fetchHassioHardwareAudio, +} from "../../../src/data/hassio/hardware"; +import { hassioStyle } from "../resources/hassio-style"; +import { haStyle } from "../../../src/resources/styles"; + +@customElement("hassio-addon-audio") +class HassioAddonAudio extends LitElement { + @property() public hass!: HomeAssistant; + @property() public addon!: HassioAddonDetails; + @property() private _error?: string; + @property() private _inputDevices?: HassioHardwareAudioDevice[]; + @property() private _outputDevices?: HassioHardwareAudioDevice[]; + @property() private _selectedInput!: null | string; + @property() private _selectedOutput!: null | string; + + protected render(): TemplateResult { + return html` + +
+ ${this._error + ? html` +
${this._error}
+ ` + : ""} + + + + ${this._inputDevices && + this._inputDevices.map((item) => { + return html` + ${item.name} + `; + })} + + + + + ${this._outputDevices && + this._outputDevices.map((item) => { + return html` + ${item.name} + `; + })} + + +
+
+ Save +
+
+ `; + } + + static get styles(): CSSResult[] { + return [ + haStyle, + hassioStyle, + css` + :host, + paper-card, + paper-dropdown-menu { + display: block; + } + .errors { + color: var(--google-red-500); + margin-bottom: 16px; + } + paper-item { + width: 450px; + } + .card-actions { + text-align: right; + } + `, + ]; + } + + protected update(changedProperties: PropertyValues): void { + super.update(changedProperties); + if (changedProperties.has("addon")) { + this._addonChanged(); + } + } + + private _setInputDevice(ev): void { + const device = ev.detail.device; + if (device) { + this._selectedInput = device; + } + } + + private _setOutputDevice(ev): void { + const device = ev.detail.device; + if (device) { + this._selectedOutput = device; + } + } + + private async _addonChanged(): Promise { + this._selectedInput = this.addon.audio_input; + this._selectedOutput = this.addon.audio_output; + if (this._outputDevices) { + return; + } + + const noDevice: HassioHardwareAudioDevice[] = [ + { device: undefined, name: "-" }, + ]; + + try { + const { audio } = await fetchHassioHardwareAudio(this.hass); + const inupt = Object.keys(audio.input).map((key) => ({ + device: key, + name: audio.input[key], + })); + const output = Object.keys(audio.output).map((key) => ({ + device: key, + name: audio.output[key], + })); + + this._inputDevices = noDevice.concat(inupt); + this._outputDevices = noDevice.concat(output); + } catch { + this._error = "Failed to fetch audio hardware"; + this._inputDevices = noDevice; + this._outputDevices = noDevice; + } + } + + private async _saveSettings(): Promise { + this._error = undefined; + const data: HassioAddonSetOptionParams = { + audio_input: this._selectedInput || null, + audio_output: this._selectedOutput || null, + }; + try { + await setHassioAddonOption(this.hass, this.addon.slug, data); + } catch { + this._error = "Failed to set addon audio device"; + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "hassio-addon-audio": HassioAddonAudio; + } +} diff --git a/hassio/src/addon-view/hassio-addon-config.js b/hassio/src/addon-view/hassio-addon-config.js deleted file mode 100644 index a4061b8899..0000000000 --- a/hassio/src/addon-view/hassio-addon-config.js +++ /dev/null @@ -1,111 +0,0 @@ -import "@polymer/iron-autogrow-textarea/iron-autogrow-textarea"; -import "@material/mwc-button"; -import "@polymer/paper-card/paper-card"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -import "../../../src/components/buttons/ha-call-api-button"; - -class HassioAddonConfig extends PolymerElement { - static get template() { - return html` - - -
- - -
-
- Reset to defaults - Save -
-
- `; - } - - static get properties() { - return { - hass: Object, - addon: { - type: Object, - observer: "addonChanged", - }, - addonSlug: String, - config: { - type: String, - observer: "configChanged", - }, - configParsed: Object, - error: String, - resetData: { - type: Object, - value: { - options: null, - }, - }, - }; - } - - addonChanged(addon) { - this.config = addon ? JSON.stringify(addon.options, null, 2) : ""; - } - - configChanged(config) { - try { - this.$.config.classList.remove("syntaxerror"); - this.configParsed = JSON.parse(config); - } catch (err) { - this.$.config.classList.add("syntaxerror"); - this.configParsed = null; - } - } - - saveTapped() { - this.error = null; - - this.hass - .callApi("post", `hassio/addons/${this.addonSlug}/options`, { - options: this.configParsed, - }) - .catch((resp) => { - this.error = resp.body.message; - }); - } -} - -customElements.define("hassio-addon-config", HassioAddonConfig); diff --git a/hassio/src/addon-view/hassio-addon-config.ts b/hassio/src/addon-view/hassio-addon-config.ts new file mode 100644 index 0000000000..5d36267a81 --- /dev/null +++ b/hassio/src/addon-view/hassio-addon-config.ts @@ -0,0 +1,158 @@ +import "@polymer/iron-autogrow-textarea/iron-autogrow-textarea"; +import "@material/mwc-button"; +import "@polymer/paper-card/paper-card"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + PropertyValues, + TemplateResult, +} from "lit-element"; + +import { HomeAssistant } from "../../../src/types"; +import { + HassioAddonDetails, + setHassioAddonOption, + HassioAddonSetOptionParams, +} from "../../../src/data/hassio/addon"; +import { hassioStyle } from "../resources/hassio-style"; +import { haStyle } from "../../../src/resources/styles"; +import { PolymerChangedEvent } from "../../../src/polymer-types"; +import { fireEvent } from "../../../src/common/dom/fire_event"; + +@customElement("hassio-addon-config") +class HassioAddonConfig extends LitElement { + @property() public hass!: HomeAssistant; + @property() public addon!: HassioAddonDetails; + @property() private _error?: string; + @property() private _config!: string; + @property({ type: Boolean }) private _configHasChanged = false; + + protected render(): TemplateResult { + return html` + +
+ ${this._error + ? html` +
${this._error}
+ ` + : ""} + +
+
+ + Reset to defaults + + + Save + +
+
+ `; + } + + static get styles(): CSSResult[] { + return [ + haStyle, + hassioStyle, + css` + :host { + display: block; + } + paper-card { + display: block; + } + .card-actions { + display: flex; + justify-content: space-between; + } + .errors { + color: var(--google-red-500); + margin-bottom: 16px; + } + iron-autogrow-textarea { + width: 100%; + font-family: monospace; + } + .syntaxerror { + color: var(--google-red-500); + } + `, + ]; + } + + protected updated(changedProperties: PropertyValues): void { + super.updated(changedProperties); + if (changedProperties.has("addon")) { + this._config = JSON.stringify(this.addon.options, null, 2); + } + } + + private _configChanged(ev: PolymerChangedEvent): void { + this._config = + ev.detail.value || JSON.stringify(this.addon.options, null, 2); + this._configHasChanged = + this._config !== JSON.stringify(this.addon.options, null, 2); + } + + private async _resetTapped(): Promise { + this._error = undefined; + const data: HassioAddonSetOptionParams = { + options: null, + }; + try { + await setHassioAddonOption(this.hass, this.addon.slug, data); + this._configHasChanged = false; + const eventdata = { + success: true, + response: undefined, + path: "options", + }; + fireEvent(this, "hass-api-called", eventdata); + } catch (err) { + this._error = `Failed to reset addon configuration, ${err.body?.message || + err}`; + } + } + + private async _saveTapped(): Promise { + let data: HassioAddonSetOptionParams; + this._error = undefined; + try { + data = { + options: JSON.parse(this._config), + }; + } catch (err) { + this._error = err; + return; + } + try { + await setHassioAddonOption(this.hass, this.addon.slug, data); + this._configHasChanged = false; + const eventdata = { + success: true, + response: undefined, + path: "options", + }; + fireEvent(this, "hass-api-called", eventdata); + } catch (err) { + this._error = `Failed to save addon configuration, ${err.body?.message || + err}`; + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "hassio-addon-config": HassioAddonConfig; + } +} diff --git a/hassio/src/addon-view/hassio-addon-info.js b/hassio/src/addon-view/hassio-addon-info.js deleted file mode 100644 index 1ef1386ecb..0000000000 --- a/hassio/src/addon-view/hassio-addon-info.js +++ /dev/null @@ -1,624 +0,0 @@ -import "@polymer/iron-icon/iron-icon"; -import "@material/mwc-button"; -import "@polymer/paper-card/paper-card"; -import "@polymer/paper-tooltip/paper-tooltip"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -import "../../../src/components/ha-label-badge"; -import "../../../src/components/ha-markdown"; -import "../../../src/components/buttons/ha-call-api-button"; -import "../../../src/components/ha-switch"; -import "../../../src/resources/ha-style"; -import "../components/hassio-card-content"; - -import { EventsMixin } from "../../../src/mixins/events-mixin"; -import { navigate } from "../../../src/common/navigate"; -import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown"; - -const PERMIS_DESC = { - rating: { - title: "Add-on Security Rating", - description: - "Hass.io provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an add-on requires on your system, the lower the score, thus raising the possible security risks.\n\nA score is on a scale from 1 to 6. Where 1 is the lowest score (considered the most insecure and highest risk) and a score of 6 is the highest score (considered the most secure and lowest risk).", - }, - host_network: { - title: "Host Network", - description: - "Add-ons usually run in their own isolated network layer, which prevents them from accessing the network of the host operating system. In some cases, this network isolation can limit add-ons in providing their services and therefore, the isolation can be lifted by the add-on author, giving the add-on full access to the network capabilities of the host machine. This gives the add-on more networking capabilities but lowers the security, hence, the security rating of the add-on will be lowered when this option is used by the add-on.", - }, - homeassistant_api: { - title: "Home Assistant API Access", - description: - "This add-on is allowed to access your running Home Assistant instance directly via the Home Assistant API. This mode handles authentication for the add-on as well, which enables an add-on to interact with Home Assistant without the need for additional authentication tokens.", - }, - full_access: { - title: "Full Hardware Access", - description: - "This add-on is given full access to the hardware of your system, by request of the add-on author. Access is comparable to the privileged mode in Docker. Since this opens up possible security risks, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.", - }, - hassio_api: { - title: "Hass.io API Access", - description: - "The add-on was given access to the Hass.io API, by request of the add-on author. By default, the add-on can access general version information of your system. When the add-on requests 'manager' or 'admin' level access to the API, it will gain access to control multiple parts of your Hass.io system. This permission is indicated by this badge and will impact the security score of the addon negatively.", - }, - docker_api: { - title: "Full Docker Access", - description: - "The add-on author has requested the add-on to have management access to the Docker instance running on your system. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.", - }, - host_pid: { - title: "Host Processes Namespace", - description: - "Usually, the processes the add-on runs, are isolated from all other system processes. The add-on author has requested the add-on to have access to the system processes running on the host system instance, and allow the add-on to spawn processes on the host system as well. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.", - }, - apparmor: { - title: "AppArmor", - description: - "AppArmor ('Application Armor') is a Linux kernel security module that restricts add-ons capabilities like network access, raw socket access, and permission to read, write, or execute specific files.\n\nAdd-on authors can provide their security profiles, optimized for the add-on, or request it to be disabled. If AppArmor is disabled, it will raise security risks and therefore, has a negative impact on the security score of the add-on.", - }, - auth_api: { - title: "Home Assistant Authentication", - description: - "An add-on can authenticate users against Home Assistant, allowing add-ons to give users the possibility to log into applications running inside add-ons, using their Home Assistant username/password. This badge indicates if the add-on author requests this capability.", - }, - ingress: { - title: "Ingress", - description: - "This add-on is using Ingress to embed its interface securely into Home Assistant.", - }, -}; - -class HassioAddonInfo extends EventsMixin(PolymerElement) { - static get template() { - return html` - - - - - - - -
-
- [[addon.name]] -
- - -
-
-
- [[addon.description]].
- Visit - [[addon.name]] page for - details. -
- -
- - - - - - - - - - -
- -
-
- - -
-
- - `; - } - - static get properties() { - return { - hass: Object, - addon: Object, - addonSlug: String, - isRunning: { type: Boolean, computed: "computeIsRunning(addon)" }, - }; - } - - computeIsRunning(addon) { - return addon && addon.state === "started"; - } - - computeUpdateAvailable(addon) { - return ( - addon && - !addon.detached && - addon.version && - addon.version !== addon.last_version - ); - } - - computeHassioApi(addon) { - return ( - addon.hassio_api && - (addon.hassio_role === "manager" || addon.hassio_role === "admin") - ); - } - - computeApparmorClassName(apparmor) { - if (apparmor === "profile") { - return "green"; - } - if (apparmor === "disable") { - return "red"; - } - return ""; - } - - pathWebui(webui) { - return webui && webui.replace("[HOST]", document.location.hostname); - } - - computeShowWebUI(ingress, webui, isRunning) { - return !ingress && webui && isRunning; - } - - openIngress() { - navigate(this, `/hassio/ingress/${this.addon.slug}`); - } - - computeShowIngressUI(ingress, isRunning) { - return ingress && isRunning; - } - - computeStartOnBoot(state) { - return state === "auto"; - } - - computeSecurityClassName(rating) { - if (rating > 4) { - return "green"; - } - if (rating > 2) { - return "yellow"; - } - return "red"; - } - - startOnBootToggled() { - const data = { boot: this.addon.boot === "auto" ? "manual" : "auto" }; - this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/options`, data); - } - - autoUpdateToggled() { - const data = { auto_update: !this.addon.auto_update }; - this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/options`, data); - } - - protectionToggled() { - const data = { protected: !this.addon.protected }; - this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/security`, data); - this.set("addon.protected", !this.addon.protected); - } - - panelToggled() { - const data = { ingress_panel: !this.addon.ingress_panel }; - this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/options`, data); - } - - showMoreInfo(e) { - const id = e.target.getAttribute("id"); - showHassioMarkdownDialog(this, { - title: PERMIS_DESC[id].title, - content: PERMIS_DESC[id].description, - }); - } - - openChangelog() { - this.hass - .callApi("get", `hassio/addons/${this.addonSlug}/changelog`) - .then( - (resp) => resp, - () => "Error getting changelog" - ) - .then((content) => { - showHassioMarkdownDialog(this, { - title: "Changelog", - content: content, - }); - }); - } - - _unistallClicked() { - if (!confirm("Are you sure you want to uninstall this add-on?")) { - return; - } - const path = `hassio/addons/${this.addonSlug}/uninstall`; - const eventData = { - path: path, - }; - this.hass - .callApi("post", path) - .then( - (resp) => { - eventData.success = true; - eventData.response = resp; - }, - (resp) => { - eventData.success = false; - eventData.response = resp; - } - ) - .then(() => { - this.fire("hass-api-called", eventData); - }); - } - - _computeCannotIngressSidebar(hass, addon) { - return !addon.ingress || !this._computeHA92plus(hass); - } - - _computeUsesProtectedOptions(addon) { - return addon.docker_api || addon.full_access || addon.host_pid; - } - - _computeHA92plus(hass) { - const [major, minor] = hass.config.version.split(".", 2); - return Number(major) > 0 || (major === "0" && Number(minor) >= 92); - } -} -customElements.define("hassio-addon-info", HassioAddonInfo); diff --git a/hassio/src/addon-view/hassio-addon-info.ts b/hassio/src/addon-view/hassio-addon-info.ts new file mode 100644 index 0000000000..c31de13b80 --- /dev/null +++ b/hassio/src/addon-view/hassio-addon-info.ts @@ -0,0 +1,796 @@ +import "@material/mwc-button"; +import "@polymer/iron-icon/iron-icon"; +import "@polymer/paper-card/paper-card"; +import "@polymer/paper-tooltip/paper-tooltip"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import { classMap } from "lit-html/directives/class-map"; + +import "../../../src/components/buttons/ha-call-api-button"; +import "../../../src/components/buttons/ha-progress-button"; +import "../../../src/components/ha-label-badge"; +import "../../../src/components/ha-markdown"; +import "../../../src/components/ha-switch"; +import "../components/hassio-card-content"; + +import { fireEvent } from "../../../src/common/dom/fire_event"; +import { + HassioAddonDetails, + HassioAddonSetOptionParams, + HassioAddonSetSecurityParams, + setHassioAddonOption, + setHassioAddonSecurity, + uninstallHassioAddon, + installHassioAddon, + fetchHassioAddonChangelog, +} from "../../../src/data/hassio/addon"; +import { hassioStyle } from "../resources/hassio-style"; +import { haStyle } from "../../../src/resources/styles"; +import { HomeAssistant } from "../../../src/types"; +import { navigate } from "../../../src/common/navigate"; +import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown"; + +const PERMIS_DESC = { + rating: { + title: "Add-on Security Rating", + description: + "Hass.io provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an add-on requires on your system, the lower the score, thus raising the possible security risks.\n\nA score is on a scale from 1 to 6. Where 1 is the lowest score (considered the most insecure and highest risk) and a score of 6 is the highest score (considered the most secure and lowest risk).", + }, + host_network: { + title: "Host Network", + description: + "Add-ons usually run in their own isolated network layer, which prevents them from accessing the network of the host operating system. In some cases, this network isolation can limit add-ons in providing their services and therefore, the isolation can be lifted by the add-on author, giving the add-on full access to the network capabilities of the host machine. This gives the add-on more networking capabilities but lowers the security, hence, the security rating of the add-on will be lowered when this option is used by the add-on.", + }, + homeassistant_api: { + title: "Home Assistant API Access", + description: + "This add-on is allowed to access your running Home Assistant instance directly via the Home Assistant API. This mode handles authentication for the add-on as well, which enables an add-on to interact with Home Assistant without the need for additional authentication tokens.", + }, + full_access: { + title: "Full Hardware Access", + description: + "This add-on is given full access to the hardware of your system, by request of the add-on author. Access is comparable to the privileged mode in Docker. Since this opens up possible security risks, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.", + }, + hassio_api: { + title: "Hass.io API Access", + description: + "The add-on was given access to the Hass.io API, by request of the add-on author. By default, the add-on can access general version information of your system. When the add-on requests 'manager' or 'admin' level access to the API, it will gain access to control multiple parts of your Hass.io system. This permission is indicated by this badge and will impact the security score of the addon negatively.", + }, + docker_api: { + title: "Full Docker Access", + description: + "The add-on author has requested the add-on to have management access to the Docker instance running on your system. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.", + }, + host_pid: { + title: "Host Processes Namespace", + description: + "Usually, the processes the add-on runs, are isolated from all other system processes. The add-on author has requested the add-on to have access to the system processes running on the host system instance, and allow the add-on to spawn processes on the host system as well. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.", + }, + apparmor: { + title: "AppArmor", + description: + "AppArmor ('Application Armor') is a Linux kernel security module that restricts add-ons capabilities like network access, raw socket access, and permission to read, write, or execute specific files.\n\nAdd-on authors can provide their security profiles, optimized for the add-on, or request it to be disabled. If AppArmor is disabled, it will raise security risks and therefore, has a negative impact on the security score of the add-on.", + }, + auth_api: { + title: "Home Assistant Authentication", + description: + "An add-on can authenticate users against Home Assistant, allowing add-ons to give users the possibility to log into applications running inside add-ons, using their Home Assistant username/password. This badge indicates if the add-on author requests this capability.", + }, + ingress: { + title: "Ingress", + description: + "This add-on is using Ingress to embed its interface securely into Home Assistant.", + }, +}; + +@customElement("hassio-addon-info") +class HassioAddonInfo extends LitElement { + @property() public hass!: HomeAssistant; + @property() public addon!: HassioAddonDetails; + @property() private _error?: string; + @property({ type: Boolean }) private _installing = false; + + protected render(): TemplateResult { + return html` + ${this._computeUpdateAvailable + ? html` + +
+ + ${!this.addon.available + ? html` +

+ This update is no longer compatible with your system. +

+ ` + : ""} +
+
+ + Update + + ${this.addon.changelog + ? html` + + Changelog + + ` + : ""} +
+
+ ` + : ""} + ${!this.addon.protected + ? html` + +
+ Protection mode on this add-on is disabled! This gives the add-on full access to the entire system, which adds security risks, and could damage your system when used incorrectly. Only disable the protection mode if you know, need AND trust the source of this add-on. +
+
+ Enable Protection mode +
+
+ + ` + : ""} + + +
+
+ ${this.addon.name} +
+ ${this.addon.version + ? html` + ${this.addon.version} + ${this._computeIsRunning + ? html` + + ` + : html` + + `} + ` + : html` + ${this.addon.last_version} + `} +
+
+
+ ${this.addon.description}.
+ Visit + + ${this.addon.name} page + for details. +
+ ${this.addon.logo + ? html` + + ` + : ""} +
+ + ${this.addon.host_network + ? html` + + ` + : ""} + ${this.addon.full_access + ? html` + + ` + : ""} + ${this.addon.homeassistant_api + ? html` + + ` + : ""} + ${this._computeHassioApi + ? html` + + ` + : ""} + ${this.addon.docker_api + ? html` + + ` + : ""} + ${this.addon.host_pid + ? html` + + ` + : ""} + ${this.addon.apparmor + ? html` + + ` + : ""} + ${this.addon.auth_api + ? html` + + ` + : ""} + ${this.addon.ingress + ? html` + + ` + : ""} +
+ + ${this.addon.version + ? html` +
+
Start on boot
+ +
+
+
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. + + +
+ +
+ ` + : ""} + ` + : ""} + ${this._error + ? html` +
${this._error}
+ ` + : ""} +
+
+ ${this.addon.version + ? html` + + Uninstall + + ${this.addon.build + ? html` + + Rebuild + + ` + : ""} + ${this._computeIsRunning + ? html` + + Restart + + + Stop + + ` + : html` + + Start + + `} + ${this._computeShowWebUI + ? html` + + + Open web UI + + + ` + : ""} + ${this._computeShowIngressUI + ? html` + + Open web UI + + ` + : ""} + ` + : html` + ${!this.addon.available + ? html` +

+ This add-on is not available on your system. +

+ ` + : ""} + + Install + + `} +
+
+ + ${this.addon.long_description + ? html` + +
+ +
+
+ ` + : ""} + `; + } + + static get styles(): CSSResult[] { + return [ + haStyle, + hassioStyle, + css` + :host { + display: block; + } + paper-card { + display: block; + margin-bottom: 16px; + } + paper-card.warning { + background-color: var(--google-red-500); + color: white; + --paper-card-header-color: white; + } + paper-card.warning mwc-button { + --mdc-theme-primary: white !important; + } + .warning { + color: var(--google-red-500); + --mdc-theme-primary: var(--google-red-500); + } + .light-color { + color: var(--secondary-text-color); + } + .addon-header { + font-size: 24px; + color: var(--paper-card-header-color, --primary-text-color); + } + .addon-version { + float: right; + font-size: 15px; + vertical-align: middle; + } + .errors { + color: var(--google-red-500); + margin-bottom: 16px; + } + .description { + margin-bottom: 16px; + } + .logo img { + max-height: 60px; + margin: 16px 0; + display: block; + } + .state { + display: flex; + margin: 33px 0; + } + .state div { + width: 180px; + display: inline-block; + } + .state iron-icon { + width: 16px; + height: 16px; + color: var(--secondary-text-color); + } + ha-switch { + display: flex; + } + iron-icon.running { + color: var(--paper-green-400); + } + iron-icon.stopped { + color: var(--google-red-300); + } + ha-call-api-button { + font-weight: 500; + color: var(--primary-color); + } + .right { + float: right; + } + ha-markdown img { + max-width: 100%; + } + protection-enable mwc-button { + --mdc-theme-primary: white; + } + .description a, + ha-markdown 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; + --iron-icon-height: 45px; + } + `, + ]; + } + + private get _computeHassioApi(): boolean { + return ( + this.addon.hassio_api && + (this.addon.hassio_role === "manager" || + this.addon.hassio_role === "admin") + ); + } + + private get _computeApparmorClassName(): string { + if (this.addon.apparmor === "profile") { + return "green"; + } + if (this.addon.apparmor === "disable") { + return "red"; + } + return ""; + } + + private _showMoreInfo(ev): void { + const id = ev.target.getAttribute("id"); + showHassioMarkdownDialog(this, { + title: PERMIS_DESC[id].title, + content: PERMIS_DESC[id].description, + }); + } + + private get _computeIsRunning(): boolean { + return this.addon?.state === "started"; + } + + private get _computeUpdateAvailable(): boolean | "" { + return ( + this.addon && + !this.addon.detached && + this.addon.version && + this.addon.version !== this.addon.last_version + ); + } + + private get _pathWebui(): string | null { + return ( + this.addon.webui && + this.addon.webui.replace("[HOST]", document.location.hostname) + ); + } + + private get _computeShowWebUI(): boolean | "" | null { + return !this.addon.ingress && this.addon.webui && this._computeIsRunning; + } + + private _openIngress(): void { + navigate(this, `/hassio/ingress/${this.addon.slug}`); + } + + private get _computeShowIngressUI(): boolean { + return this.addon.ingress && this._computeIsRunning; + } + + private get _computeCannotIngressSidebar(): boolean { + return !this.addon.ingress || !this._computeHA92plus; + } + + private get _computeUsesProtectedOptions(): boolean { + return ( + this.addon.docker_api || this.addon.full_access || this.addon.host_pid + ); + } + + private get _computeHA92plus(): boolean { + const [major, minor] = this.hass.config.version.split(".", 2); + return Number(major) > 0 || (major === "0" && Number(minor) >= 92); + } + + private async _startOnBootToggled(): Promise { + this._error = undefined; + const data: HassioAddonSetOptionParams = { + boot: this.addon.boot === "auto" ? "manual" : "auto", + }; + 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 = { + auto_update: !this.addon.auto_update, + }; + 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 _protectionToggled(): Promise { + this._error = undefined; + const data: HassioAddonSetSecurityParams = { + protected: !this.addon.protected, + }; + try { + await setHassioAddonSecurity(this.hass, this.addon.slug, data); + const eventdata = { + success: true, + response: undefined, + path: "security", + }; + fireEvent(this, "hass-api-called", eventdata); + } catch (err) { + this._error = `Failed to set addon security option, ${err.body?.message || + err}`; + } + } + + private async _panelToggled(): Promise { + this._error = undefined; + const data: HassioAddonSetOptionParams = { + ingress_panel: !this.addon.ingress_panel, + }; + 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 _openChangelog(): Promise { + this._error = undefined; + try { + const content = await fetchHassioAddonChangelog( + this.hass, + this.addon.slug + ); + showHassioMarkdownDialog(this, { + title: "Changelog", + content, + }); + } catch (err) { + this._error = `Failed to get addon changelog, ${err.body?.message || + err}`; + } + } + + private async _installClicked(): Promise { + this._error = undefined; + this._installing = true; + try { + await installHassioAddon(this.hass, this.addon.slug); + const eventdata = { + success: true, + response: undefined, + path: "install", + }; + fireEvent(this, "hass-api-called", eventdata); + } catch (err) { + this._error = `Failed to install addon, ${err.body?.message || err}`; + } + this._installing = false; + } + + private async _uninstallClicked(): Promise { + if (!confirm("Are you sure you want to uninstall this add-on?")) { + return; + } + this._error = undefined; + try { + await uninstallHassioAddon(this.hass, this.addon.slug); + const eventdata = { + success: true, + response: undefined, + path: "uninstall", + }; + fireEvent(this, "hass-api-called", eventdata); + } catch (err) { + this._error = `Failed to uninstall addon, ${err.body?.message || err}`; + } + } +} +declare global { + interface HTMLElementTagNameMap { + "hassio-addon-info": HassioAddonInfo; + } +} diff --git a/hassio/src/addon-view/hassio-addon-logs.js b/hassio/src/addon-view/hassio-addon-logs.js deleted file mode 100644 index 154630b01e..0000000000 --- a/hassio/src/addon-view/hassio-addon-logs.js +++ /dev/null @@ -1,66 +0,0 @@ -import "@material/mwc-button"; -import "@polymer/paper-card/paper-card"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html"; - -import "../../../src/resources/ha-style"; - -class HassioAddonLogs extends PolymerElement { - static get template() { - return html` - - ${ANSI_HTML_STYLE} - -
-
- Refresh -
-
- `; - } - - static get properties() { - return { - hass: Object, - addonSlug: { - type: String, - observer: "addonSlugChanged", - }, - }; - } - - addonSlugChanged(slug) { - if (!this.hass) { - setTimeout(() => { - this.addonChanged(slug); - }, 0); - return; - } - - this.refresh(); - } - - refresh() { - this.hass - .callApi("get", `hassio/addons/${this.addonSlug}/logs`) - .then((text) => { - while (this.$.content.lastChild) { - this.$.content.removeChild(this.$.content.lastChild); - } - this.$.content.appendChild(parseTextToColoredPre(text)); - }); - } -} - -customElements.define("hassio-addon-logs", HassioAddonLogs); diff --git a/hassio/src/addon-view/hassio-addon-logs.ts b/hassio/src/addon-view/hassio-addon-logs.ts new file mode 100644 index 0000000000..0f568df2c7 --- /dev/null +++ b/hassio/src/addon-view/hassio-addon-logs.ts @@ -0,0 +1,95 @@ +import "@material/mwc-button"; +import "@polymer/paper-card/paper-card"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, + query, +} from "lit-element"; +import { HomeAssistant } from "../../../src/types"; +import { + HassioAddonDetails, + fetchHassioAddonLogs, +} from "../../../src/data/hassio/addon"; +import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html"; +import { hassioStyle } from "../resources/hassio-style"; +import { haStyle } from "../../../src/resources/styles"; + +@customElement("hassio-addon-logs") +class HassioAddonLogs extends LitElement { + @property() public hass!: HomeAssistant; + @property() public addon!: HassioAddonDetails; + @property() private _error?: string; + @query("#content") private _logContent!: any; + + public async connectedCallback(): Promise { + super.connectedCallback(); + await this._loadData(); + } + + protected render(): TemplateResult { + return html` + + ${this._error + ? html` +
${this._error}
+ ` + : ""} +
+
+ Refresh +
+
+ `; + } + + static get styles(): CSSResult[] { + return [ + haStyle, + hassioStyle, + ANSI_HTML_STYLE, + css` + :host, + paper-card { + display: block; + } + pre { + overflow-x: auto; + white-space: pre-wrap; + overflow-wrap: break-word; + } + .errors { + color: var(--google-red-500); + margin-bottom: 16px; + } + `, + ]; + } + + private async _loadData(): Promise { + this._error = undefined; + try { + const content = await fetchHassioAddonLogs(this.hass, this.addon.slug); + while (this._logContent.lastChild) { + this._logContent.removeChild(this._logContent.lastChild as Node); + } + this._logContent.appendChild(parseTextToColoredPre(content)); + } catch (err) { + this._error = `Failed to get addon logs, ${err.body?.message || err}`; + } + } + + private async _refresh(): Promise { + await this._loadData(); + } +} + +declare global { + interface HTMLElementTagNameMap { + "hassio-addon-logs": HassioAddonLogs; + } +} diff --git a/hassio/src/addon-view/hassio-addon-network.js b/hassio/src/addon-view/hassio-addon-network.js deleted file mode 100644 index fcb80ce0e5..0000000000 --- a/hassio/src/addon-view/hassio-addon-network.js +++ /dev/null @@ -1,129 +0,0 @@ -import "@polymer/paper-card/paper-card"; -import "@polymer/paper-input/paper-input"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -import "../../../src/components/buttons/ha-call-api-button"; -import "../../../src/resources/ha-style"; -import { EventsMixin } from "../../../src/mixins/events-mixin"; - -class HassioAddonNetwork extends EventsMixin(PolymerElement) { - static get template() { - return html` - - -
- - - - - - - - - - - -
ContainerHostDescription
-
-
- Reset to defaults - Save -
-
- `; - } - - static get properties() { - return { - hass: Object, - addonSlug: String, - config: Object, - addon: { - type: Object, - observer: "addonChanged", - }, - error: String, - resetData: { - type: Object, - value: { - network: null, - }, - }, - }; - } - - addonChanged(addon) { - if (!addon) return; - - const network = addon.network || {}; - const description = addon.network_description || {}; - const items = Object.keys(network).map((key) => ({ - container: key, - host: network[key], - description: description[key], - })); - this.config = items.sort(function(el1, el2) { - return el1.host - el2.host; - }); - } - - saveTapped() { - this.error = null; - const data = {}; - this.config.forEach(function(item) { - data[item.container] = parseInt(item.host); - }); - const path = `hassio/addons/${this.addonSlug}/options`; - - this.hass - .callApi("post", path, { - network: data, - }) - .then( - () => { - this.fire("hass-api-called", { success: true, path: path }); - }, - (resp) => { - this.error = resp.body.message; - } - ); - } -} - -customElements.define("hassio-addon-network", HassioAddonNetwork); diff --git a/hassio/src/addon-view/hassio-addon-network.ts b/hassio/src/addon-view/hassio-addon-network.ts new file mode 100644 index 0000000000..2ad9e008a7 --- /dev/null +++ b/hassio/src/addon-view/hassio-addon-network.ts @@ -0,0 +1,202 @@ +import "@polymer/paper-card/paper-card"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + PropertyValues, + TemplateResult, +} from "lit-element"; + +import { PaperInputElement } from "@polymer/paper-input/paper-input"; + +import { HomeAssistant } from "../../../src/types"; +import { + HassioAddonDetails, + HassioAddonSetOptionParams, + setHassioAddonOption, +} from "../../../src/data/hassio/addon"; +import { hassioStyle } from "../resources/hassio-style"; +import { haStyle } from "../../../src/resources/styles"; +import { fireEvent } from "../../../src/common/dom/fire_event"; + +interface NetworkItem { + description: string; + container: string; + host: number | null; +} + +interface NetworkItemInput extends PaperInputElement { + container: string; +} + +@customElement("hassio-addon-network") +class HassioAddonNetwork extends LitElement { + @property() public hass!: HomeAssistant; + @property() public addon!: HassioAddonDetails; + @property() private _error?: string; + @property() private _config?: NetworkItem[]; + + public connectedCallback(): void { + super.connectedCallback(); + this._setNetworkConfig(); + } + + protected render(): TemplateResult { + if (!this._config) { + return html``; + } + + return html` + +
+ ${this._error + ? html` +
${this._error}
+ ` + : ""} + + + + + + + + + ${this._config!.map((item) => { + return html` + + + + + + `; + })} + +
ContainerHostDescription
${item.container} + + ${item.description}
+
+
+ + Reset to defaults + + Save +
+
+ `; + } + + static get styles(): CSSResult[] { + return [ + haStyle, + hassioStyle, + css` + :host { + display: block; + } + paper-card { + display: block; + } + .errors { + color: var(--google-red-500); + margin-bottom: 16px; + } + .card-actions { + display: flex; + justify-content: space-between; + } + `, + ]; + } + + protected update(changedProperties: PropertyValues): void { + super.update(changedProperties); + if (changedProperties.has("addon")) { + this._setNetworkConfig(); + } + } + + private _setNetworkConfig(): void { + const network = this.addon.network || {}; + const description = this.addon.network_description || {}; + const items: NetworkItem[] = Object.keys(network).map((key) => { + return { + container: key, + host: network[key], + description: description[key], + }; + }); + this._config = items.sort((a, b) => (a.container > b.container ? 1 : -1)); + } + + private async _configChanged(ev: Event): Promise { + const target = ev.target as NetworkItemInput; + this._config!.forEach((item) => { + if ( + item.container === target.container && + item.host !== parseInt(String(target.value), 10) + ) { + item.host = target.value ? parseInt(String(target.value), 10) : null; + } + }); + } + + private async _resetTapped(): Promise { + const data: HassioAddonSetOptionParams = { + network: null, + }; + + 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 network configuration, ${err.body + ?.message || err}`; + } + } + + private async _saveTapped(): Promise { + this._error = undefined; + const networkconfiguration = {}; + this._config!.forEach((item) => { + networkconfiguration[item.container] = parseInt(String(item.host), 10); + }); + + const data: HassioAddonSetOptionParams = { + network: networkconfiguration, + }; + + 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 network configuration, ${err.body + ?.message || err}`; + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "hassio-addon-network": HassioAddonNetwork; + } +} diff --git a/hassio/src/addon-view/hassio-addon-view.js b/hassio/src/addon-view/hassio-addon-view.js deleted file mode 100644 index 26aaa7c50c..0000000000 --- a/hassio/src/addon-view/hassio-addon-view.js +++ /dev/null @@ -1,139 +0,0 @@ -import "@polymer/app-layout/app-header-layout/app-header-layout"; -import "@polymer/app-layout/app-header/app-header"; -import "@polymer/app-layout/app-toolbar/app-toolbar"; -import "@polymer/paper-icon-button/paper-icon-button"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -import "./hassio-addon-audio"; -import "./hassio-addon-config"; -import "./hassio-addon-info"; -import "./hassio-addon-logs"; -import "./hassio-addon-network"; - -class HassioAddonView extends PolymerElement { - static get template() { - return html` - - -
- - - -
-
- `; - } - - static get properties() { - return { - hass: Object, - route: { - type: Object, - observer: "routeDataChanged", - }, - addonSlug: { - type: String, - computed: "_computeSlug(route)", - }, - addon: Object, - }; - } - - ready() { - super.ready(); - this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev)); - } - - apiCalled(ev) { - const path = ev.detail.path; - - if (!path) return; - - if (path.substr(path.lastIndexOf("/") + 1) === "uninstall") { - history.back(); - } else { - this.routeDataChanged(this.route); - } - } - - routeDataChanged(routeData) { - const addon = routeData.path.substr(1); - this.hass.callApi("get", `hassio/addons/${addon}/info`).then( - (info) => { - this.addon = info.data; - }, - () => { - this.addon = null; - } - ); - } - - _computeSlug(route) { - return route.path.substr(1); - } -} - -customElements.define("hassio-addon-view", HassioAddonView); diff --git a/hassio/src/addon-view/hassio-addon-view.ts b/hassio/src/addon-view/hassio-addon-view.ts new file mode 100644 index 0000000000..4f5b2cfd70 --- /dev/null +++ b/hassio/src/addon-view/hassio-addon-view.ts @@ -0,0 +1,159 @@ +import "@polymer/app-layout/app-header-layout/app-header-layout"; +import "@polymer/app-layout/app-header/app-header"; +import "@polymer/app-layout/app-toolbar/app-toolbar"; +import "@polymer/paper-icon-button/paper-icon-button"; +import "@polymer/paper-spinner/paper-spinner-lite"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; + +import { HomeAssistant, Route } from "../../../src/types"; +import { + HassioAddonDetails, + fetchHassioAddonInfo, +} from "../../../src/data/hassio/addon"; +import { hassioStyle } from "../resources/hassio-style"; +import { haStyle } from "../../../src/resources/styles"; + +import "./hassio-addon-audio"; +import "./hassio-addon-config"; +import "./hassio-addon-info"; +import "./hassio-addon-logs"; +import "./hassio-addon-network"; + +@customElement("hassio-addon-view") +class HassioAddonView extends LitElement { + @property() public hass!: HomeAssistant; + @property() public route!: Route; + @property() public addon?: HassioAddonDetails; + + protected render(): TemplateResult { + if (!this.addon) { + return html` + + `; + } + return html` + +
+ + + ${this.addon && this.addon.version + ? html` + + + ${this.addon.audio + ? html` + + ` + : ""} + ${this.addon.network + ? html` + + ` + : ""} + + + ` + : ""} +
+
+ `; + } + + static get styles(): CSSResult[] { + return [ + haStyle, + hassioStyle, + css` + :host { + color: var(--primary-text-color); + --paper-card-header-color: var(--primary-text-color); + } + .content { + padding: 24px 0 32px; + display: flex; + flex-direction: column; + align-items: center; + } + hassio-addon-info, + hassio-addon-network, + hassio-addon-audio, + hassio-addon-config { + margin-bottom: 24px; + width: 600px; + } + hassio-addon-logs { + max-width: calc(100% - 8px); + min-width: 600px; + } + @media only screen and (max-width: 600px) { + hassio-addon-info, + hassio-addon-network, + hassio-addon-audio, + hassio-addon-config, + hassio-addon-logs { + max-width: 100%; + min-width: 100%; + } + } + `, + ]; + } + + protected async firstUpdated(): Promise { + await this._routeDataChanged(this.route); + this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev)); + } + + private async _apiCalled(ev): Promise { + const path: string = ev.detail.path; + + if (!path) { + return; + } + + if (path === "uninstall") { + history.back(); + } else { + await this._routeDataChanged(this.route); + } + } + + private async _routeDataChanged(routeData: Route): Promise { + const addon = routeData.path.substr(1); + try { + const addoninfo = await fetchHassioAddonInfo(this.hass, addon); + this.addon = addoninfo; + } catch { + this.addon = undefined; + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "hassio-addon-view": HassioAddonView; + } +} diff --git a/hassio/src/ansi-to-html.js b/hassio/src/ansi-to-html.ts similarity index 62% rename from hassio/src/ansi-to-html.js rename to hassio/src/ansi-to-html.ts index 438873127c..cbe66946bc 100644 --- a/hassio/src/ansi-to-html.js +++ b/hassio/src/ansi-to-html.ts @@ -1,68 +1,75 @@ -import { html } from "@polymer/polymer/lib/utils/html-tag"; +import { css } from "lit-element"; -export const ANSI_HTML_STYLE = html` - +interface State { + bold: boolean; + italic: boolean; + underline: boolean; + strikethrough: boolean; + foregroundColor: null | string; + backgroundColor: null | string; +} + +export const ANSI_HTML_STYLE = css` + .bold { + font-weight: bold; + } + .italic { + font-style: italic; + } + .underline { + text-decoration: underline; + } + .strikethrough { + text-decoration: line-through; + } + .underline.strikethrough { + text-decoration: underline line-through; + } + .fg-red { + color: rgb(222, 56, 43); + } + .fg-green { + color: rgb(57, 181, 74); + } + .fg-yellow { + color: rgb(255, 199, 6); + } + .fg-blue { + color: rgb(0, 111, 184); + } + .fg-magenta { + color: rgb(118, 38, 113); + } + .fg-cyan { + color: rgb(44, 181, 233); + } + .fg-white { + color: rgb(204, 204, 204); + } + .bg-black { + background-color: rgb(0, 0, 0); + } + .bg-red { + background-color: rgb(222, 56, 43); + } + .bg-green { + background-color: rgb(57, 181, 74); + } + .bg-yellow { + background-color: rgb(255, 199, 6); + } + .bg-blue { + background-color: rgb(0, 111, 184); + } + .bg-magenta { + background-color: rgb(118, 38, 113); + } + .bg-cyan { + background-color: rgb(44, 181, 233); + } + .bg-white { + background-color: rgb(204, 204, 204); + } `; export function parseTextToColoredPre(text) { @@ -70,7 +77,7 @@ export function parseTextToColoredPre(text) { const re = /\033(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g; let i = 0; - const state = { + const state: State = { bold: false, italic: false, underline: false, @@ -81,29 +88,42 @@ export function parseTextToColoredPre(text) { const addSpan = (content) => { const span = document.createElement("span"); - if (state.bold) span.classList.add("bold"); - if (state.italic) span.classList.add("italic"); - if (state.underline) span.classList.add("underline"); - if (state.strikethrough) span.classList.add("strikethrough"); - if (state.foregroundColor !== null) + if (state.bold) { + span.classList.add("bold"); + } + if (state.italic) { + span.classList.add("italic"); + } + if (state.underline) { + span.classList.add("underline"); + } + if (state.strikethrough) { + span.classList.add("strikethrough"); + } + if (state.foregroundColor !== null) { span.classList.add(`fg-${state.foregroundColor}`); - if (state.backgroundColor !== null) + } + if (state.backgroundColor !== null) { span.classList.add(`bg-${state.backgroundColor}`); + } span.appendChild(document.createTextNode(content)); pre.appendChild(span); }; /* eslint-disable no-cond-assign */ let match; + // tslint:disable-next-line while ((match = re.exec(text)) !== null) { - const j = match.index; + const j = match!.index; addSpan(text.substring(i, j)); i = j + match[0].length; - if (match[1] === undefined) continue; + if (match[1] === undefined) { + continue; + } - match[1].split(";").forEach((colorCode) => { - switch (parseInt(colorCode)) { + match[1].split(";").forEach((colorCode: string) => { + switch (parseInt(colorCode, 10)) { case 0: // reset state.bold = false; diff --git a/hassio/src/components/hassio-card-content.ts b/hassio/src/components/hassio-card-content.ts index 3e9de6c253..ad13904c20 100644 --- a/hassio/src/components/hassio-card-content.ts +++ b/hassio/src/components/hassio-card-content.ts @@ -23,7 +23,7 @@ class HassioCardContent extends LitElement { @property() public iconClass?: string; @property() public icon = "hass:help-circle"; - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html` - paper-card { - cursor: pointer; - } - -
-
Add-ons
- - -
- `; - } - - static get properties() { - return { - hass: Object, - addons: Array, - }; - } - - sortAddons(a, b) { - return a.name < b.name ? -1 : 1; - } - - computeIcon(addon) { - return addon.installed !== addon.version - ? "hassio:arrow-up-bold-circle" - : "hassio:puzzle"; - } - - computeIconTitle(addon) { - if (addon.installed !== addon.version) return "New version available"; - return addon.state === "started" - ? "Add-on is running" - : "Add-on is stopped"; - } - - computeIconClass(addon) { - if (addon.installed !== addon.version) return "update"; - return addon.state === "started" ? "running" : ""; - } - - addonTapped(ev) { - this.navigate("/hassio/addon/" + ev.model.addon.slug); - ev.target.blur(); - } - - openStore(ev) { - this.navigate("/hassio/store"); - ev.target.blur(); - } -} - -customElements.define("hassio-addons", HassioAddons); diff --git a/hassio/src/dashboard/hassio-addons.ts b/hassio/src/dashboard/hassio-addons.ts new file mode 100644 index 0000000000..3d21fc9ea4 --- /dev/null +++ b/hassio/src/dashboard/hassio-addons.ts @@ -0,0 +1,108 @@ +import "@polymer/paper-card/paper-card"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; + +import { HomeAssistant } from "../../../src/types"; +import { HassioAddonInfo } from "../../../src/data/hassio/addon"; +import { navigate } from "../../../src/common/navigate"; +import { hassioStyle } from "../resources/hassio-style"; +import { haStyle } from "../../../src/resources/styles"; +import "../components/hassio-card-content"; + +@customElement("hassio-addons") +class HassioAddons extends LitElement { + @property() public hass!: HomeAssistant; + @property() public addons?: HassioAddonInfo[]; + + protected render(): TemplateResult { + return html` +
+
Add-ons
+ ${!this.addons + ? html` + +
+ You don't have any add-ons installed yet. Head over to + the add-on store to + get started! +
+
+ ` + : this.addons + .sort((a, b) => (a.name > b.name ? 1 : -1)) + .map( + (addon) => html` + +
+ +
+
+ ` + )} +
+ `; + } + + static get styles(): CSSResult[] { + return [ + haStyle, + hassioStyle, + css` + paper-card { + cursor: pointer; + } + `, + ]; + } + + private _computeIcon(addon: HassioAddonInfo): string { + return addon.installed !== addon.version + ? "hassio:arrow-up-bold-circle" + : "hassio:puzzle"; + } + + private _computeIconTitle(addon: HassioAddonInfo): string { + if (addon.installed !== addon.version) { + return "New version available"; + } + return addon.state === "started" + ? "Add-on is running" + : "Add-on is stopped"; + } + + private _computeIconClass(addon: HassioAddonInfo): string { + if (addon.installed !== addon.version) { + return "update"; + } + return addon.state === "started" ? "running" : ""; + } + + private _addonTapped(ev: any): void { + navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}`); + } + + private _openStore(): void { + navigate(this, "/hassio/store"); + } +} + +declare global { + interface HTMLElementTagNameMap { + "hassio-addons": HassioAddons; + } +} diff --git a/hassio/src/dashboard/hassio-dashboard.ts b/hassio/src/dashboard/hassio-dashboard.ts index cec8ce74d3..a1db26663a 100644 --- a/hassio/src/dashboard/hassio-dashboard.ts +++ b/hassio/src/dashboard/hassio-dashboard.ts @@ -9,22 +9,22 @@ import { } from "lit-element"; import "./hassio-addons"; import "./hassio-update"; +import { haStyle } from "../../../src/resources/styles"; import { HomeAssistant } from "../../../src/types"; +import { HassioHassOSInfo } from "../../../src/data/hassio/host"; import { HassioSupervisorInfo, HassioHomeAssistantInfo, - HassioHassOSInfo, -} from "../../../src/data/hassio"; +} from "../../../src/data/hassio/supervisor"; @customElement("hassio-dashboard") class HassioDashboard extends LitElement { @property() public hass!: HomeAssistant; - @property() public supervisorInfo!: HassioSupervisorInfo; @property() public hassInfo!: HassioHomeAssistantInfo; @property() public hassOsInfo!: HassioHassOSInfo; - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html`
- ${this.error + ${this._error ? html` -
Error: ${this.error}
+
Error: ${this._error}
` : ""}
@@ -134,19 +133,20 @@ export class HassioUpdate extends LitElement { private _apiCalled(ev) { if (ev.detail.success) { - this.error = ""; + this._error = ""; return; } const response = ev.detail.response; typeof response.body === "object" - ? (this.error = response.body.message || "Unknown error") - : (this.error = response.body); + ? (this._error = response.body.message || "Unknown error") + : (this._error = response.body); } static get styles(): CSSResult[] { return [ + haStyle, hassioStyle, css` :host { diff --git a/hassio/src/dialogs/markdown/dialog-hassio-markdown.ts b/hassio/src/dialogs/markdown/dialog-hassio-markdown.ts index 87eb12e765..a8af334830 100644 --- a/hassio/src/dialogs/markdown/dialog-hassio-markdown.ts +++ b/hassio/src/dialogs/markdown/dialog-hassio-markdown.ts @@ -1,20 +1,59 @@ import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; import "@polymer/paper-icon-button/paper-icon-button"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -import "../../../../src/components/ha-markdown"; -import "../../../../src/resources/ha-style"; -import "../../../../src/components/dialog/ha-paper-dialog"; -import { customElement } from "lit-element"; import { PaperDialogElement } from "@polymer/paper-dialog"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, + query, +} from "lit-element"; + +import { hassioStyle } from "../../resources/hassio-style"; +import { haStyleDialog } from "../../../../src/resources/styles"; +import { HassioMarkdownDialogParams } from "./show-dialog-hassio-markdown"; + +import "../../../../src/components/dialog/ha-paper-dialog"; +import "../../../../src/components/ha-markdown"; @customElement("dialog-hassio-markdown") -class HassioMarkdownDialog extends PolymerElement { - static get template() { +class HassioMarkdownDialog extends LitElement { + @property() public title!: string; + @property() public content!: string; + @query("#dialog") private _dialog!: PaperDialogElement; + + public showDialog(params: HassioMarkdownDialogParams) { + this.title = params.title; + this.content = params.content; + this._dialog.open(); + } + + protected render(): TemplateResult { return html` - - - - -
[[title]]
-
- - - -
- `; - } - - static get properties() { - return { - title: String, - content: String, - }; - } - - public showDialog(params) { - this.setProperties(params); - (this.$.dialog as PaperDialogElement).open(); + `, + ]; } } diff --git a/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts b/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts index f0ae53eac4..56baec7e8c 100755 --- a/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts +++ b/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts @@ -1,20 +1,33 @@ -import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@material/mwc-button"; -import "@polymer/paper-checkbox/paper-checkbox"; +import "@polymer/app-layout/app-toolbar/app-toolbar"; +import "@polymer/iron-icon/iron-icon"; import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; import "@polymer/paper-icon-button/paper-icon-button"; -import "@polymer/iron-icon/iron-icon"; import "@polymer/paper-input/paper-input"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { getSignedPath } from "../../../../src/data/auth"; - -import "../../../../src/resources/ha-style"; -import "../../../../src/components/dialog/ha-paper-dialog"; -import { customElement } from "lit-element"; import { PaperDialogElement } from "@polymer/paper-dialog"; +import { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, + query, +} from "lit-element"; + +import { + fetchHassioSnapshotInfo, + HassioSnapshotDetail, +} from "../../../../src/data/hassio/snapshot"; +import { getSignedPath } from "../../../../src/data/auth"; import { HassioSnapshotDialogParams } from "./show-dialog-hassio-snapshot"; -import { fetchHassioSnapshotInfo } from "../../../../src/data/hassio"; +import { haStyleDialog } from "../../../../src/resources/styles"; +import { HomeAssistant } from "../../../../src/types"; +import { PolymerChangedEvent } from "../../../../src/polymer-types"; + +import "../../../../src/components/dialog/ha-paper-dialog"; const _computeFolders = (folders) => { const list: Array<{ slug: string; name: string; checked: boolean }> = []; @@ -46,21 +59,179 @@ const _computeAddons = (addons) => { })); }; -@customElement("dialog-hassio-snapshot") -class HassioSnapshotDialog extends PolymerElement { - // Commented out because it breaks Polymer! Kept around for when we migrate - // to Lit. Now just putting ts-ignore everywhere because we need this out. - // Sorry future developer. - // public hass!: HomeAssistant; - // protected error?: string; - // private snapshot?: any; - // private dialogParams?: HassioSnapshotDialogParams; - // private restoreHass!: boolean; - // private snapshotPassword!: string; +interface AddonItem { + slug: string; + name: string; + version: string; + checked: boolean | null | undefined; +} - static get template() { +interface FolderItem { + slug: string; + name: string; + checked: boolean | null | undefined; +} + +@customElement("dialog-hassio-snapshot") +class HassioSnapshotDialog extends LitElement { + @property() public hass!: HomeAssistant; + @property() private _error?: string; + @property() private snapshot?: HassioSnapshotDetail; + @property() private _folders!: FolderItem[]; + @property() private _addons!: AddonItem[]; + @property() private _dialogParams?: HassioSnapshotDialogParams; + @property() private _snapshotPassword!: string; + @property() private _restoreHass: boolean | null | undefined = true; + @query("#dialog") private _dialog!: PaperDialogElement; + + public async showDialog(params: HassioSnapshotDialogParams) { + this.snapshot = await fetchHassioSnapshotInfo(this.hass, params.slug); + this._folders = _computeFolders( + this.snapshot.folders + ).sort((a: FolderItem, b: FolderItem) => (a.name > b.name ? 1 : -1)); + this._addons = _computeAddons( + this.snapshot.addons + ).sort((a: AddonItem, b: AddonItem) => (a.name > b.name ? 1 : -1)); + + this._dialogParams = params; + + try { + this._dialog.open(); + } catch { + await this.showDialog(params); + } + } + + protected render(): TemplateResult { + if (!this.snapshot) { + return html``; + } return html` - - - - -
[[_computeName(snapshot)]]
-
-
- [[_computeType(snapshot.type)]] ([[_computeSize(snapshot.size)]])
- [[_formatDatetime(snapshot.date)]] -
-
Home Assistant:
- - Home Assistant [[snapshot.homeassistant]] - - - - - -
Actions:
-
    -
  • - - - Download Snapshot - -
  • -
  • - - - Restore Selected - -
  • - -
  • - - - Delete Snapshot - -
  • -
-
- `; + `, + ]; } - static get properties() { - return { - hass: Object, - dialogParams: Object, - snapshot: Object, - _folders: Object, - _addons: Object, - restoreHass: { - type: Boolean, - value: true, - }, - snapshotPassword: String, - error: String, - }; - } - - public async showDialog(params: HassioSnapshotDialogParams) { - // @ts-ignore - const snapshot = await fetchHassioSnapshotInfo(this.hass, params.slug); - this.setProperties({ - dialogParams: params, - snapshot, - _folders: _computeFolders(snapshot.folders), - _addons: _computeAddons(snapshot.addons), + private _updateFolders(item: FolderItem, value: boolean | null | undefined) { + this._folders = this._folders.map((folder) => { + if (folder.slug === item.slug) { + folder.checked = value; + } + return folder; }); - (this.$.dialog as PaperDialogElement).open(); } - protected _isFullSnapshot(type) { - return type === "full"; + private _updateAddons(item: AddonItem, value: boolean | null | undefined) { + this._addons = this._addons.map((addon) => { + if (addon.slug === item.slug) { + addon.checked = value; + } + return addon; + }); } - protected _partialRestoreClicked() { + private _passwordInput(ev: PolymerChangedEvent) { + this._snapshotPassword = ev.detail.value; + } + + private _partialRestoreClicked() { if (!confirm("Are you sure you want to restore this snapshot?")) { return; } - // @ts-ignore + const addons = this._addons .filter((addon) => addon.checked) .map((addon) => addon.slug); - // @ts-ignore + const folders = this._folders .filter((folder) => folder.checked) .map((folder) => folder.slug); - const data = { - // @ts-ignore - homeassistant: this.restoreHass, + const data: { + homeassistant: boolean | null | undefined; + addons: any; + folders: any; + password?: string; + } = { + homeassistant: this._restoreHass, addons, folders, }; - // @ts-ignore - if (this.snapshot.protected) { - // @ts-ignore - data.password = this.snapshotPassword; + + if (this.snapshot!.protected) { + data.password = this._snapshotPassword; } - // @ts-ignore this.hass .callApi( "POST", - // @ts-ignore - `hassio/snapshots/${this.dialogParams!.slug}/restore/partial`, + + `hassio/snapshots/${this.snapshot!.slug}/restore/partial`, data ) .then( () => { alert("Snapshot restored!"); - (this.$.dialog as PaperDialogElement).close(); + this._dialog.close(); }, (error) => { - // @ts-ignore - this.error = error.body.message; + this._error = error.body.message; } ); } - protected _fullRestoreClicked() { + private _fullRestoreClicked() { if (!confirm("Are you sure you want to restore this snapshot?")) { return; } - // @ts-ignore - const data = this.snapshot.protected - ? { - password: - // @ts-ignore - this.snapshotPassword, - } + + const data = this.snapshot!.protected + ? { password: this._snapshotPassword } : undefined; - // @ts-ignore + this.hass .callApi( "POST", - // @ts-ignore - `hassio/snapshots/${this.dialogParams!.slug}/restore/full`, + `hassio/snapshots/${this.snapshot!.slug}/restore/full`, data ) .then( () => { alert("Snapshot restored!"); - (this.$.dialog as PaperDialogElement).close(); + this._dialog.close(); }, (error) => { - // @ts-ignore - this.error = error.body.message; + this._error = error.body.message; } ); } - protected _deleteClicked() { + private _deleteClicked() { if (!confirm("Are you sure you want to delete this snapshot?")) { return; } - // @ts-ignore + this.hass - // @ts-ignore - .callApi("POST", `hassio/snapshots/${this.dialogParams!.slug}/remove`) + + .callApi("POST", `hassio/snapshots/${this.snapshot!.slug}/remove`) .then( () => { - (this.$.dialog as PaperDialogElement).close(); - // @ts-ignore - this.dialogParams!.onDelete(); + this._dialog.close(); + this._dialogParams!.onDelete(); }, (error) => { - // @ts-ignore - this.error = error.body.message; + this._error = error.body.message; } ); } - protected async _downloadClicked() { - let signedPath; + private async _downloadClicked() { + let signedPath: { path: string }; try { signedPath = await getSignedPath( - // @ts-ignore this.hass, - // @ts-ignore - `/api/hassio/snapshots/${this.dialogParams!.slug}/download` + `/api/hassio/snapshots/${this.snapshot!.slug}/download` ); } catch (err) { alert(`Error: ${err.message}`); return; } - // @ts-ignore - const name = this._computeName(this.snapshot).replace(/[^a-z0-9]+/gi, "_"); + + const name = this._computeName.replace(/[^a-z0-9]+/gi, "_"); const a = document.createElement("a"); a.href = signedPath.path; a.download = `Hass_io_${name}.tar`; - this.$.dialog.appendChild(a); + this._dialog.appendChild(a); a.click(); - this.$.dialog.removeChild(a); + this._dialog.removeChild(a); } - protected _computeName(snapshot) { - return snapshot ? snapshot.name || snapshot.slug : "Unnamed snapshot"; + private get _computeName() { + return this.snapshot + ? this.snapshot.name || this.snapshot.slug + : "Unnamed snapshot"; } - protected _computeType(type) { - return type === "full" ? "Full snapshot" : "Partial snapshot"; + private get _computeSize() { + return Math.ceil(this.snapshot!.size * 10) / 10 + " MB"; } - protected _computeSize(size) { - return Math.ceil(size * 10) / 10 + " MB"; - } - - protected _sortAddons(a, b) { - return a.name < b.name ? -1 : 1; - } - - protected _formatDatetime(datetime) { + private _formatDatetime(datetime) { return new Date(datetime).toLocaleDateString(navigator.language, { weekday: "long", year: "numeric", @@ -375,13 +442,12 @@ class HassioSnapshotDialog extends PolymerElement { }); } - protected _dialogClosed() { - this.setProperties({ - dialogParams: undefined, - snapshot: undefined, - _addons: [], - _folders: [], - }); + private _dialogClosed() { + this._dialogParams = undefined; + this.snapshot = undefined; + this._snapshotPassword = ""; + this._folders = []; + this._addons = []; } } diff --git a/hassio/src/entrypoint.js b/hassio/src/entrypoint.ts similarity index 100% rename from hassio/src/entrypoint.js rename to hassio/src/entrypoint.ts diff --git a/hassio/src/hassio-main.ts b/hassio/src/hassio-main.ts index 1b1257b06c..78954a6675 100644 --- a/hassio/src/hassio-main.ts +++ b/hassio/src/hassio-main.ts @@ -12,21 +12,24 @@ import { import { HomeAssistant } from "../../src/types"; import { fetchHassioSupervisorInfo, - fetchHassioHostInfo, - fetchHassioHassOsInfo, fetchHassioHomeAssistantInfo, HassioSupervisorInfo, - HassioHostInfo, - HassioHassOSInfo, HassioHomeAssistantInfo, - fetchHassioAddonInfo, createHassioSession, HassioPanelInfo, -} from "../../src/data/hassio"; +} from "../../src/data/hassio/supervisor"; +import { + fetchHassioHostInfo, + fetchHassioHassOsInfo, + HassioHostInfo, + HassioHassOSInfo, +} from "../../src/data/hassio/host"; +import { fetchHassioAddonInfo } from "../../src/data/hassio/addon"; import { makeDialogManager } from "../../src/dialogs/make-dialog-manager"; import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin"; // Don't codesplit it, that way the dashboard always loads fast. import "./hassio-pages-with-tabs"; +import { navigate } from "../../src/common/navigate"; // The register callback of the IronA11yKeysBehavior inside paper-icon-button // is not called, causing _keyBindings to be uninitiliazed for paper-icon-button, @@ -165,14 +168,20 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) { }), ]); if (!addon.ingress_url) { - throw new Error("Add-on does not support Ingress"); + alert("Add-on does not support Ingress"); + return; + } + if (addon.state !== "started") { + alert("Add-on is not running. Please start it first"); + navigate(this, `/hassio/addon/${addon.slug}`, true); + return; } location.assign(addon.ingress_url); // await a promise that doesn't resolve, so we show the loading screen // while we load the next page. await new Promise(() => undefined); } catch (err) { - alert(`Unable to open ingress connection `); + alert("Unable to open ingress connection"); } } diff --git a/hassio/src/hassio-pages-with-tabs.ts b/hassio/src/hassio-pages-with-tabs.ts index fad59ec5d9..9bb5eca5cd 100644 --- a/hassio/src/hassio-pages-with-tabs.ts +++ b/hassio/src/hassio-pages-with-tabs.ts @@ -23,12 +23,11 @@ import scrollToTarget from "../../src/common/dom/scroll-to-target"; import { haStyle } from "../../src/resources/styles"; import { HomeAssistant, Route } from "../../src/types"; import { navigate } from "../../src/common/navigate"; +import { HassioHostInfo, HassioHassOSInfo } from "../../src/data/hassio/host"; import { HassioSupervisorInfo, - HassioHostInfo, HassioHomeAssistantInfo, - HassioHassOSInfo, -} from "../../src/data/hassio"; +} from "../../src/data/hassio/supervisor"; const HAS_REFRESH_BUTTON = ["store", "snapshots"]; @@ -42,7 +41,7 @@ class HassioPagesWithTabs extends LitElement { @property() public hassInfo!: HassioHomeAssistantInfo; @property() public hassOsInfo!: HassioHassOSInfo; - protected render(): TemplateResult | void { + protected render(): TemplateResult { const page = this._page; return html` @@ -127,6 +126,10 @@ class HassioPagesWithTabs extends LitElement { --paper-tabs-selection-bar-color: #fff; text-transform: uppercase; } + app-header, + app-toolbar { + background-color: var(--primary-color); + } `, ]; } diff --git a/hassio/src/hassio-tabs-router.ts b/hassio/src/hassio-tabs-router.ts index a90a683fc0..a77d821bc8 100644 --- a/hassio/src/hassio-tabs-router.ts +++ b/hassio/src/hassio-tabs-router.ts @@ -11,12 +11,11 @@ import "./dashboard/hassio-dashboard"; import "./snapshots/hassio-snapshots"; import "./addon-store/hassio-addon-store"; import "./system/hassio-system"; +import { HassioHostInfo, HassioHassOSInfo } from "../../src/data/hassio/host"; import { HassioSupervisorInfo, - HassioHostInfo, HassioHomeAssistantInfo, - HassioHassOSInfo, -} from "../../src/data/hassio"; +} from "../../src/data/hassio/supervisor"; @customElement("hassio-tabs-router") class HassioTabsRouter extends HassRouterPage { diff --git a/hassio/src/ingress-view/hassio-ingress-view.ts b/hassio/src/ingress-view/hassio-ingress-view.ts index bcf6ed0e4a..a9ccf349f1 100644 --- a/hassio/src/ingress-view/hassio-ingress-view.ts +++ b/hassio/src/ingress-view/hassio-ingress-view.ts @@ -9,11 +9,11 @@ import { css, } from "lit-element"; import { HomeAssistant, Route } from "../../../src/types"; +import { createHassioSession } from "../../../src/data/hassio/supervisor"; import { - createHassioSession, HassioAddonDetails, fetchHassioAddonInfo, -} from "../../../src/data/hassio"; +} from "../../../src/data/hassio/addon"; import "../../../src/layouts/hass-loading-screen"; import "../../../src/layouts/hass-subpage"; @@ -23,7 +23,7 @@ class HassioIngressView extends LitElement { @property() public route!: Route; @property() private _addon?: HassioAddonDetails; - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this._addon) { return html` diff --git a/hassio/src/resources/hassio-style.js b/hassio/src/resources/hassio-style.ts similarity index 100% rename from hassio/src/resources/hassio-style.js rename to hassio/src/resources/hassio-style.ts diff --git a/hassio/src/snapshots/hassio-snapshots.ts b/hassio/src/snapshots/hassio-snapshots.ts index 73cc92bd5c..db8d047cd7 100644 --- a/hassio/src/snapshots/hassio-snapshots.ts +++ b/hassio/src/snapshots/hassio-snapshots.ts @@ -17,19 +17,20 @@ import "@polymer/paper-radio-group/paper-radio-group"; import "../components/hassio-card-content"; import { hassioStyle } from "../resources/hassio-style"; +import { haStyle } from "../../../src/resources/styles"; import { showHassioSnapshotDialog } from "../dialogs/snapshot/show-dialog-hassio-snapshot"; import { HomeAssistant } from "../../../src/types"; import { HassioSnapshot, - HassioSupervisorInfo, fetchHassioSnapshots, reloadHassioSnapshots, HassioFullSnapshotCreateParams, HassioPartialSnapshotCreateParams, createHassioFullSnapshot, createHassioPartialSnapshot, -} from "../../../src/data/hassio"; +} from "../../../src/data/hassio/snapshot"; +import { HassioSupervisorInfo } from "../../../src/data/hassio/supervisor"; import { PolymerChangedEvent } from "../../../src/polymer-types"; import { fireEvent } from "../../../src/common/dom/fire_event"; @@ -75,7 +76,7 @@ class HassioSnapshots extends LitElement { await this._updateSnapshots(); } - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html`
@@ -334,6 +335,7 @@ class HassioSnapshots extends LitElement { static get styles(): CSSResultArray { return [ + haStyle, hassioStyle, css` paper-radio-group { diff --git a/hassio/src/system/hassio-host-info.js b/hassio/src/system/hassio-host-info.js deleted file mode 100644 index e64d2a83dc..0000000000 --- a/hassio/src/system/hassio-host-info.js +++ /dev/null @@ -1,201 +0,0 @@ -import "@material/mwc-button"; -import "@polymer/paper-card/paper-card"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -import "../../../src/components/buttons/ha-call-api-button"; -import { EventsMixin } from "../../../src/mixins/events-mixin"; - -import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown"; - -class HassioHostInfo extends EventsMixin(PolymerElement) { - static get template() { - return html` - - -
-

Host system

- - - - - - - - - - - - -
Hostname[[data.hostname]]
System[[data.operating_system]]
- - Hardware - - - -
-
- - - - -
-
- `; - } - - static get properties() { - return { - hass: Object, - data: Object, - hassOsInfo: Object, - errors: String, - }; - } - - ready() { - super.ready(); - this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev)); - } - - apiCalled(ev) { - if (ev.detail.success) { - this.errors = null; - return; - } - - var response = ev.detail.response; - - if (typeof response.body === "object") { - this.errors = response.body.message || "Unknown error"; - } else { - this.errors = response.body; - } - } - - _computeUpdateAvailable(data) { - return data && data.version !== data.version_latest; - } - - _featureAvailable(data, feature) { - return data && data.features && data.features.includes(feature); - } - - _showHardware() { - this.hass - .callApi("get", "hassio/hardware/info") - .then( - (resp) => this._objectToMarkdown(resp.data), - () => "Error getting hardware info" - ) - .then((content) => { - showHassioMarkdownDialog(this, { - title: "Hardware", - content: content, - }); - }); - } - - _objectToMarkdown(obj, indent = "") { - 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}`); - } - } - }); - return data; - } - - _changeHostnameClicked() { - const curHostname = this.data.hostname; - const hostname = prompt("Please enter a new hostname:", curHostname); - if (hostname && hostname !== curHostname) { - this.hass.callApi("post", "hassio/host/options", { hostname }); - } - } -} - -customElements.define("hassio-host-info", HassioHostInfo); diff --git a/hassio/src/system/hassio-host-info.ts b/hassio/src/system/hassio-host-info.ts new file mode 100644 index 0000000000..b02dbf1b92 --- /dev/null +++ b/hassio/src/system/hassio-host-info.ts @@ -0,0 +1,239 @@ +import "@material/mwc-button"; +import "@polymer/paper-card/paper-card"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; + +import { hassioStyle } from "../resources/hassio-style"; +import { haStyle } from "../../../src/resources/styles"; +import { + HassioHostInfo as HassioHostInfoType, + HassioHassOSInfo, +} from "../../../src/data/hassio/host"; +import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware"; +import { HomeAssistant } from "../../../src/types"; +import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown"; + +import "../../../src/components/buttons/ha-call-api-button"; + +@customElement("hassio-host-info") +class HassioHostInfo extends LitElement { + @property() public hass!: HomeAssistant; + @property() public hostInfo!: HassioHostInfoType; + @property() public hassOsInfo!: HassioHassOSInfo; + @property() private _errors?: string; + + public render(): TemplateResult | void { + return html` + +
+

Host system

+ + + + + + + + + + + ${this.hostInfo.deployment + ? html` + + + + + ` + : ""} + +
Hostname${this.hostInfo.hostname}
System${this.hostInfo.operating_system}
Deployment${this.hostInfo.deployment}
+ + Hardware + + ${this.hostInfo.features.includes("hostname") + ? html` + + Change hostname + + ` + : ""} + ${this._errors + ? html` +
Error: ${this._errors}
+ ` + : ""} +
+
+ ${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 + ` + : ""} +
+
+ `; + } + + static get styles(): CSSResult[] { + return [ + haStyle, + hassioStyle, + css` + paper-card { + display: inline-block; + width: 400px; + margin-left: 8px; + } + .card-content { + height: 200px; + color: var(--primary-text-color); + } + @media screen and (max-width: 830px) { + paper-card { + margin-top: 8px; + margin-left: 0; + width: 100%; + } + .card-content { + height: auto; + } + } + .info { + width: 100%; + } + .info td:nth-child(2) { + text-align: right; + } + .errors { + color: var(--google-red-500); + margin-top: 16px; + } + mwc-button.info { + max-width: calc(50% - 12px); + } + table.info { + margin-bottom: 10px; + } + .warning { + --mdc-theme-primary: var(--google-red-500); + } + `, + ]; + } + + protected firstUpdated(): void { + this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev)); + } + + 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 _showHardware(): Promise { + try { + const content = this._objectToMarkdown( + await fetchHassioHardwareInfo(this.hass) + ); + showHassioMarkdownDialog(this, { + title: "Hardware", + content, + }); + } catch (err) { + showHassioMarkdownDialog(this, { + title: "Hardware", + content: "Error getting hardware info", + }); + } + } + + 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}`); + } + } + }); + + return data; + } + + private _changeHostnameClicked(): void { + const curHostname = this.hostInfo.hostname; + const hostname = prompt("Please enter a new hostname:", curHostname); + if (hostname && hostname !== curHostname) { + this.hass.callApi("POST", "hassio/host/options", { hostname }); + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "hassio-host-info": HassioHostInfo; + } +} diff --git a/hassio/src/system/hassio-supervisor-info.js b/hassio/src/system/hassio-supervisor-info.js deleted file mode 100644 index b2a87b3dd4..0000000000 --- a/hassio/src/system/hassio-supervisor-info.js +++ /dev/null @@ -1,175 +0,0 @@ -import "@material/mwc-button"; -import "@polymer/paper-card/paper-card"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -import "../../../src/components/buttons/ha-call-api-button"; -import { EventsMixin } from "../../../src/mixins/events-mixin"; - -class HassioSupervisorInfo extends EventsMixin(PolymerElement) { - static get template() { - return html` - - -
-

Hass.io supervisor

- - - - - - - - - - - - -
Version[[data.version]]
Latest version[[data.last_version]]
- -
-
- Reload - - - -
-
- `; - } - - static get properties() { - return { - hass: Object, - data: Object, - errors: String, - leaveBeta: { - type: Object, - value: { channel: "stable" }, - }, - }; - } - - ready() { - super.ready(); - this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev)); - } - - apiCalled(ev) { - if (ev.detail.success) { - this.errors = null; - return; - } - - var response = ev.detail.response; - - if (typeof response.body === "object") { - this.errors = response.body.message || "Unknown error"; - } else { - this.errors = response.body; - } - } - - computeUpdateAvailable(data) { - return data.version !== data.last_version; - } - - _equals(a, b) { - return a === b; - } - - _joinBeta() { - if ( - !confirm(`WARNING: -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 inludes beta releases for: -- Home Assistant (Release Candidates) -- Hass.io supervisor -- Host system`) - ) { - return; - } - const method = "post"; - const path = "hassio/supervisor/options"; - const data = { channel: "beta" }; - - const eventData = { - method: method, - path: path, - data: data, - }; - - this.hass - .callApi(method, path, data) - .then( - (resp) => { - eventData.success = true; - eventData.response = resp; - }, - (resp) => { - eventData.success = false; - eventData.response = resp; - } - ) - .then(() => { - this.fire("hass-api-called", eventData); - }); - } -} - -customElements.define("hassio-supervisor-info", HassioSupervisorInfo); diff --git a/hassio/src/system/hassio-supervisor-info.ts b/hassio/src/system/hassio-supervisor-info.ts new file mode 100644 index 0000000000..91854c0952 --- /dev/null +++ b/hassio/src/system/hassio-supervisor-info.ts @@ -0,0 +1,184 @@ +import "@material/mwc-button"; +import "@polymer/paper-card/paper-card"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; + +import { fireEvent } from "../../../src/common/dom/fire_event"; +import { + HassioSupervisorInfo as HassioSupervisorInfoType, + setSupervisorOption, + SupervisorOptions, +} from "../../../src/data/hassio/supervisor"; +import { HomeAssistant } from "../../../src/types"; +import { hassioStyle } from "../resources/hassio-style"; +import { haStyle } from "../../../src/resources/styles"; + +import "../../../src/components/buttons/ha-call-api-button"; + +@customElement("hassio-supervisor-info") +class HassioSupervisorInfo extends LitElement { + @property() public hass!: HomeAssistant; + @property() public supervisorInfo!: HassioSupervisorInfoType; + @property() private _errors?: string; + + public render(): TemplateResult | void { + return html` + +
+

Hass.io supervisor

+ + + + + + + + + + + ${this.supervisorInfo.channel !== "stable" + ? html` + + + + + ` + : ""} + +
Version${this.supervisorInfo.version}
Latest version${this.supervisorInfo.last_version}
Channel${this.supervisorInfo.channel}
+ ${this._errors + ? html` +
Error: ${this._errors}
+ ` + : ""} +
+
+ Reload + ${this.supervisorInfo.version !== this.supervisorInfo.last_version + ? html` + Update + ` + : ""} + ${this.supervisorInfo.channel === "beta" + ? html` + Leave beta channel + ` + : ""} + ${this.supervisorInfo.channel === "stable" + ? html` + Join beta channel + ` + : ""} +
+
+ `; + } + + static get styles(): CSSResult[] { + return [ + haStyle, + hassioStyle, + css` + paper-card { + display: inline-block; + width: 400px; + } + .card-content { + height: 200px; + color: var(--primary-text-color); + } + @media screen and (max-width: 830px) { + paper-card { + width: 100%; + } + .card-content { + height: auto; + } + } + .info { + width: 100%; + } + .info td:nth-child(2) { + text-align: right; + } + .errors { + color: var(--google-red-500); + margin-top: 16px; + } + `, + ]; + } + + protected firstUpdated(): void { + this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev)); + } + + 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() { + if ( + !confirm(`WARNING: +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 inludes beta releases for: +- Home Assistant (Release Candidates) +- Hass.io supervisor +- Host system`) + ) { + return; + } + try { + const data: SupervisorOptions = { channel: "beta" }; + await setSupervisorOption(this.hass, data); + const eventdata = { + success: true, + response: undefined, + path: "option", + }; + fireEvent(this, "hass-api-called", eventdata); + } catch (err) { + this._errors = `Error joining beta channel, ${err.body?.message || err}`; + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "hassio-supervisor-info": HassioSupervisorInfo; + } +} diff --git a/hassio/src/system/hassio-supervisor-log.js b/hassio/src/system/hassio-supervisor-log.js deleted file mode 100644 index e9b48c2e48..0000000000 --- a/hassio/src/system/hassio-supervisor-log.js +++ /dev/null @@ -1,64 +0,0 @@ -import "@material/mwc-button"; -import "@polymer/paper-card/paper-card"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html"; - -class HassioSupervisorLog extends PolymerElement { - static get template() { - return html` - - ${ANSI_HTML_STYLE} - -
-
- Refresh -
-
- `; - } - - static get properties() { - return { - hass: Object, - }; - } - - ready() { - super.ready(); - this.loadData(); - } - - loadData() { - this.hass.callApi("get", "hassio/supervisor/logs").then( - (text) => { - while (this.$.content.lastChild) { - this.$.content.removeChild(this.$.content.lastChild); - } - this.$.content.appendChild(parseTextToColoredPre(text)); - }, - () => { - this.$.content.innerHTML = - 'Error fetching logs'; - } - ); - } - - refresh() { - this.loadData(); - } -} - -customElements.define("hassio-supervisor-log", HassioSupervisorLog); diff --git a/hassio/src/system/hassio-supervisor-log.ts b/hassio/src/system/hassio-supervisor-log.ts new file mode 100644 index 0000000000..9524aac54c --- /dev/null +++ b/hassio/src/system/hassio-supervisor-log.ts @@ -0,0 +1,87 @@ +import "@material/mwc-button"; +import "@polymer/paper-card/paper-card"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, + query, +} from "lit-element"; + +import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html"; +import { hassioStyle } from "../resources/hassio-style"; +import { haStyle } from "../../../src/resources/styles"; +import { HomeAssistant } from "../../../src/types"; +import { fetchSupervisorLogs } from "../../../src/data/hassio/supervisor"; + +@customElement("hassio-supervisor-log") +class HassioSupervisorLog extends LitElement { + @property() public hass!: HomeAssistant; + @property() private _error?: string; + @query("#content") private _logContent!: HTMLDivElement; + + public async connectedCallback(): Promise { + super.connectedCallback(); + await this._loadData(); + } + + public render(): TemplateResult | void { + return html` + + ${this._error + ? html` +
${this._error}
+ ` + : ""} +
+
+ Refresh +
+
+ `; + } + + static get styles(): CSSResult[] { + return [ + haStyle, + hassioStyle, + ANSI_HTML_STYLE, + css` + pre { + white-space: pre-wrap; + } + .errors { + color: var(--google-red-500); + margin-bottom: 16px; + } + `, + ]; + } + + private async _loadData(): Promise { + this._error = undefined; + try { + const content = await fetchSupervisorLogs(this.hass); + while (this._logContent.lastChild) { + this._logContent.removeChild(this._logContent.lastChild as Node); + } + this._logContent.appendChild(parseTextToColoredPre(content)); + } catch (err) { + this._error = `Failed to get supervisor logs, ${err.body?.message || + err}`; + } + } + + private async _refresh(): Promise { + await this._loadData(); + } +} + +declare global { + interface HTMLElementTagNameMap { + "hassio-supervisor-log": HassioSupervisorLog; + } +} diff --git a/hassio/src/system/hassio-system.js b/hassio/src/system/hassio-system.js deleted file mode 100644 index a4d67b56ba..0000000000 --- a/hassio/src/system/hassio-system.js +++ /dev/null @@ -1,51 +0,0 @@ -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -import "./hassio-host-info"; -import "./hassio-supervisor-info"; -import "./hassio-supervisor-log"; - -class HassioSystem extends PolymerElement { - static get template() { - return html` - -
-
Information
- - -
System log
- -
- `; - } - - static get properties() { - return { - hass: Object, - supervisorInfo: Object, - hostInfo: Object, - hassOsInfo: Object, - }; - } -} - -customElements.define("hassio-system", HassioSystem); diff --git a/hassio/src/system/hassio-system.ts b/hassio/src/system/hassio-system.ts new file mode 100644 index 0000000000..5fbff5de08 --- /dev/null +++ b/hassio/src/system/hassio-system.ts @@ -0,0 +1,76 @@ +import "@polymer/paper-menu-button/paper-menu-button"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; + +import { hassioStyle } from "../resources/hassio-style"; +import { haStyle } from "../../../src/resources/styles"; +import { + HassioHostInfo, + HassioHassOSInfo, +} from "../../../src/data/hassio/host"; +import { HassioSupervisorInfo } from "../../../src/data/hassio/supervisor"; +import { HomeAssistant } from "../../../src/types"; + +import "./hassio-host-info"; +import "./hassio-supervisor-info"; +import "./hassio-supervisor-log"; + +@customElement("hassio-system") +class HassioSystem extends LitElement { + @property() public hass!: HomeAssistant; + @property() public supervisorInfo!: HassioSupervisorInfo; + @property() public hostInfo!: HassioHostInfo; + @property() public hassOsInfo!: HassioHassOSInfo; + + public render(): TemplateResult | void { + return html` +
+
Information
+ + +
System log
+ +
+ `; + } + + static get styles(): CSSResult[] { + return [ + haStyle, + hassioStyle, + css` + .content { + margin: 4px; + color: var(--primary-text-color); + } + .title { + margin-top: 24px; + color: var(--primary-text-color); + font-size: 2em; + padding-left: 8px; + margin-bottom: 8px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hassio-system": HassioSystem; + } +} diff --git a/package.json b/package.json index 17670b2b1b..80a981ee46 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "scripts": { "build": "script/build_frontend", "lint": "eslint src hassio/src gallery/src && tslint 'src/**/*.ts' 'hassio/src/**/*.ts' 'gallery/src/**/*.ts' 'cast/src/**/*.ts' 'test-mocha/**/*.ts' && tsc", + "lint-hassio": "eslint hassio/src && tslint 'hassio/src/**/*.ts'", "mocha": "node_modules/.bin/ts-mocha -p test-mocha/tsconfig.test.json --opts test-mocha/mocha.opts", "test": "npm run lint && npm run mocha", "docker_build": "sh ./script/docker_run.sh build $npm_package_version", @@ -26,7 +27,7 @@ "@material/mwc-fab": "^0.10.0", "@material/mwc-ripple": "^0.10.0", "@material/mwc-switch": "^0.10.0", - "@mdi/svg": "4.7.95", + "@mdi/svg": "4.8.95", "@polymer/app-layout": "^3.0.2", "@polymer/app-localize-behavior": "^3.0.1", "@polymer/app-route": "^3.0.2", @@ -87,8 +88,10 @@ "intl-messageformat": "^2.2.0", "js-yaml": "^3.13.1", "leaflet": "^1.4.0", + "leaflet-draw": "^1.0.4", "lit-element": "^2.2.1", "lit-html": "^1.1.0", + "lit-virtualizer": "^0.4.2", "marked": "^0.6.1", "mdn-polyfills": "^5.16.0", "memoize-one": "^5.0.2", @@ -122,6 +125,7 @@ "@types/hls.js": "^0.12.3", "@types/js-yaml": "^3.12.1", "@types/leaflet": "^1.4.3", + "@types/leaflet-draw": "^1.0.1", "@types/memoize-one": "4.1.0", "@types/mocha": "^5.2.6", "@types/webspeechapi": "^0.0.29", diff --git a/setup.py b/setup.py index 70980f89aa..274e7bceda 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20200108.2", + version="20200129.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors", diff --git a/src/auth/ha-authorize.ts b/src/auth/ha-authorize.ts index e026a69bf0..bf8feb2685 100644 --- a/src/auth/ha-authorize.ts +++ b/src/auth/ha-authorize.ts @@ -91,7 +91,6 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) { .redirectUri="${this.redirectUri}" .oauth2State="${this.oauth2State}" .authProvider="${this._authProvider}" - .step="{{step}}" > ${inactiveProviders.length > 0 diff --git a/src/cards/ha-badges-card.ts b/src/cards/ha-badges-card.ts index 7e12681456..33e54d9e50 100644 --- a/src/cards/ha-badges-card.ts +++ b/src/cards/ha-badges-card.ts @@ -12,7 +12,7 @@ class HaBadgesCard extends LitElement { @property() public hass?: HomeAssistant; @property() public states?: HassEntity[]; - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this.hass || !this.states) { return html``; } diff --git a/src/common/dom/setup-leaflet-map.ts b/src/common/dom/setup-leaflet-map.ts index abf9008295..f29b5e257b 100644 --- a/src/common/dom/setup-leaflet-map.ts +++ b/src/common/dom/setup-leaflet-map.ts @@ -2,10 +2,12 @@ import { Map } from "leaflet"; // Sets up a Leaflet map on the provided DOM element export type LeafletModuleType = typeof import("leaflet"); +export type LeafletDrawModuleType = typeof import("leaflet-draw"); export const setupLeafletMap = async ( mapElement: HTMLElement, - darkMode = false + darkMode = false, + draw = false ): Promise<[Map, LeafletModuleType]> => { if (!mapElement.parentNode) { throw new Error("Cannot setup Leaflet map on disconnected element"); @@ -16,6 +18,10 @@ export const setupLeafletMap = async ( )) as LeafletModuleType; Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/"; + if (draw) { + await import(/* webpackChunkName: "leaflet-draw" */ "leaflet-draw"); + } + const map = Leaflet.map(mapElement); const style = document.createElement("link"); style.setAttribute("href", "/static/images/leaflet/leaflet.css"); diff --git a/src/common/entity/compute_active_state.ts b/src/common/entity/compute_active_state.ts new file mode 100644 index 0000000000..db69f04f0e --- /dev/null +++ b/src/common/entity/compute_active_state.ts @@ -0,0 +1,12 @@ +import { HassEntity } from "home-assistant-js-websocket"; + +export const computeActiveState = (stateObj: HassEntity): string => { + const domain = stateObj.entity_id.split(".")[0]; + let state = stateObj.state; + + if (domain === "climate") { + state = stateObj.attributes.hvac_action; + } + + return state; +}; diff --git a/src/common/entity/domain_icon.ts b/src/common/entity/domain_icon.ts index 0bb2535a77..9d36310a2e 100644 --- a/src/common/entity/domain_icon.ts +++ b/src/common/entity/domain_icon.ts @@ -8,7 +8,7 @@ import { DEFAULT_DOMAIN_ICON } from "../const"; const fixedIcons = { alert: "hass:alert", alexa: "hass:amazon-alexa", - automation: "hass:playlist-play", + automation: "hass:robot", calendar: "hass:calendar", camera: "hass:video", climate: "hass:thermostat", @@ -36,8 +36,8 @@ const fixedIcons = { plant: "hass:flower", proximity: "hass:apple-safari", remote: "hass:remote", - scene: "hass:google-pages", - script: "hass:file-document", + scene: "hass:palette", + script: "hass:script-text", sensor: "hass:eye", simple_alarm: "hass:bell", sun: "hass:white-balance-sunny", @@ -48,7 +48,7 @@ const fixedIcons = { water_heater: "hass:thermometer", weather: "hass:weather-cloudy", weblink: "hass:open-in-new", - zone: "hass:map-marker", + zone: "hass:map-marker-radius", }; export const domainIcon = (domain: string, state?: string): string => { diff --git a/src/common/location/add_distance_to_coord.ts b/src/common/location/add_distance_to_coord.ts new file mode 100644 index 0000000000..807a9334a4 --- /dev/null +++ b/src/common/location/add_distance_to_coord.ts @@ -0,0 +1,12 @@ +export const addDistanceToCoord = ( + location: [number, number], + dx: number, + dy: number +): [number, number] => { + const rEarth = 6378000; + const newLatitude = location[0] + (dy / rEarth) * (180 / Math.PI); + const newLongitude = + location[1] + + ((dx / rEarth) * (180 / Math.PI)) / Math.cos((location[0] * Math.PI) / 180); + return [newLatitude, newLongitude]; +}; diff --git a/src/common/search/search-input.ts b/src/common/search/search-input.ts index 2c3e772c42..b1654a3fe6 100644 --- a/src/common/search/search-input.ts +++ b/src/common/search/search-input.ts @@ -20,7 +20,7 @@ class SearchInput extends LitElement { this.shadowRoot!.querySelector("paper-input")!.focus(); } - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html`
`; diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index 7827938381..70408e77bb 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -255,7 +255,9 @@ export class HaDataTable extends BaseElement { diff --git a/src/components/device/ha-area-devices-picker.ts b/src/components/device/ha-area-devices-picker.ts index c3a2396fa6..c6195d7b93 100644 --- a/src/components/device/ha-area-devices-picker.ts +++ b/src/components/device/ha-area-devices-picker.ts @@ -239,6 +239,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) { } protected updated(changedProps: PropertyValues) { + super.updated(changedProps); if (changedProps.has("area") && this.area) { this._areaPicker = true; this.value = this.area; @@ -254,9 +255,9 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) { } } - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this._devices || !this._areas || !this._entities) { - return; + return html``; } const areas = this._getDevices( this._devices, diff --git a/src/components/device/ha-device-automation-picker.ts b/src/components/device/ha-device-automation-picker.ts index 34eacfaa25..e7237d106e 100644 --- a/src/components/device/ha-device-automation-picker.ts +++ b/src/components/device/ha-device-automation-picker.ts @@ -83,7 +83,7 @@ export abstract class HaDeviceAutomationPicker< return `${this._automations[idx].device_id}_${idx}`; } - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (this._renderEmpty) { return html``; } diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts index d7e0c15073..4463bb7b80 100644 --- a/src/components/device/ha-device-picker.ts +++ b/src/components/device/ha-device-picker.ts @@ -203,9 +203,9 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { ]; } - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this.devices || !this.areas || !this.entities) { - return; + return html``; } const devices = this._getDevices( this.devices, diff --git a/src/components/device/ha-devices-picker.ts b/src/components/device/ha-devices-picker.ts index 638b29e27d..1902a97c64 100644 --- a/src/components/device/ha-devices-picker.ts +++ b/src/components/device/ha-devices-picker.ts @@ -37,9 +37,9 @@ class HaDevicesPicker extends LitElement { public pickedDeviceLabel?: string; @property({ attribute: "pick-device-label" }) public pickDeviceLabel?: string; - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this.hass) { - return; + return html``; } const currentDevices = this._currentDevices; diff --git a/src/components/entity/ha-entities-picker.ts b/src/components/entity/ha-entities-picker.ts index 48cf365207..05fd062eb5 100644 --- a/src/components/entity/ha-entities-picker.ts +++ b/src/components/entity/ha-entities-picker.ts @@ -40,9 +40,9 @@ class HaEntitiesPickerLight extends LitElement { public pickedEntityLabel?: string; @property({ attribute: "pick-entity-label" }) public pickEntityLabel?: string; - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this.hass) { - return; + return html``; } const currentEntities = this._currentEntities; diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts index 84dec91a87..189d3daa9a 100644 --- a/src/components/entity/ha-entity-picker.ts +++ b/src/components/entity/ha-entity-picker.ts @@ -145,7 +145,7 @@ class HaEntityPicker extends LitElement { } } - protected render(): TemplateResult | void { + protected render(): TemplateResult { const states = this._getStates( this._hass, this.includeDomains, diff --git a/src/components/entity/ha-entity-toggle.ts b/src/components/entity/ha-entity-toggle.ts index bdbbdea1c2..6c5f757724 100644 --- a/src/components/entity/ha-entity-toggle.ts +++ b/src/components/entity/ha-entity-toggle.ts @@ -28,7 +28,7 @@ class HaEntityToggle extends LitElement { @property() public stateObj?: HassEntity; @property() private _isOn: boolean = false; - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this.stateObj) { return html` diff --git a/src/components/entity/ha-state-label-badge.ts b/src/components/entity/ha-state-label-badge.ts index 33f7e1e5c1..ce8a4c6344 100644 --- a/src/components/entity/ha-state-label-badge.ts +++ b/src/components/entity/ha-state-label-badge.ts @@ -52,7 +52,7 @@ export class HaStateLabelBadge extends LitElement { this.clearInterval(); } - protected render(): TemplateResult | void { + protected render(): TemplateResult { const state = this.state; if (!state) { diff --git a/src/components/entity/state-badge.ts b/src/components/entity/state-badge.ts index f1ca755c4f..172c56261b 100644 --- a/src/components/entity/state-badge.ts +++ b/src/components/entity/state-badge.ts @@ -16,26 +16,36 @@ import { HassEntity } from "home-assistant-js-websocket"; // tslint:disable-next-line import { HaIcon } from "../ha-icon"; import { HomeAssistant } from "../../types"; +import { computeActiveState } from "../../common/entity/compute_active_state"; +import { ifDefined } from "lit-html/directives/if-defined"; +import { iconColorCSS } from "../../common/style/icon_color_css"; -class StateBadge extends LitElement { +export class StateBadge extends LitElement { public hass?: HomeAssistant; @property() public stateObj?: HassEntity; @property() public overrideIcon?: string; @property() public overrideImage?: string; + @property({ type: Boolean }) public stateColor?: boolean; @query("ha-icon") private _icon!: HaIcon; - protected render(): TemplateResult | void { + protected render(): TemplateResult { const stateObj = this.stateObj; if (!stateObj) { return html``; } + const domain = computeStateDomain(stateObj); + return html` `; @@ -67,14 +77,14 @@ class StateBadge extends LitElement { hostStyle.backgroundImage = `url(${imageUrl})`; iconStyle.display = "none"; } else { - if (stateObj.attributes.hs_color) { + if (stateObj.attributes.hs_color && this.stateColor !== false) { const hue = stateObj.attributes.hs_color[0]; const sat = stateObj.attributes.hs_color[1]; if (sat > 10) { iconStyle.color = `hsl(${hue}, 100%, ${100 - sat / 2}%)`; } } - if (stateObj.attributes.brightness) { + if (stateObj.attributes.brightness && this.stateColor !== false) { const brightness = stateObj.attributes.brightness; if (typeof brightness !== "number") { const errorMessage = `Type error: state-badge expected number, but type of ${ @@ -111,19 +121,7 @@ class StateBadge extends LitElement { transition: color 0.3s ease-in-out, filter 0.3s ease-in-out; } - /* Color the icon if light or sun is on */ - ha-icon[data-domain="light"][data-state="on"], - ha-icon[data-domain="switch"][data-state="on"], - ha-icon[data-domain="binary_sensor"][data-state="on"], - ha-icon[data-domain="fan"][data-state="on"], - ha-icon[data-domain="sun"][data-state="above_horizon"] { - color: var(--paper-item-icon-active-color, #fdd835); - } - - /* Color the icon if unavailable */ - ha-icon[data-state="unavailable"] { - color: var(--state-icon-unavailable-color); - } + ${iconColorCSS} `; } } diff --git a/src/components/ha-area-picker.ts b/src/components/ha-area-picker.ts new file mode 100644 index 0000000000..5e7e7f992d --- /dev/null +++ b/src/components/ha-area-picker.ts @@ -0,0 +1,229 @@ +import "@polymer/paper-input/paper-input"; +import "@polymer/paper-item/paper-item"; +import "@polymer/paper-item/paper-item-body"; +import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light"; +import "@polymer/paper-listbox/paper-listbox"; +import { + LitElement, + TemplateResult, + html, + css, + CSSResult, + customElement, + property, +} from "lit-element"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { SubscribeMixin } from "../mixins/subscribe-mixin"; + +import { HomeAssistant } from "../types"; +import { fireEvent } from "../common/dom/fire_event"; +import { PolymerChangedEvent } from "../polymer-types"; +import { + AreaRegistryEntry, + subscribeAreaRegistry, + createAreaRegistryEntry, +} from "../data/area_registry"; +import { + showPromptDialog, + showAlertDialog, +} from "../dialogs/generic/show-dialog-box"; + +const rowRenderer = ( + root: HTMLElement, + _owner, + model: { item: AreaRegistryEntry } +) => { + if (!root.firstElementChild) { + root.innerHTML = ` + + + +
[[item.name]]
+
+
+ `; + } + root.querySelector(".name")!.textContent = model.item.name!; + if (model.item.area_id === "add_new") { + root.querySelector("paper-item")!.className = "add-new"; + } else { + root.querySelector("paper-item")!.classList.remove("add-new"); + } +}; + +@customElement("ha-area-picker") +export class HaAreaPicker extends SubscribeMixin(LitElement) { + @property() public hass!: HomeAssistant; + @property() public label?: string; + @property() public value?: string; + @property() public _areas?: AreaRegistryEntry[]; + @property({ type: Boolean, attribute: "no-add" }) + public noAdd?: boolean; + @property() private _opened?: boolean; + + public hassSubscribe(): UnsubscribeFunc[] { + return [ + subscribeAreaRegistry(this.hass.connection!, (areas) => { + this._areas = this.noAdd + ? areas + : [ + ...areas, + { + area_id: "add_new", + name: this.hass.localize("ui.components.area-picker.add_new"), + }, + ]; + }), + ]; + } + + protected render(): TemplateResult { + if (!this._areas) { + return html``; + } + return html` + + + ${this.value + ? html` + + ${this.hass.localize("ui.components.area-picker.clear")} + + ` + : ""} + ${this._areas.length > 0 + ? html` + + ${this.hass.localize("ui.components.area-picker.toggle")} + + ` + : ""} + + + `; + } + + private _clearValue(ev: Event) { + ev.stopPropagation(); + this._setValue(""); + } + + private get _value() { + return this.value || ""; + } + + private _openedChanged(ev: PolymerChangedEvent) { + this._opened = ev.detail.value; + } + + private _areaChanged(ev: PolymerChangedEvent) { + const newValue = ev.detail.value; + + if (newValue !== "add_new") { + if (newValue !== this._value) { + this._setValue(newValue); + } + return; + } + + (ev.target as any).value = this._value; + showPromptDialog(this, { + title: this.hass.localize("ui.components.area-picker.add_dialog.title"), + text: this.hass.localize("ui.components.area-picker.add_dialog.text"), + confirmText: this.hass.localize( + "ui.components.area-picker.add_dialog.add" + ), + inputLabel: this.hass.localize( + "ui.components.area-picker.add_dialog.name" + ), + confirm: async (name) => { + if (!name) { + return; + } + try { + const area = await createAreaRegistryEntry(this.hass, { + name, + }); + this._areas = [...this._areas!, area]; + this._setValue(area.area_id); + } catch (err) { + showAlertDialog(this, { + text: this.hass.localize( + "ui.components.area-picker.add_dialog.failed_create_area" + ), + }); + } + }, + }); + } + + private _setValue(value: string) { + this.value = value; + setTimeout(() => { + fireEvent(this, "value-changed", { value }); + fireEvent(this, "change"); + }, 0); + } + + static get styles(): CSSResult { + return css` + paper-input > paper-icon-button { + width: 24px; + height: 24px; + padding: 2px; + color: var(--secondary-text-color); + } + [hidden] { + display: none; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-area-picker": HaAreaPicker; + } +} diff --git a/src/components/ha-attributes.js b/src/components/ha-attributes.js deleted file mode 100644 index 1a1836c2f2..0000000000 --- a/src/components/ha-attributes.js +++ /dev/null @@ -1,91 +0,0 @@ -import "@polymer/iron-flex-layout/iron-flex-layout-classes"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -import hassAttributeUtil from "../util/hass-attributes-util"; - -class HaAttributes extends PolymerElement { - static get template() { - return html` - - - -
- -
- [[computeAttribution(stateObj)]] -
-
- `; - } - - static get properties() { - return { - stateObj: { - type: Object, - }, - extraFilters: { - type: String, - value: "", - }, - filtersArray: { - type: Array, - computed: "computeFiltersArray(extraFilters)", - }, - }; - } - - computeFiltersArray(extraFilters) { - return Object.keys(hassAttributeUtil.LOGIC_STATE_ATTRIBUTES).concat( - extraFilters ? extraFilters.split(",") : [] - ); - } - - computeDisplayAttributes(stateObj, filtersArray) { - if (!stateObj) { - return []; - } - return Object.keys(stateObj.attributes).filter(function(key) { - return filtersArray.indexOf(key) === -1; - }); - } - - formatAttribute(attribute) { - return attribute.replace(/_/g, " "); - } - - formatAttributeValue(stateObj, attribute) { - var value = stateObj.attributes[attribute]; - if (value === null) return "-"; - if (Array.isArray(value)) { - return value.join(", "); - } - return value instanceof Object ? JSON.stringify(value, null, 2) : value; - } - - computeAttribution(stateObj) { - return stateObj.attributes.attribution; - } -} - -customElements.define("ha-attributes", HaAttributes); diff --git a/src/components/ha-attributes.ts b/src/components/ha-attributes.ts new file mode 100644 index 0000000000..c0c14faca8 --- /dev/null +++ b/src/components/ha-attributes.ts @@ -0,0 +1,97 @@ +import { + property, + LitElement, + TemplateResult, + html, + CSSResult, + css, + customElement, +} from "lit-element"; +import { HassEntity } from "home-assistant-js-websocket"; + +import hassAttributeUtil from "../util/hass-attributes-util"; + +@customElement("ha-attributes") +class HaAttributes extends LitElement { + @property() public stateObj?: HassEntity; + @property() public extraFilters?: string; + + protected render(): TemplateResult { + if (!this.stateObj) { + return html``; + } + + return html` +
+ ${this.computeDisplayAttributes( + Object.keys(hassAttributeUtil.LOGIC_STATE_ATTRIBUTES).concat( + this.extraFilters ? this.extraFilters.split(",") : [] + ) + ).map( + (attribute) => html` +
+
${attribute.replace(/_/g, " ")}
+
+ ${this.formatAttributeValue(attribute)} +
+
+ ` + )} + ${this.stateObj.attributes.attribution + ? html` +
+ ${this.stateObj.attributes.attribution} +
+ ` + : ""} +
+ `; + } + + static get styles(): CSSResult { + return css` + .data-entry { + display: flex; + flex-direction: row; + justify-content: space-between; + } + .data-entry .value { + max-width: 200px; + overflow-wrap: break-word; + } + .attribution { + color: var(--secondary-text-color); + text-align: right; + } + `; + } + + private computeDisplayAttributes(filtersArray: string[]): string[] { + if (!this.stateObj) { + return []; + } + return Object.keys(this.stateObj.attributes).filter((key) => { + return filtersArray.indexOf(key) === -1; + }); + } + + private formatAttributeValue(attribute: string): string { + if (!this.stateObj) { + return "-"; + } + const value = this.stateObj.attributes[attribute]; + if (value === null) { + return "-"; + } + if (Array.isArray(value)) { + return value.join(", "); + } + return value instanceof Object ? JSON.stringify(value, null, 2) : value; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-attributes": HaAttributes; + } +} diff --git a/src/components/ha-camera-stream.ts b/src/components/ha-camera-stream.ts index 1faea13c46..7b8de2ae0d 100644 --- a/src/components/ha-camera-stream.ts +++ b/src/components/ha-camera-stream.ts @@ -42,7 +42,7 @@ class HaCameraStream extends LitElement { this._attached = false; } - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this.stateObj || !this._attached) { return html``; } diff --git a/src/components/ha-chips.ts b/src/components/ha-chips.ts index 132fb1f99d..df2fe418c2 100644 --- a/src/components/ha-chips.ts +++ b/src/components/ha-chips.ts @@ -47,12 +47,9 @@ export class HaChips extends LitElement { } private _handleClick(ev) { - fireEvent( - this, - "chip-clicked", - { index: ev.target.closest("button").index }, - { bubbles: false } - ); + fireEvent(this, "chip-clicked", { + index: ev.target.closest("button").index, + }); } static get styles(): CSSResult { diff --git a/src/components/ha-dialog.ts b/src/components/ha-dialog.ts new file mode 100644 index 0000000000..ff770e7bb7 --- /dev/null +++ b/src/components/ha-dialog.ts @@ -0,0 +1,31 @@ +import { customElement, CSSResult, css } from "lit-element"; +import "@material/mwc-dialog"; +import { style } from "@material/mwc-dialog/mwc-dialog-css"; +// tslint:disable-next-line +import { Dialog } from "@material/mwc-dialog"; +import { Constructor } from "../types"; +// tslint:disable-next-line +const MwcDialog = customElements.get("mwc-dialog") as Constructor; + +@customElement("ha-dialog") +export class HaDialog extends MwcDialog { + protected static get styles(): CSSResult[] { + return [ + style, + css` + .mdc-dialog__actions { + justify-content: var(--justify-action-buttons, flex-end); + } + .mdc-dialog__container { + align-items: var(--vertial-align-dialog, center); + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-dialog": HaDialog; + } +} diff --git a/src/components/ha-form/ha-form-boolean.ts b/src/components/ha-form/ha-form-boolean.ts index cfcda6c015..cc5355274e 100644 --- a/src/components/ha-form/ha-form-boolean.ts +++ b/src/components/ha-form/ha-form-boolean.ts @@ -51,7 +51,7 @@ export class HaFormBoolean extends LitElement implements HaFormElement { static get styles(): CSSResult { return css` paper-checkbox { - display: inline-block; + display: block; padding: 22px 0; } `; diff --git a/src/components/ha-form/ha-form-positive_time_period_dict.ts b/src/components/ha-form/ha-form-positive_time_period_dict.ts index 8fcc6cbc36..d7ff9ebb59 100644 --- a/src/components/ha-form/ha-form-positive_time_period_dict.ts +++ b/src/components/ha-form/ha-form-positive_time_period_dict.ts @@ -23,7 +23,7 @@ export class HaFormTimePeriod extends LitElement implements HaFormElement { } } - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html`
diff --git a/src/components/ha-menu-button.ts b/src/components/ha-menu-button.ts index 32a60028ef..7f64e34432 100644 --- a/src/components/ha-menu-button.ts +++ b/src/components/ha-menu-button.ts @@ -42,7 +42,7 @@ class HaMenuButton extends LitElement { } } - protected render(): TemplateResult | void { + protected render(): TemplateResult { const hasNotifications = (this.narrow || this.hass.dockedSidebar === "always_hidden") && (this._hasNotifications || @@ -135,7 +135,7 @@ class HaMenuButton extends LitElement { top: 5px; right: 2px; border-radius: 50%; - border: 2px solid var(--primary-color); + border: 2px solid var(--app-header-background-color); } `; } diff --git a/src/components/ha-related-items.ts b/src/components/ha-related-items.ts new file mode 100644 index 0000000000..ab459ce0c4 --- /dev/null +++ b/src/components/ha-related-items.ts @@ -0,0 +1,323 @@ +import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; +import { + customElement, + html, + LitElement, + property, + PropertyValues, + TemplateResult, + CSSResult, + css, +} from "lit-element"; +import { fireEvent } from "../common/dom/fire_event"; +import { + AreaRegistryEntry, + subscribeAreaRegistry, +} from "../data/area_registry"; +import { ConfigEntry, getConfigEntries } from "../data/config_entries"; +import { + DeviceRegistryEntry, + subscribeDeviceRegistry, +} from "../data/device_registry"; +import { SceneEntity } from "../data/scene"; +import { findRelated, ItemType, RelatedResult } from "../data/search"; +import { SubscribeMixin } from "../mixins/subscribe-mixin"; +import { HomeAssistant } from "../types"; +import "./ha-switch"; + +@customElement("ha-related-items") +export class HaRelatedItems extends SubscribeMixin(LitElement) { + @property() public hass!: HomeAssistant; + @property() public itemType!: ItemType; + @property() public itemId!: string; + @property() private _entries?: ConfigEntry[]; + @property() private _devices?: DeviceRegistryEntry[]; + @property() private _areas?: AreaRegistryEntry[]; + @property() private _related?: RelatedResult; + + public hassSubscribe(): UnsubscribeFunc[] { + return [ + subscribeDeviceRegistry(this.hass.connection!, (devices) => { + this._devices = devices; + }), + subscribeAreaRegistry(this.hass.connection!, (areas) => { + this._areas = areas; + }), + ]; + } + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + getConfigEntries(this.hass).then((configEntries) => { + this._entries = configEntries; + }); + } + + protected updated(changedProps: PropertyValues) { + super.updated(changedProps); + if ( + (changedProps.has("itemId") || changedProps.has("itemType")) && + this.itemId && + this.itemType + ) { + this._findRelated(); + } + } + + protected render(): TemplateResult { + if (!this._related) { + return html``; + } + return html` + ${this._related.config_entry && this._entries + ? this._related.config_entry.map((relatedConfigEntryId) => { + const entry: ConfigEntry | undefined = this._entries!.find( + (configEntry) => configEntry.entry_id === relatedConfigEntryId + ); + if (!entry) { + return; + } + return html` +

+ ${this.hass.localize( + "ui.components.related-items.integration" + )}: +

+ + ${this.hass.localize(`component.${entry.domain}.config.title`)}: + ${entry.title} + + `; + }) + : ""} + ${this._related.device && this._devices + ? this._related.device.map((relatedDeviceId) => { + const device: DeviceRegistryEntry | undefined = this._devices!.find( + (dev) => dev.id === relatedDeviceId + ); + if (!device) { + return; + } + return html` +

+ ${this.hass.localize("ui.components.related-items.device")}: +

+ + ${device.name_by_user || device.name} + + `; + }) + : ""} + ${this._related.area && this._areas + ? this._related.area.map((relatedAreaId) => { + const area: AreaRegistryEntry | undefined = this._areas!.find( + (ar) => ar.area_id === relatedAreaId + ); + if (!area) { + return; + } + return html` +

+ ${this.hass.localize("ui.components.related-items.area")}: +

+ ${area.name} + `; + }) + : ""} + ${this._related.entity + ? html` +

+ ${this.hass.localize("ui.components.related-items.entity")}: +

+
    + ${this._related.entity.map((entityId) => { + const entity: HassEntity | undefined = this.hass.states[ + entityId + ]; + if (!entity) { + return; + } + return html` +
  • + +
  • + `; + })} +
+ ` + : ""} + ${this._related.group + ? html` +

${this.hass.localize("ui.components.related-items.group")}:

+
    + ${this._related.group.map((groupId) => { + const group: HassEntity | undefined = this.hass.states[groupId]; + if (!group) { + return; + } + return html` +
  • + +
  • + `; + })} +
+ ` + : ""} + ${this._related.scene + ? html` +

${this.hass.localize("ui.components.related-items.scene")}:

+
    + ${this._related.scene.map((sceneId) => { + const scene: SceneEntity | undefined = this.hass.states[ + sceneId + ]; + if (!scene) { + return; + } + return html` +
  • + +
  • + `; + })} +
+ ` + : ""} + ${this._related.automation + ? html` +

+ ${this.hass.localize("ui.components.related-items.automation")}: +

+
    + ${this._related.automation.map((automationId) => { + const automation: HassEntity | undefined = this.hass.states[ + automationId + ]; + if (!automation) { + return; + } + return html` +
  • + +
  • + `; + })} +
+ ` + : ""} + ${this._related.script + ? html` +

+ ${this.hass.localize("ui.components.related-items.script")}: +

+
    + ${this._related.script.map((scriptId) => { + const script: HassEntity | undefined = this.hass.states[ + scriptId + ]; + if (!script) { + return; + } + return html` +
  • + +
  • + `; + })} +
+ ` + : ""} + `; + } + + private async _findRelated() { + this._related = await findRelated(this.hass, this.itemType, this.itemId); + await this.updateComplete; + fireEvent(this, "iron-resize"); + } + + private _openMoreInfo(ev: CustomEvent) { + const entityId = (ev.target as any).entityId; + fireEvent(this, "hass-more-info", { entityId }); + } + + private _close() { + fireEvent(this, "close-dialog"); + } + + static get styles(): CSSResult { + return css` + a { + color: var(--primary-color); + } + button.link { + color: var(--primary-color); + text-align: left; + cursor: pointer; + background: none; + border-width: initial; + border-style: none; + border-color: initial; + border-image: initial; + padding: 0px; + font: inherit; + text-decoration: underline; + } + h3 { + font-family: var(--paper-font-title_-_font-family); + -webkit-font-smoothing: var( + --paper-font-title_-_-webkit-font-smoothing + ); + font-size: var(--paper-font-title_-_font-size); + font-weight: var(--paper-font-headline-_font-weight); + letter-spacing: var(--paper-font-title_-_letter-spacing); + line-height: var(--paper-font-title_-_line-height); + opacity: var(--dark-primary-opacity); + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-related-items": HaRelatedItems; + } +} diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index ad6682cd37..bad87cdb23 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -520,10 +520,13 @@ class HaSidebar extends LitElement { } a { + text-decoration: none; color: var(--sidebar-text-color); font-weight: 500; font-size: 14px; - text-decoration: none; + position: relative; + display: block; + outline: 0; } paper-icon-item { @@ -546,7 +549,8 @@ class HaSidebar extends LitElement { color: var(--sidebar-icon-color); } - .iron-selected paper-icon-item:before { + .iron-selected paper-icon-item::before, + a:not(.iron-selected):focus::before { border-radius: 4px; position: absolute; top: 0; @@ -555,11 +559,22 @@ class HaSidebar extends LitElement { left: 0; pointer-events: none; content: ""; - background-color: var(--sidebar-selected-icon-color); - opacity: 0.12; transition: opacity 15ms linear; will-change: opacity; } + .iron-selected paper-icon-item::before { + background-color: var(--sidebar-selected-icon-color); + opacity: 0.12; + } + a:not(.iron-selected):focus::before { + background-color: currentColor; + opacity: var(--dark-divider-opacity); + margin: 4px 8px; + } + .iron-selected paper-icon-item:focus::before, + .iron-selected:focus paper-icon-item::before { + opacity: 0.2; + } .iron-selected paper-icon-item[pressed]:before { opacity: 0.37; diff --git a/src/components/ha-switch.ts b/src/components/ha-switch.ts index 5a684d2be7..9426082439 100644 --- a/src/components/ha-switch.ts +++ b/src/components/ha-switch.ts @@ -21,6 +21,7 @@ export class HaSwitch extends MwcSwitch { "slotted", Boolean(this._slot.assignedNodes().length) ); + this._slot.addEventListener("click", () => (this.checked = !this.checked)); } protected static get styles(): CSSResult[] { diff --git a/src/components/map/ha-location-editor.ts b/src/components/map/ha-location-editor.ts index 0d4133b832..f5fc32186f 100644 --- a/src/components/map/ha-location-editor.ts +++ b/src/components/map/ha-location-editor.ts @@ -8,33 +8,49 @@ import { customElement, PropertyValues, } from "lit-element"; -import { Marker, Map, LeafletMouseEvent, DragEndEvent, LatLng } from "leaflet"; +import { + Marker, + Map, + LeafletMouseEvent, + DragEndEvent, + LatLng, + Circle, + DivIcon, +} from "leaflet"; import { setupLeafletMap, LeafletModuleType, } from "../../common/dom/setup-leaflet-map"; import { fireEvent } from "../../common/dom/fire_event"; +import { nextRender } from "../../common/util/render-status"; @customElement("ha-location-editor") class LocationEditor extends LitElement { @property() public location?: [number, number]; + @property() public radius?: number; + @property() public icon?: string; public fitZoom = 16; - + private _iconEl?: DivIcon; private _ignoreFitToMap?: [number, number]; // tslint:disable-next-line private Leaflet?: LeafletModuleType; private _leafletMap?: Map; - private _locationMarker?: Marker; + private _locationMarker?: Marker | Circle; public fitMap(): void { if (!this._leafletMap || !this.location) { return; } - this._leafletMap.setView(this.location, this.fitZoom); + if ((this._locationMarker as Circle).getBounds) { + this._leafletMap.fitBounds((this._locationMarker as Circle).getBounds()); + } else { + this._leafletMap.setView(this.location, this.fitZoom); + } + this._ignoreFitToMap = this.location; } - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html`
`; @@ -53,11 +69,23 @@ class LocationEditor extends LitElement { return; } - this._updateMarker(); - if (!this._ignoreFitToMap || this._ignoreFitToMap !== this.location) { - this.fitMap(); + if (changedProps.has("location")) { + this._updateMarker(); + if ( + this.location && + (!this._ignoreFitToMap || + this._ignoreFitToMap[0] !== this.location[0] || + this._ignoreFitToMap[1] !== this.location[1]) + ) { + this.fitMap(); + } + } + if (changedProps.has("radius")) { + this._updateRadius(); + } + if (changedProps.has("icon")) { + this._updateIcon(); } - this._ignoreFitToMap = undefined; } private get _mapEl(): HTMLDivElement { @@ -65,18 +93,23 @@ class LocationEditor extends LitElement { } private async _initMap(): Promise { - [this._leafletMap, this.Leaflet] = await setupLeafletMap(this._mapEl); + [this._leafletMap, this.Leaflet] = await setupLeafletMap( + this._mapEl, + false, + Boolean(this.radius) + ); this._leafletMap.addEventListener( "click", // @ts-ignore - (ev: LeafletMouseEvent) => this._updateLocation(ev.latlng) + (ev: LeafletMouseEvent) => this._locationUpdated(ev.latlng) ); + this._updateIcon(); this._updateMarker(); this.fitMap(); this._leafletMap.invalidateSize(); } - private _updateLocation(latlng: LatLng) { + private _locationUpdated(latlng: LatLng) { let longitude = latlng.lng; if (Math.abs(longitude) > 180.0) { // Normalize longitude if map provides values beyond -180 to +180 degrees. @@ -86,7 +119,68 @@ class LocationEditor extends LitElement { fireEvent(this, "change", undefined, { bubbles: false }); } - private _updateMarker(): void { + private _radiusUpdated() { + this._ignoreFitToMap = this.location; + this.radius = (this._locationMarker as Circle).getRadius(); + fireEvent(this, "change", undefined, { bubbles: false }); + } + + private _updateIcon() { + if (!this.icon) { + this._iconEl = undefined; + return; + } + + // create icon + let iconHTML = ""; + const el = document.createElement("ha-icon"); + el.setAttribute("icon", this.icon); + iconHTML = el.outerHTML; + + this._iconEl = this.Leaflet!.divIcon({ + html: iconHTML, + iconSize: [24, 24], + className: "light leaflet-edit-move", + }); + this._setIcon(); + } + + private _setIcon() { + if (!this._locationMarker || !this._iconEl) { + return; + } + + if (!this.radius) { + (this._locationMarker as Marker).setIcon(this._iconEl); + return; + } + + // @ts-ignore + const moveMarker = this._locationMarker.editing._moveMarker; + moveMarker.setIcon(this._iconEl); + } + + private _setupEdit() { + // @ts-ignore + this._locationMarker.editing.enable(); + // @ts-ignore + const moveMarker = this._locationMarker.editing._moveMarker; + // @ts-ignore + const resizeMarker = this._locationMarker.editing._resizeMarkers[0]; + this._setIcon(); + moveMarker.addEventListener( + "dragend", + // @ts-ignore + (ev: DragEndEvent) => this._locationUpdated(ev.target.getLatLng()) + ); + resizeMarker.addEventListener( + "dragend", + // @ts-ignore + (ev: DragEndEvent) => this._radiusUpdated(ev) + ); + } + + private async _updateMarker(): Promise { if (!this.location) { if (this._locationMarker) { this._locationMarker.remove(); @@ -97,17 +191,41 @@ class LocationEditor extends LitElement { if (this._locationMarker) { this._locationMarker.setLatLng(this.location); + if (this.radius) { + // @ts-ignore + this._locationMarker.editing.disable(); + await nextRender(); + this._setupEdit(); + } return; } - this._locationMarker = this.Leaflet!.marker(this.location, { - draggable: true, - }); - this._locationMarker.addEventListener( - "dragend", - // @ts-ignore - (ev: DragEndEvent) => this._updateLocation(ev.target.getLatLng()) - ); - this._leafletMap!.addLayer(this._locationMarker); + + if (!this.radius) { + this._locationMarker = this.Leaflet!.marker(this.location, { + draggable: true, + }); + this._setIcon(); + this._locationMarker.addEventListener( + "dragend", + // @ts-ignore + (ev: DragEndEvent) => this._locationUpdated(ev.target.getLatLng()) + ); + this._leafletMap!.addLayer(this._locationMarker); + } else { + this._locationMarker = this.Leaflet!.circle(this.location, { + color: "#FF9800", + radius: this.radius, + }); + this._leafletMap!.addLayer(this._locationMarker); + this._setupEdit(); + } + } + + private _updateRadius(): void { + if (!this._locationMarker || !this.radius) { + return; + } + (this._locationMarker as Circle).setRadius(this.radius); } static get styles(): CSSResult { @@ -119,6 +237,14 @@ class LocationEditor extends LitElement { #map { height: 100%; } + .leaflet-edit-move { + border-radius: 50%; + cursor: move !important; + } + .leaflet-edit-resize { + border-radius: 50%; + cursor: nesw-resize !important; + } `; } } diff --git a/src/components/map/ha-locations-editor.ts b/src/components/map/ha-locations-editor.ts new file mode 100644 index 0000000000..6eee640fa2 --- /dev/null +++ b/src/components/map/ha-locations-editor.ts @@ -0,0 +1,305 @@ +import { + LitElement, + property, + TemplateResult, + html, + CSSResult, + css, + customElement, + PropertyValues, +} from "lit-element"; +import { + Marker, + Map, + DragEndEvent, + LatLng, + Circle, + MarkerOptions, + DivIcon, +} from "leaflet"; +import { + setupLeafletMap, + LeafletModuleType, +} from "../../common/dom/setup-leaflet-map"; +import { fireEvent } from "../../common/dom/fire_event"; + +declare global { + // for fire event + interface HASSDomEvents { + "location-updated": { id: string; location: [number, number] }; + "radius-updated": { id: string; radius: number }; + "marker-clicked": { id: string }; + } +} + +export interface MarkerLocation { + latitude: number; + longitude: number; + radius?: number; + name?: string; + id: string; + icon?: string; + radius_color?: string; + editable?: boolean; +} + +@customElement("ha-locations-editor") +export class HaLocationsEditor extends LitElement { + @property() public locations?: MarkerLocation[]; + public fitZoom = 16; + + // tslint:disable-next-line + private Leaflet?: LeafletModuleType; + // tslint:disable-next-line + private _leafletMap?: Map; + private _locationMarkers?: { [key: string]: Marker | Circle }; + private _circles: { [key: string]: Circle } = {}; + + public fitMap(): void { + if ( + !this._leafletMap || + !this._locationMarkers || + !Object.keys(this._locationMarkers).length + ) { + return; + } + const bounds = this.Leaflet!.latLngBounds( + Object.values(this._locationMarkers).map((item) => item.getLatLng()) + ); + this._leafletMap.fitBounds(bounds.pad(0.5)); + } + + public fitMarker(id: string): void { + if (!this._leafletMap || !this._locationMarkers) { + return; + } + const marker = this._locationMarkers[id]; + if (!marker) { + return; + } + if ((marker as Circle).getBounds) { + this._leafletMap.fitBounds((marker as Circle).getBounds()); + (marker as Circle).bringToFront(); + } else { + const circle = this._circles[id]; + if (circle) { + this._leafletMap.fitBounds(circle.getBounds()); + } else { + this._leafletMap.setView(marker.getLatLng(), this.fitZoom); + } + } + } + + protected render(): TemplateResult { + return html` +
+ `; + } + + protected firstUpdated(changedProps: PropertyValues): void { + super.firstUpdated(changedProps); + this._initMap(); + } + + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + + // Still loading. + if (!this.Leaflet) { + return; + } + + if (changedProps.has("locations")) { + this._updateMarkers(); + } + } + + private get _mapEl(): HTMLDivElement { + return this.shadowRoot!.querySelector("div")!; + } + + private async _initMap(): Promise { + [this._leafletMap, this.Leaflet] = await setupLeafletMap( + this._mapEl, + false, + true + ); + this._updateMarkers(); + this.fitMap(); + this._leafletMap.invalidateSize(); + } + + private _updateLocation(ev: DragEndEvent) { + const marker = ev.target; + const latlng: LatLng = marker.getLatLng(); + let longitude: number = latlng.lng; + if (Math.abs(longitude) > 180.0) { + // Normalize longitude if map provides values beyond -180 to +180 degrees. + longitude = (((longitude % 360.0) + 540.0) % 360.0) - 180.0; + } + const location: [number, number] = [latlng.lat, longitude]; + fireEvent( + this, + "location-updated", + { id: marker.id, location }, + { bubbles: false } + ); + } + + private _updateRadius(ev: DragEndEvent) { + const marker = ev.target; + const circle = this._locationMarkers![marker.id] as Circle; + fireEvent( + this, + "radius-updated", + { id: marker.id, radius: circle.getRadius() }, + { bubbles: false } + ); + } + + private _markerClicked(ev: DragEndEvent) { + const marker = ev.target; + fireEvent(this, "marker-clicked", { id: marker.id }, { bubbles: false }); + } + + private _updateMarkers(): void { + if (this._locationMarkers) { + Object.values(this._locationMarkers).forEach((marker) => { + marker.remove(); + }); + this._locationMarkers = undefined; + + Object.values(this._circles).forEach((circle) => circle.remove()); + this._circles = {}; + } + + if (!this.locations || !this.locations.length) { + return; + } + + this._locationMarkers = {}; + + this.locations.forEach((location: MarkerLocation) => { + let icon: DivIcon | undefined; + if (location.icon) { + // create icon + const el = document.createElement("div"); + el.className = "named-icon"; + if (location.name) { + el.innerText = location.name; + } + const iconEl = document.createElement("ha-icon"); + iconEl.setAttribute("icon", location.icon); + el.prepend(iconEl); + + icon = this.Leaflet!.divIcon({ + html: el.outerHTML, + iconSize: [24, 24], + className: "light", + }); + } + if (location.radius) { + const circle = this.Leaflet!.circle( + [location.latitude, location.longitude], + { + color: location.radius_color ? location.radius_color : "#FF9800", + radius: location.radius, + } + ); + circle.addTo(this._leafletMap!); + if (location.editable) { + // @ts-ignore + circle.editing.enable(); + // @ts-ignore + const moveMarker = circle.editing._moveMarker; + // @ts-ignore + const resizeMarker = circle.editing._resizeMarkers[0]; + if (icon) { + moveMarker.setIcon(icon); + } + resizeMarker.id = moveMarker.id = location.id; + moveMarker + .addEventListener( + "dragend", + // @ts-ignore + (ev: DragEndEvent) => this._updateLocation(ev) + ) + .addEventListener( + "click", + // @ts-ignore + (ev: MouseEvent) => this._markerClicked(ev) + ); + resizeMarker.addEventListener( + "dragend", + // @ts-ignore + (ev: DragEndEvent) => this._updateRadius(ev) + ); + this._locationMarkers![location.id] = circle; + } else { + this._circles[location.id] = circle; + } + } + if (!location.radius || !location.editable) { + const options: MarkerOptions = { + draggable: Boolean(location.editable), + title: location.name, + }; + + if (icon) { + options.icon = icon; + } + + const marker = this.Leaflet!.marker( + [location.latitude, location.longitude], + options + ) + .addEventListener( + "dragend", + // @ts-ignore + (ev: DragEndEvent) => this._updateLocation(ev) + ) + .addEventListener( + "click", + // @ts-ignore + (ev: MouseEvent) => this._markerClicked(ev) + ) + .addTo(this._leafletMap); + marker.id = location.id; + + this._locationMarkers![location.id] = marker; + } + }); + } + + static get styles(): CSSResult { + return css` + :host { + display: block; + height: 300px; + } + #map { + height: 100%; + } + .leaflet-marker-draggable { + cursor: move !important; + } + .leaflet-edit-resize { + border-radius: 50%; + cursor: nesw-resize !important; + } + .named-icon { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + text-align: center; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-locations-editor": HaLocationsEditor; + } +} diff --git a/src/components/user/ha-user-badge.ts b/src/components/user/ha-user-badge.ts index 842b89c4ad..f7685987f6 100644 --- a/src/components/user/ha-user-badge.ts +++ b/src/components/user/ha-user-badge.ts @@ -31,7 +31,7 @@ const computeInitials = (name: string) => { class StateBadge extends LitElement { @property() public user?: User | CurrentUser; - protected render(): TemplateResult | void { + protected render(): TemplateResult { const user = this.user; const initials = user ? computeInitials(user.name) : "?"; return html` diff --git a/src/components/user/ha-user-picker.ts b/src/components/user/ha-user-picker.ts index 817f1fa4c9..b76e8bff7e 100644 --- a/src/components/user/ha-user-picker.ts +++ b/src/components/user/ha-user-picker.ts @@ -34,7 +34,7 @@ class HaUserPicker extends LitElement { .sort((a, b) => compare(a.name, b.name)); }); - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html` ; - -interface HassioResponse { - data: T; - result: "ok"; -} - -interface CreateSessionResponse { - session: string; -} - -export interface HassioAddonInfo { - name: string; - slug: string; - description: string; - repository: "core" | "local" | string; - version: string; - installed: string | undefined; - detached: boolean; - available: boolean; - build: boolean; - url: string | null; - icon: boolean; - logo: boolean; -} - -export interface HassioAddonDetails { - name: string; - slug: string; - description: string; - long_description: null | string; - auto_update: boolean; - url: null | string; - detached: boolean; - available: boolean; - arch: "armhf" | "aarch64" | "i386" | "amd64"; - machine: any; - homeassistant: string; - repository: null | string; - version: null | string; - last_version: string; - state: "none" | "started" | "stopped"; - boot: "auto" | "manual"; - build: boolean; - options: object; - network: null | object; - host_network: boolean; - host_pid: boolean; - host_ipc: boolean; - host_dbus: boolean; - privileged: any; - apparmor: "disable" | "default" | "profile"; - devices: string[]; - auto_uart: boolean; - icon: boolean; - logo: boolean; - changelog: boolean; - hassio_api: boolean; - hassio_role: "default" | "homeassistant" | "manager" | "admin"; - homeassistant_api: boolean; - auth_api: boolean; - full_access: boolean; - protected: boolean; - rating: "1-6"; - stdin: boolean; - webui: null | string; - gpio: boolean; - kernel_modules: boolean; - devicetree: boolean; - docker_api: boolean; - audio: boolean; - audio_input: null | string; - audio_output: null | string; - services_role: string[]; - discovery: string[]; - ip_address: string; - ingress: boolean; - ingress_entry: null | string; - ingress_url: null | string; -} - -export interface HassioAddonRepository { - slug: string; - name: string; - source: string; - url: string; - maintainer: string; -} - -export interface HassioAddonsInfo { - addons: HassioAddonInfo[]; - repositories: HassioAddonRepository[]; -} -export interface HassioHassOSInfo { - version: string; - version_cli: string; - version_latest: string; - version_cli_latest: string; - board: "ova" | "rpi"; -} -export type HassioHomeAssistantInfo = any; -export type HassioSupervisorInfo = any; -export type HassioHostInfo = any; - -export interface HassioSnapshot { - slug: string; - date: string; - name: string; - type: "full" | "partial"; - protected: boolean; -} - -export interface HassioSnapshotDetail extends HassioSnapshot { - size: string; - homeassistant: string; - addons: Array<{ - slug: "ADDON_SLUG"; - name: "NAME"; - version: "INSTALLED_VERSION"; - size: "SIZE_IN_MB"; - }>; - repositories: string[]; - folders: string[]; -} - -export interface HassioFullSnapshotCreateParams { - name: string; - password?: string; -} -export interface HassioPartialSnapshotCreateParams { - name: string; - folders: string[]; - addons: string[]; - password?: string; -} - -const hassioApiResultExtractor = (response: HassioResponse) => - response.data; - -export const createHassioSession = async (hass: HomeAssistant) => { - const response = await hass.callApi>( - "POST", - "hassio/ingress/session" - ); - document.cookie = `ingress_session=${response.data.session};path=/api/hassio_ingress/`; -}; - -export const reloadHassioAddons = (hass: HomeAssistant) => - hass.callApi("POST", `hassio/addons/reload`); - -export const fetchHassioAddonsInfo = (hass: HomeAssistant) => - hass - .callApi>("GET", `hassio/addons`) - .then(hassioApiResultExtractor); - -export const fetchHassioAddonInfo = (hass: HomeAssistant, addon: string) => - hass - .callApi>( - "GET", - `hassio/addons/${addon}/info` - ) - .then(hassioApiResultExtractor); - -export const fetchHassioSupervisorInfo = (hass: HomeAssistant) => - hass - .callApi>( - "GET", - "hassio/supervisor/info" - ) - .then(hassioApiResultExtractor); - -export const fetchHassioHostInfo = (hass: HomeAssistant) => - hass - .callApi>("GET", "hassio/host/info") - .then(hassioApiResultExtractor); - -export const fetchHassioHassOsInfo = (hass: HomeAssistant) => - hass - .callApi>("GET", "hassio/hassos/info") - .then(hassioApiResultExtractor); - -export const fetchHassioHomeAssistantInfo = (hass: HomeAssistant) => - hass - .callApi>( - "GET", - "hassio/homeassistant/info" - ) - .then(hassioApiResultExtractor); - -export const fetchHassioSnapshots = (hass: HomeAssistant) => - hass - .callApi>( - "GET", - "hassio/snapshots" - ) - .then((resp) => resp.data.snapshots); - -export const reloadHassioSnapshots = (hass: HomeAssistant) => - hass.callApi("POST", `hassio/snapshots/reload`); - -export const createHassioFullSnapshot = ( - hass: HomeAssistant, - data: HassioFullSnapshotCreateParams -) => hass.callApi("POST", "hassio/snapshots/new/full", data); - -export const createHassioPartialSnapshot = ( - hass: HomeAssistant, - data: HassioPartialSnapshotCreateParams -) => hass.callApi("POST", "hassio/snapshots/new/partial", data); - -export const fetchHassioSnapshotInfo = ( - hass: HomeAssistant, - snapshot: string -) => - hass - .callApi>( - "GET", - `hassio/snapshots/${snapshot}/info` - ) - .then(hassioApiResultExtractor); diff --git a/src/data/hassio/addon.ts b/src/data/hassio/addon.ts new file mode 100644 index 0000000000..58eb5e47b2 --- /dev/null +++ b/src/data/hassio/addon.ts @@ -0,0 +1,176 @@ +import { HomeAssistant } from "../../types"; +import { HassioResponse, hassioApiResultExtractor } from "./common"; + +export interface HassioAddonInfo { + name: string; + slug: string; + description: string; + repository: "core" | "local" | string; + version: string; + state: "none" | "started" | "stopped"; + installed: string | undefined; + detached: boolean; + available: boolean; + build: boolean; + url: string | null; + icon: boolean; + logo: boolean; +} + +export interface HassioAddonDetails extends HassioAddonInfo { + name: string; + slug: string; + description: string; + long_description: null | string; + auto_update: boolean; + url: null | string; + detached: boolean; + available: boolean; + arch: "armhf" | "aarch64" | "i386" | "amd64"; + machine: any; + homeassistant: string; + last_version: string; + boot: "auto" | "manual"; + build: boolean; + options: object; + network: null | object; + network_description: null | object; + host_network: boolean; + host_pid: boolean; + host_ipc: boolean; + host_dbus: boolean; + privileged: any; + apparmor: "disable" | "default" | "profile"; + devices: string[]; + auto_uart: boolean; + icon: boolean; + logo: boolean; + changelog: boolean; + hassio_api: boolean; + hassio_role: "default" | "homeassistant" | "manager" | "admin"; + homeassistant_api: boolean; + auth_api: boolean; + full_access: boolean; + protected: boolean; + rating: "1-6"; + stdin: boolean; + webui: null | string; + gpio: boolean; + kernel_modules: boolean; + devicetree: boolean; + docker_api: boolean; + audio: boolean; + audio_input: null | string; + audio_output: null | string; + services_role: string[]; + discovery: string[]; + ip_address: string; + ingress: boolean; + ingress_panel: boolean; + ingress_entry: null | string; + ingress_url: null | string; +} + +export interface HassioAddonsInfo { + addons: HassioAddonInfo[]; + repositories: HassioAddonRepository[]; +} + +export interface HassioAddonSetSecurityParams { + protected?: boolean; +} + +export interface HassioAddonRepository { + slug: string; + name: string; + source: string; + url: string; + maintainer: string; +} + +export interface HassioAddonSetOptionParams { + audio_input?: string | null; + audio_output?: string | null; + options?: object | null; + boot?: "auto" | "manual"; + auto_update?: boolean; + ingress_panel?: boolean; + network?: object | null; +} + +export const reloadHassioAddons = async (hass: HomeAssistant) => { + await hass.callApi>("POST", `hassio/addons/reload`); +}; + +export const fetchHassioAddonsInfo = async (hass: HomeAssistant) => { + return hassioApiResultExtractor( + await hass.callApi>("GET", `hassio/addons`) + ); +}; + +export const fetchHassioAddonInfo = async ( + hass: HomeAssistant, + slug: string +) => { + return hassioApiResultExtractor( + await hass.callApi>( + "GET", + `hassio/addons/${slug}/info` + ) + ); +}; + +export const fetchHassioAddonChangelog = async ( + hass: HomeAssistant, + slug: string +) => { + return hass.callApi("GET", `hassio/addons/${slug}/changelog`); +}; + +export const fetchHassioAddonLogs = async ( + hass: HomeAssistant, + slug: string +) => { + return hass.callApi("GET", `hassio/addons/${slug}/logs`); +}; + +export const setHassioAddonOption = async ( + hass: HomeAssistant, + slug: string, + data: HassioAddonSetOptionParams +) => { + await hass.callApi>( + "POST", + `hassio/addons/${slug}/options`, + data + ); +}; + +export const setHassioAddonSecurity = async ( + hass: HomeAssistant, + slug: string, + data: HassioAddonSetSecurityParams +) => { + await hass.callApi>( + "POST", + `hassio/addons/${slug}/security`, + data + ); +}; + +export const installHassioAddon = async (hass: HomeAssistant, slug: string) => { + return hass.callApi>( + "POST", + `hassio/addons/${slug}/install` + ); +}; + +export const uninstallHassioAddon = async ( + hass: HomeAssistant, + slug: string +) => { + await hass.callApi>( + "POST", + `hassio/addons/${slug}/uninstall` + ); +}; diff --git a/src/data/hassio/common.ts b/src/data/hassio/common.ts new file mode 100644 index 0000000000..b96d410c51 --- /dev/null +++ b/src/data/hassio/common.ts @@ -0,0 +1,7 @@ +export interface HassioResponse { + data: T; + result: "ok"; +} + +export const hassioApiResultExtractor = (response: HassioResponse) => + response.data; diff --git a/src/data/hassio/hardware.ts b/src/data/hassio/hardware.ts new file mode 100644 index 0000000000..ee18581f2b --- /dev/null +++ b/src/data/hassio/hardware.ts @@ -0,0 +1,37 @@ +import { HomeAssistant } from "../../types"; +import { HassioResponse, hassioApiResultExtractor } from "./common"; + +export interface HassioHardwareAudioDevice { + device?: string; + name: string; +} + +interface HassioHardwareAudioList { + audio: { input: any; output: any }; +} + +export interface HassioHardwareInfo { + serial: string[]; + input: string[]; + disk: string[]; + gpio: string[]; + audio: object; +} + +export const fetchHassioHardwareAudio = async (hass: HomeAssistant) => { + return hassioApiResultExtractor( + await hass.callApi>( + "GET", + "hassio/hardware/audio" + ) + ); +}; + +export const fetchHassioHardwareInfo = async (hass: HomeAssistant) => { + return hassioApiResultExtractor( + await hass.callApi>( + "GET", + "hassio/hardware/info" + ) + ); +}; diff --git a/src/data/hassio/host.ts b/src/data/hassio/host.ts new file mode 100644 index 0000000000..f058114c27 --- /dev/null +++ b/src/data/hassio/host.ts @@ -0,0 +1,29 @@ +import { HomeAssistant } from "../../types"; +import { HassioResponse, hassioApiResultExtractor } from "./common"; + +export type HassioHostInfo = any; + +export interface HassioHassOSInfo { + version: string; + version_cli: string; + version_latest: string; + version_cli_latest: string; + board: "ova" | "rpi"; +} + +export const fetchHassioHostInfo = async (hass: HomeAssistant) => { + const response = await hass.callApi>( + "GET", + "hassio/host/info" + ); + return hassioApiResultExtractor(response); +}; + +export const fetchHassioHassOsInfo = async (hass: HomeAssistant) => { + return hassioApiResultExtractor( + await hass.callApi>( + "GET", + "hassio/hassos/info" + ) + ); +}; diff --git a/src/data/hassio/snapshot.ts b/src/data/hassio/snapshot.ts new file mode 100644 index 0000000000..1dabe53559 --- /dev/null +++ b/src/data/hassio/snapshot.ts @@ -0,0 +1,81 @@ +import { HomeAssistant } from "../../types"; +import { HassioResponse, hassioApiResultExtractor } from "./common"; + +export interface HassioSnapshot { + slug: string; + date: string; + name: string; + type: "full" | "partial"; + protected: boolean; +} + +export interface HassioSnapshotDetail extends HassioSnapshot { + size: number; + homeassistant: string; + addons: Array<{ + slug: "ADDON_SLUG"; + name: "NAME"; + version: "INSTALLED_VERSION"; + size: "SIZE_IN_MB"; + }>; + repositories: string[]; + folders: string[]; +} + +export interface HassioFullSnapshotCreateParams { + name: string; + password?: string; +} +export interface HassioPartialSnapshotCreateParams { + name: string; + folders: string[]; + addons: string[]; + password?: string; +} + +export const fetchHassioSnapshots = async (hass: HomeAssistant) => { + return hassioApiResultExtractor( + await hass.callApi>( + "GET", + "hassio/snapshots" + ) + ).snapshots; +}; + +export const fetchHassioSnapshotInfo = async ( + hass: HomeAssistant, + snapshot: string +) => { + return hassioApiResultExtractor( + await hass.callApi>( + "GET", + `hassio/snapshots/${snapshot}/info` + ) + ); +}; + +export const reloadHassioSnapshots = async (hass: HomeAssistant) => { + await hass.callApi>("POST", `hassio/snapshots/reload`); +}; + +export const createHassioFullSnapshot = async ( + hass: HomeAssistant, + data: HassioFullSnapshotCreateParams +) => { + await hass.callApi>( + "POST", + `hassio/snapshots/new/full`, + data + ); +}; + +export const createHassioPartialSnapshot = async ( + hass: HomeAssistant, + data: HassioFullSnapshotCreateParams +) => { + await hass.callApi>( + "POST", + `hassio/snapshots/new/partial`, + data + ); +}; diff --git a/src/data/hassio/supervisor.ts b/src/data/hassio/supervisor.ts new file mode 100644 index 0000000000..166cea837f --- /dev/null +++ b/src/data/hassio/supervisor.ts @@ -0,0 +1,61 @@ +import { HomeAssistant, PanelInfo } from "../../types"; +import { HassioResponse, hassioApiResultExtractor } from "./common"; + +export type HassioHomeAssistantInfo = any; +export type HassioSupervisorInfo = any; + +export type HassioPanelInfo = PanelInfo< + | undefined + | { + ingress?: string; + } +>; + +export interface CreateSessionResponse { + session: string; +} + +export interface SupervisorOptions { + channel: "beta" | "dev" | "stable"; +} + +export const fetchHassioHomeAssistantInfo = async (hass: HomeAssistant) => { + return hassioApiResultExtractor( + await hass.callApi>( + "GET", + "hassio/homeassistant/info" + ) + ); +}; + +export const fetchHassioSupervisorInfo = async (hass: HomeAssistant) => { + return hassioApiResultExtractor( + await hass.callApi>( + "GET", + "hassio/supervisor/info" + ) + ); +}; + +export const fetchSupervisorLogs = async (hass: HomeAssistant) => { + return hass.callApi("GET", "hassio/supervisor/logs"); +}; + +export const createHassioSession = async (hass: HomeAssistant) => { + const response = await hass.callApi>( + "POST", + "hassio/ingress/session" + ); + document.cookie = `ingress_session=${response.data.session};path=/api/hassio_ingress/`; +}; + +export const setSupervisorOption = async ( + hass: HomeAssistant, + data: SupervisorOptions +) => { + await hass.callApi>( + "POST", + "hassio/supervisor/options", + data + ); +}; diff --git a/src/data/logbook.ts b/src/data/logbook.ts new file mode 100644 index 0000000000..be4ab3166e --- /dev/null +++ b/src/data/logbook.ts @@ -0,0 +1,7 @@ +export interface LogbookEntry { + when: string; + name: string; + message: string; + entity_id?: string; + domain: string; +} diff --git a/src/data/scene.ts b/src/data/scene.ts index 177b8dfbd8..6e3e65a66f 100644 --- a/src/data/scene.ts +++ b/src/data/scene.ts @@ -4,6 +4,7 @@ import { } from "home-assistant-js-websocket"; import { HomeAssistant, ServiceCallResponse } from "../types"; +import { navigate } from "../common/navigate"; export const SCENE_IGNORED_DOMAINS = [ "sensor", @@ -18,6 +19,22 @@ export const SCENE_IGNORED_DOMAINS = [ "zone", ]; +let inititialSceneEditorData: Partial | undefined; + +export const showSceneEditor = ( + el: HTMLElement, + data?: Partial +) => { + inititialSceneEditorData = data; + navigate(el, "/config/scene/edit/new"); +}; + +export const getSceneEditorInitData = () => { + const data = inititialSceneEditorData; + inititialSceneEditorData = undefined; + return data; +}; + export interface SceneEntity extends HassEntityBase { attributes: HassEntityAttributeBase & { id?: string }; } diff --git a/src/data/script.ts b/src/data/script.ts index 781ed79af5..c52979b664 100644 --- a/src/data/script.ts +++ b/src/data/script.ts @@ -5,6 +5,7 @@ import { HassEntityBase, HassEntityAttributeBase, } from "home-assistant-js-websocket"; +import { navigate } from "../common/navigate"; export interface ScriptEntity extends HassEntityBase { attributes: HassEntityAttributeBase & { @@ -65,3 +66,19 @@ export const triggerScript = ( export const deleteScript = (hass: HomeAssistant, objectId: string) => hass.callApi("DELETE", `config/script/config/${objectId}`); + +let inititialScriptEditorData: Partial | undefined; + +export const showScriptEditor = ( + el: HTMLElement, + data?: Partial +) => { + inititialScriptEditorData = data; + navigate(el, "/config/script/new"); +}; + +export const getScriptEditorInitData = () => { + const data = inititialScriptEditorData; + inititialScriptEditorData = undefined; + return data; +}; diff --git a/src/data/search.ts b/src/data/search.ts new file mode 100644 index 0000000000..1e00372c9d --- /dev/null +++ b/src/data/search.ts @@ -0,0 +1,33 @@ +import { HomeAssistant } from "../types"; + +export interface RelatedResult { + area?: string[]; + automation?: string[]; + config_entry?: string[]; + device?: string[]; + entity?: string[]; + group?: string[]; + scene?: string[]; + script?: string[]; +} + +export type ItemType = + | "area" + | "automation" + | "config_entry" + | "device" + | "entity" + | "group" + | "scene" + | "script"; + +export const findRelated = ( + hass: HomeAssistant, + itemType: ItemType, + itemId: string +): Promise => + hass.callWS({ + type: "search/related", + item_type: itemType, + item_id: itemId, + }); diff --git a/src/data/zha.ts b/src/data/zha.ts index d0d57d38d8..0507747150 100644 --- a/src/data/zha.ts +++ b/src/data/zha.ts @@ -22,6 +22,7 @@ export interface ZHADevice { user_given_name?: string; power_source?: string; area_id?: string; + device_type: string; } export interface Attribute { @@ -126,6 +127,32 @@ export const unbindDevices = ( target_ieee: targetIEEE, }); +export const bindDeviceToGroup = ( + hass: HomeAssistant, + deviceIEEE: string, + groupId: number, + clusters: Cluster[] +): Promise => + hass.callWS({ + type: "zha/groups/bind", + source_ieee: deviceIEEE, + group_id: groupId, + bindings: clusters, + }); + +export const unbindDeviceFromGroup = ( + hass: HomeAssistant, + deviceIEEE: string, + groupId: number, + clusters: Cluster[] +): Promise => + hass.callWS({ + type: "zha/groups/unbind", + source_ieee: deviceIEEE, + group_id: groupId, + bindings: clusters, + }); + export const readAttributeValue = ( hass: HomeAssistant, data: ReadAttributeServiceData diff --git a/src/data/zone.ts b/src/data/zone.ts new file mode 100644 index 0000000000..1bd97a4351 --- /dev/null +++ b/src/data/zone.ts @@ -0,0 +1,46 @@ +import { HomeAssistant } from "../types"; + +export interface Zone { + id: string; + name: string; + icon?: string; + latitude: number; + longitude: number; + passive?: boolean; + radius?: number; +} + +export interface ZoneMutableParams { + icon: string; + latitude: number; + longitude: number; + name: string; + passive: boolean; + radius: number; +} + +export const fetchZones = (hass: HomeAssistant) => + hass.callWS({ type: "zone/list" }); + +export const createZone = (hass: HomeAssistant, values: ZoneMutableParams) => + hass.callWS({ + type: "zone/create", + ...values, + }); + +export const updateZone = ( + hass: HomeAssistant, + zoneId: string, + updates: Partial +) => + hass.callWS({ + type: "zone/update", + zone_id: zoneId, + ...updates, + }); + +export const deleteZone = (hass: HomeAssistant, zoneId: string) => + hass.callWS({ + type: "zone/delete", + zone_id: zoneId, + }); diff --git a/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts b/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts index a8c987d216..e660f44019 100644 --- a/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts +++ b/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts @@ -48,7 +48,7 @@ class DialogConfigEntrySystemOptions extends LitElement { await this.updateComplete; } - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this._params) { return html``; } @@ -115,7 +115,7 @@ class DialogConfigEntrySystemOptions extends LitElement { .disabled=${this._submitting} > ${this.hass.localize( - "ui.panel.config.entity_registry.editor.update" + "ui.panel.config.entities.editor.update" )}
diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index d51838a682..2ff2a75f55 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -11,6 +11,7 @@ import { import "@material/mwc-button"; import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; import "@polymer/paper-tooltip/paper-tooltip"; +import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-spinner/paper-spinner"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; @@ -115,7 +116,7 @@ class DataEntryFlowDialog extends LitElement { this._scheduleCenterDialog(); } - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this._params) { return html``; } @@ -124,6 +125,7 @@ class DataEntryFlowDialog extends LitElement { ${this._loading || (this._step === null && this._handlers === undefined) @@ -134,53 +136,62 @@ class DataEntryFlowDialog extends LitElement { ? // When we are going to next step, we render 1 round of empty // to reset the element. "" - : this._step === null - ? // Show handler picker - html` - - ` - : this._step.type === "form" - ? html` - - ` - : this._step.type === "external" - ? html` - - ` - : this._step.type === "abort" - ? html` - - ` - : this._devices === undefined || this._areas === undefined - ? // When it's a create entry result, we will fetch device & area registry - html` - - ` : html` - + + ${this._step === null + ? // Show handler picker + html` + + ` + : this._step.type === "form" + ? html` + + ` + : this._step.type === "external" + ? html` + + ` + : this._step.type === "abort" + ? html` + + ` + : this._devices === undefined || this._areas === undefined + ? // When it's a create entry result, we will fetch device & area registry + html` + + ` + : html` + + `} `} `; @@ -311,13 +322,19 @@ class DataEntryFlowDialog extends LitElement { haStyleDialog, css` ha-paper-dialog { - max-width: 500px; + max-width: 600px; } ha-paper-dialog > * { margin: 0; display: block; padding: 0; } + paper-icon-button { + display: inline-block; + padding: 8px; + margin: 16px 16px 0 0; + float: right; + } `, ]; } diff --git a/src/dialogs/config-flow/show-dialog-options-flow.ts b/src/dialogs/config-flow/show-dialog-options-flow.ts index b1e22475e8..3e1b6d1742 100644 --- a/src/dialogs/config-flow/show-dialog-options-flow.ts +++ b/src/dialogs/config-flow/show-dialog-options-flow.ts @@ -44,12 +44,25 @@ export const showOptionsFlowDialog = ( : ""; }, - renderShowFormStepHeader(hass, _step) { - return hass.localize(`ui.dialogs.options_flow.form.header`); + renderShowFormStepHeader(hass, step) { + return ( + hass.localize( + `component.${step.handler}.options.step.${step.step_id}.title` + ) || hass.localize(`ui.dialogs.options_flow.form.header`) + ); }, - renderShowFormStepDescription(_hass, _step) { - return ""; + renderShowFormStepDescription(hass, step) { + const description = localizeKey( + hass.localize, + `component.${step.handler}.config.step.${step.step_id}.description`, + step.description_placeholders + ); + return description + ? html` + + ` + : ""; }, renderShowFormStepFieldLabel(hass, step, field) { diff --git a/src/dialogs/config-flow/step-flow-abort.ts b/src/dialogs/config-flow/step-flow-abort.ts index bbb3378764..211fa6c522 100644 --- a/src/dialogs/config-flow/step-flow-abort.ts +++ b/src/dialogs/config-flow/step-flow-abort.ts @@ -24,7 +24,7 @@ class StepFlowAbort extends LitElement { @property() private step!: DataEntryFlowStepAbort; - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html`

${this.hass.localize( diff --git a/src/dialogs/config-flow/step-flow-create-entry.ts b/src/dialogs/config-flow/step-flow-create-entry.ts index 9fe62ffd2b..fe6d1ffeab 100644 --- a/src/dialogs/config-flow/step-flow-create-entry.ts +++ b/src/dialogs/config-flow/step-flow-create-entry.ts @@ -11,7 +11,7 @@ import "@material/mwc-button"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-listbox/paper-listbox"; - +import "../../components/ha-area-picker"; import { HomeAssistant } from "../../types"; import { fireEvent } from "../../common/dom/fire_event"; import { configFlowContentStyles } from "./styles"; @@ -19,12 +19,9 @@ import { DeviceRegistryEntry, updateDeviceRegistryEntry, } from "../../data/device_registry"; -import { - AreaRegistryEntry, - createAreaRegistryEntry, -} from "../../data/area_registry"; import { DataEntryFlowStepCreateEntry } from "../../data/data_entry_flow"; import { FlowConfig } from "./show-dialog-data-entry-flow"; +import { showAlertDialog } from "../generic/show-dialog-box"; @customElement("step-flow-create-entry") class StepFlowCreateEntry extends LitElement { @@ -39,10 +36,7 @@ class StepFlowCreateEntry extends LitElement { @property() public devices!: DeviceRegistryEntry[]; - @property() - public areas!: AreaRegistryEntry[]; - - protected render(): TemplateResult | void { + protected render(): TemplateResult { const localize = this.hass.localize; return html` @@ -62,28 +56,11 @@ class StepFlowCreateEntry extends LitElement { ${device.name}
${device.model} (${device.manufacturer})

- - - - ${localize( - "ui.panel.config.integrations.config_entry.no_area" - )} - - ${this.areas.map( - (area) => html` - - ${area.name} - - ` - )} - - + @value-changed=${this._areaPicked} + >
` )} @@ -91,16 +68,6 @@ class StepFlowCreateEntry extends LitElement { `}
- ${this.devices.length > 0 - ? html` - ${localize( - "ui.panel.config.integrations.config_flow.add_area" - )} - ` - : ""} - ${localize( "ui.panel.config.integrations.config_flow.finish" @@ -114,52 +81,24 @@ class StepFlowCreateEntry extends LitElement { fireEvent(this, "flow-update", { step: undefined }); } - private async _addArea() { - const name = prompt( - this.hass.localize( - "ui.panel.config.integrations.config_flow.name_new_area" - ) - ); - if (!name) { - return; - } - try { - const area = await createAreaRegistryEntry(this.hass, { - name, - }); - this.areas = [...this.areas, area]; - } catch (err) { - alert( - this.hass.localize( - "ui.panel.config.integrations.config_flow.failed_create_area" - ) - ); - } - } + private async _areaPicked(ev: CustomEvent) { + const picker = ev.currentTarget as any; + const device = picker.device; - private async _handleAreaChanged(ev: Event) { - const dropdown = ev.currentTarget as any; - const device = dropdown.device; - - // Item first becomes null, then new item. - if (!dropdown.selectedItem) { - return; - } - - const area = dropdown.selectedItem.area; + const area = ev.detail.value; try { await updateDeviceRegistryEntry(this.hass, device, { area_id: area, }); } catch (err) { - alert( - this.hass.localize( + showAlertDialog(this, { + text: this.hass.localize( "ui.panel.config.integrations.config_flow.error_saving_area", "error", - "err.message" - ) - ); - dropdown.value = null; + err.message + ), + }); + picker.value = null; } } @@ -180,7 +119,7 @@ class StepFlowCreateEntry extends LitElement { border-radius: 4px; margin: 4px; display: inline-block; - width: 200px; + width: 250px; } .buttons > *:last-child { margin-left: auto; diff --git a/src/dialogs/config-flow/step-flow-external.ts b/src/dialogs/config-flow/step-flow-external.ts index 47cd149705..d2dde40c98 100644 --- a/src/dialogs/config-flow/step-flow-external.ts +++ b/src/dialogs/config-flow/step-flow-external.ts @@ -28,7 +28,7 @@ class StepFlowExternal extends LitElement { @property() private step!: DataEntryFlowStepExternal; - protected render(): TemplateResult | void { + protected render(): TemplateResult { const localize = this.hass.localize; return html` diff --git a/src/dialogs/config-flow/step-flow-form.ts b/src/dialogs/config-flow/step-flow-form.ts index c97c491df5..172cf333a9 100644 --- a/src/dialogs/config-flow/step-flow-form.ts +++ b/src/dialogs/config-flow/step-flow-form.ts @@ -40,7 +40,7 @@ class StepFlowForm extends LitElement { @property() private _errorMsg?: string; - protected render(): TemplateResult | void { + protected render(): TemplateResult { const step = this.step; const stepData = this._stepDataProcessed; diff --git a/src/dialogs/config-flow/step-flow-loading.ts b/src/dialogs/config-flow/step-flow-loading.ts index aebee5db4a..6df00345ca 100644 --- a/src/dialogs/config-flow/step-flow-loading.ts +++ b/src/dialogs/config-flow/step-flow-loading.ts @@ -10,7 +10,7 @@ import "@polymer/paper-spinner/paper-spinner-lite"; @customElement("step-flow-loading") class StepFlowLoading extends LitElement { - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html`
diff --git a/src/dialogs/config-flow/step-flow-pick-handler.ts b/src/dialogs/config-flow/step-flow-pick-handler.ts index 19963854af..76170c4b6c 100644 --- a/src/dialogs/config-flow/step-flow-pick-handler.ts +++ b/src/dialogs/config-flow/step-flow-pick-handler.ts @@ -19,6 +19,7 @@ import "../../components/ha-icon-next"; import "../../common/search/search-input"; import { styleMap } from "lit-html/directives/style-map"; import { FlowConfig } from "./show-dialog-data-entry-flow"; +import { configFlowContentStyles } from "./styles"; interface HandlerObj { name: string; @@ -58,7 +59,7 @@ class StepFlowPickHandler extends LitElement { ); }); - protected render(): TemplateResult | void { + protected render(): TemplateResult { const handlers = this._getHandlers(this.handlers, this.filter); return html` @@ -133,28 +134,27 @@ class StepFlowPickHandler extends LitElement { }); } - static get styles(): CSSResult { - return css` - h2 { - margin-bottom: 2px; - padding-left: 16px; - } - div { - overflow: auto; - max-height: 600px; - } - paper-item { - cursor: pointer; - } - p { - text-align: center; - padding: 16px; - margin: 0; - } - p > a { - color: var(--primary-color); - } - `; + static get styles(): CSSResult[] { + return [ + configFlowContentStyles, + css` + div { + overflow: auto; + max-height: 600px; + } + paper-item { + cursor: pointer; + } + p { + text-align: center; + padding: 16px; + margin: 0; + } + p > a { + color: var(--primary-color); + } + `, + ]; } } diff --git a/src/dialogs/confirmation/dialog-confirmation.ts b/src/dialogs/confirmation/dialog-confirmation.ts deleted file mode 100644 index 2ef6003b54..0000000000 --- a/src/dialogs/confirmation/dialog-confirmation.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { - LitElement, - html, - css, - CSSResult, - TemplateResult, - customElement, - property, -} from "lit-element"; -import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; -import "@polymer/paper-input/paper-input"; - -import "../../components/dialog/ha-paper-dialog"; -import "../../components/ha-switch"; - -import { HomeAssistant } from "../../types"; -import { ConfirmationDialogParams } from "./show-dialog-confirmation"; -import { PolymerChangedEvent } from "../../polymer-types"; -import { haStyleDialog } from "../../resources/styles"; - -@customElement("dialog-confirmation") -class DialogConfirmation extends LitElement { - @property() public hass!: HomeAssistant; - @property() private _params?: ConfirmationDialogParams; - - public async showDialog(params: ConfirmationDialogParams): Promise { - this._params = params; - } - - protected render(): TemplateResult | void { - if (!this._params) { - return html``; - } - - return html` - -

- ${this._params.title - ? this._params.title - : this.hass.localize("ui.dialogs.confirmation.title")} -

- -

${this._params.text}

-
-
- - ${this._params.cancelBtnText - ? this._params.cancelBtnText - : this.hass.localize("ui.dialogs.confirmation.cancel")} - - - ${this._params.confirmBtnText - ? this._params.confirmBtnText - : this.hass.localize("ui.dialogs.confirmation.ok")} - -
-
- `; - } - - private async _dismiss(): Promise { - this._params = undefined; - } - - private async _confirm(): Promise { - this._params!.confirm(); - this._dismiss(); - } - - private _openedChanged(ev: PolymerChangedEvent): void { - if (!(ev.detail as any).value) { - this._params = undefined; - } - } - - static get styles(): CSSResult[] { - return [ - haStyleDialog, - css` - ha-paper-dialog { - min-width: 400px; - max-width: 500px; - } - @media (max-width: 400px) { - ha-paper-dialog { - min-width: initial; - } - } - p { - margin: 0; - padding-top: 6px; - padding-bottom: 24px; - color: var(--primary-text-color); - } - .secondary { - color: var(--secondary-text-color); - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "dialog-confirmation": DialogConfirmation; - } -} diff --git a/src/dialogs/confirmation/show-dialog-confirmation.ts b/src/dialogs/confirmation/show-dialog-confirmation.ts deleted file mode 100644 index df57cf5558..0000000000 --- a/src/dialogs/confirmation/show-dialog-confirmation.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { fireEvent } from "../../common/dom/fire_event"; - -export interface ConfirmationDialogParams { - title?: string; - text: string; - confirmBtnText?: string; - cancelBtnText?: string; - confirm: () => void; -} - -export const loadConfirmationDialog = () => - import(/* webpackChunkName: "confirmation" */ "./dialog-confirmation"); - -export const showConfirmationDialog = ( - element: HTMLElement, - systemLogDetailParams: ConfirmationDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-confirmation", - dialogImport: loadConfirmationDialog, - dialogParams: systemLogDetailParams, - }); -}; diff --git a/src/dialogs/device-registry-detail/dialog-device-registry-detail.ts b/src/dialogs/device-registry-detail/dialog-device-registry-detail.ts index 5043adf429..c53a6b933c 100644 --- a/src/dialogs/device-registry-detail/dialog-device-registry-detail.ts +++ b/src/dialogs/device-registry-detail/dialog-device-registry-detail.ts @@ -15,15 +15,12 @@ import "@polymer/paper-item/paper-item"; import "@material/mwc-button/mwc-button"; import "../../components/dialog/ha-paper-dialog"; +import "../../components/ha-area-picker"; import { DeviceRegistryDetailDialogParams } from "./show-dialog-device-registry-detail"; import { PolymerChangedEvent } from "../../polymer-types"; import { haStyleDialog } from "../../resources/styles"; import { HomeAssistant } from "../../types"; -import { - subscribeAreaRegistry, - AreaRegistryEntry, -} from "../../data/area_registry"; import { computeDeviceName } from "../../data/device_registry"; @customElement("dialog-device-registry-detail") @@ -33,11 +30,9 @@ class DialogDeviceRegistryDetail extends LitElement { @property() private _nameByUser!: string; @property() private _error?: string; @property() private _params?: DeviceRegistryDetailDialogParams; - @property() private _areas?: AreaRegistryEntry[]; @property() private _areaId?: string; private _submitting?: boolean; - private _unsubAreas?: any; public async showDialog( params: DeviceRegistryDetailDialogParams @@ -49,21 +44,7 @@ class DialogDeviceRegistryDetail extends LitElement { await this.updateComplete; } - public connectedCallback() { - super.connectedCallback(); - this._unsubAreas = subscribeAreaRegistry(this.hass.connection, (areas) => { - this._areas = areas; - }); - } - - public disconnectedCallback() { - super.disconnectedCallback(); - if (this._unsubAreas) { - this._unsubAreas(); - } - } - - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this._params) { return html``; } @@ -88,38 +69,20 @@ class DialogDeviceRegistryDetail extends LitElement { -
- - - - ${this.hass.localize( - "ui.panel.config.integrations.config_entry.no_area" - )} - - ${this._renderAreas()} - - -
+
- ${this.hass.localize( - "ui.panel.config.entity_registry.editor.update" - )} + ${this.hass.localize("ui.panel.config.devices.update")}
@@ -131,35 +94,8 @@ class DialogDeviceRegistryDetail extends LitElement { this._nameByUser = ev.detail.value; } - private _renderAreas() { - if (!this._areas) { - return; - } - return this._areas!.map( - (area) => html` - ${area.name} - ` - ); - } - - private _computeSelectedArea() { - if (!this._params || !this._areas) { - return -1; - } - const device = this._params!.device; - if (!device.area_id) { - return 0; - } - // +1 because of "No Area" entry - return this._areas.findIndex((area) => area.area_id === device.area_id) + 1; - } - - private _areaIndexChanged(event): void { - const selectedAreaIdx = event.target!.selected; - this._areaId = - selectedAreaIdx < 1 - ? undefined - : this._areas![selectedAreaIdx - 1].area_id; + private _areaPicked(event: CustomEvent): void { + this._areaId = event.detail.value; } private async _updateEntry(): Promise { diff --git a/src/dialogs/domain-toggler/dialog-domain-toggler.ts b/src/dialogs/domain-toggler/dialog-domain-toggler.ts index 5f34ea3647..6ecec2f953 100644 --- a/src/dialogs/domain-toggler/dialog-domain-toggler.ts +++ b/src/dialogs/domain-toggler/dialog-domain-toggler.ts @@ -22,7 +22,7 @@ class DomainTogglerDialog extends LitElement { this._params = params; } - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this._params) { return html``; } diff --git a/src/dialogs/generic/dialog-box.ts b/src/dialogs/generic/dialog-box.ts new file mode 100644 index 0000000000..77b37f2092 --- /dev/null +++ b/src/dialogs/generic/dialog-box.ts @@ -0,0 +1,162 @@ +import { + LitElement, + html, + css, + CSSResult, + TemplateResult, + customElement, + property, +} from "lit-element"; +import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; +import "@polymer/paper-input/paper-input"; + +import "../../components/dialog/ha-paper-dialog"; +import "../../components/ha-switch"; + +import { HomeAssistant } from "../../types"; +import { DialogParams } from "./show-dialog-box"; +import { PolymerChangedEvent } from "../../polymer-types"; +import { haStyleDialog } from "../../resources/styles"; +import { classMap } from "lit-html/directives/class-map"; + +@customElement("dialog-box") +class DialogBox extends LitElement { + @property() public hass!: HomeAssistant; + @property() private _params?: DialogParams; + @property() private _value?: string; + + public async showDialog(params: DialogParams): Promise { + this._params = params; + if (params.prompt) { + this._value = params.defaultValue; + } + } + + protected render(): TemplateResult { + if (!this._params) { + return html``; + } + + const confirmPrompt = this._params.confirmation || this._params.prompt; + + return html` + +

+ ${this._params.title + ? this._params.title + : this._params.confirmation && + this.hass.localize( + "ui.dialogs.generic.default_confirmation_title" + )} +

+ + ${this._params.text + ? html` +

+ ${this._params.text} +

+ ` + : ""} + ${this._params.prompt + ? html` + + ` + : ""} +
+
+ ${confirmPrompt && + html` + + ${this._params.dismissText + ? this._params.dismissText + : this.hass.localize("ui.dialogs.generic.cancel")} + + `} + + ${this._params.confirmText + ? this._params.confirmText + : this.hass.localize("ui.dialogs.generic.ok")} + +
+
+ `; + } + + private _valueChanged(ev: PolymerChangedEvent) { + this._value = ev.detail.value; + } + + private async _dismiss(): Promise { + if (this._params!.cancel) { + this._params!.cancel(); + } + this._params = undefined; + } + + private async _confirm(): Promise { + if (this._params!.confirm) { + this._params!.confirm(this._value); + } + this._dismiss(); + } + + private _openedChanged(ev: PolymerChangedEvent): void { + if (!(ev.detail as any).value) { + this._params = undefined; + } + } + + static get styles(): CSSResult[] { + return [ + haStyleDialog, + css` + ha-paper-dialog { + min-width: 400px; + max-width: 500px; + } + @media (max-width: 400px) { + ha-paper-dialog { + min-width: initial; + } + } + p { + margin: 0; + padding-top: 6px; + padding-bottom: 24px; + color: var(--primary-text-color); + } + .no-bottom-padding { + padding-bottom: 0; + } + .secondary { + color: var(--secondary-text-color); + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-box": DialogBox; + } +} diff --git a/src/dialogs/generic/show-dialog-box.ts b/src/dialogs/generic/show-dialog-box.ts new file mode 100644 index 0000000000..bd88af81b6 --- /dev/null +++ b/src/dialogs/generic/show-dialog-box.ts @@ -0,0 +1,62 @@ +import { fireEvent } from "../../common/dom/fire_event"; + +interface AlertDialogParams { + confirmText?: string; + text?: string; + title?: string; + confirm?: (out?: string) => void; +} + +interface ConfirmationDialogParams extends AlertDialogParams { + dismissText?: string; + cancel?: () => void; +} + +interface PromptDialogParams extends AlertDialogParams { + inputLabel?: string; + inputType?: string; + defaultValue?: string; +} + +export interface DialogParams + extends ConfirmationDialogParams, + PromptDialogParams { + confirmation?: boolean; + prompt?: boolean; +} + +export const loadGenericDialog = () => + import(/* webpackChunkName: "confirmation" */ "./dialog-box"); + +export const showAlertDialog = ( + element: HTMLElement, + dialogParams: AlertDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-box", + dialogImport: loadGenericDialog, + dialogParams, + }); +}; + +export const showConfirmationDialog = ( + element: HTMLElement, + dialogParams: ConfirmationDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-box", + dialogImport: loadGenericDialog, + dialogParams: { ...dialogParams, confirmation: true }, + }); +}; + +export const showPromptDialog = ( + element: HTMLElement, + dialogParams: PromptDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-box", + dialogImport: loadGenericDialog, + dialogParams: { ...dialogParams, prompt: true }, + }); +}; diff --git a/src/dialogs/ha-more-info-dialog.js b/src/dialogs/ha-more-info-dialog.js index 1d95816055..3a34c1cfc9 100644 --- a/src/dialogs/ha-more-info-dialog.js +++ b/src/dialogs/ha-more-info-dialog.js @@ -6,7 +6,6 @@ import { PolymerElement } from "@polymer/polymer/polymer-element"; import "../resources/ha-style"; import "./more-info/more-info-controls"; -import "./more-info/more-info-settings"; import { computeStateDomain } from "../common/entity/compute_state_domain"; import { isComponentLoaded } from "../common/config/is_component_loaded"; @@ -26,8 +25,7 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) { border-radius: 2px; } - more-info-controls, - more-info-settings { + more-info-controls { --more-info-header-background: var(--secondary-background-color); --more-info-header-color: var(--primary-text-color); --ha-more-info-app-toolbar-title: { @@ -46,8 +44,7 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) { /* overrule the ha-style-dialog max-height on small screens */ @media all and (max-width: 450px), all and (max-height: 500px) { - more-info-controls, - more-info-settings { + more-info-controls { --more-info-header-background: var(--primary-color); --more-info-header-color: var(--text-primary-color); } @@ -79,24 +76,14 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) { } - - + `; } @@ -118,11 +105,6 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) { _dialogElement: Object, _registryInfo: Object, - _page: { - type: String, - value: null, - }, - dataDomain: { computed: "_computeDomain(stateObj)", reflectToAttribute: true, @@ -137,9 +119,6 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) { ready() { super.ready(); this._dialogElement = this; - this.addEventListener("more-info-page", (ev) => { - this._page = ev.detail.page; - }); } _computeDomain(stateObj) { @@ -154,7 +133,6 @@ class HaMoreInfoDialog extends DialogMixin(PolymerElement) { if (!newVal) { this.setProperties({ opened: false, - _page: null, _registryInfo: null, large: false, }); diff --git a/src/dialogs/make-dialog-manager.ts b/src/dialogs/make-dialog-manager.ts index 41e2cc5cba..02af7d4e96 100644 --- a/src/dialogs/make-dialog-manager.ts +++ b/src/dialogs/make-dialog-manager.ts @@ -5,6 +5,7 @@ declare global { // for fire event interface HASSDomEvents { "show-dialog": ShowDialogParams; + "close-dialog": undefined; } // for add event listener interface HTMLElementEventMap { diff --git a/src/dialogs/more-info/controls/more-info-automation.js b/src/dialogs/more-info/controls/more-info-automation.js deleted file mode 100644 index d766184ee1..0000000000 --- a/src/dialogs/more-info/controls/more-info-automation.js +++ /dev/null @@ -1,53 +0,0 @@ -import "@material/mwc-button"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -import "../../../components/ha-relative-time"; - -import LocalizeMixin from "../../../mixins/localize-mixin"; - -class MoreInfoAutomation extends LocalizeMixin(PolymerElement) { - static get template() { - return html` - - -
-
[[localize('ui.card.automation.last_triggered')]]:
- -
- -
- - [[localize('ui.card.automation.trigger')]] - -
- `; - } - - static get properties() { - return { - hass: Object, - stateObj: Object, - }; - } - - handleTriggerTapped() { - this.hass.callService("automation", "trigger", { - entity_id: this.stateObj.entity_id, - }); - } -} - -customElements.define("more-info-automation", MoreInfoAutomation); diff --git a/src/dialogs/more-info/controls/more-info-automation.ts b/src/dialogs/more-info/controls/more-info-automation.ts new file mode 100644 index 0000000000..62dc5cd7c6 --- /dev/null +++ b/src/dialogs/more-info/controls/more-info-automation.ts @@ -0,0 +1,68 @@ +import { + LitElement, + html, + TemplateResult, + CSSResult, + css, + property, + customElement, +} from "lit-element"; +import { HassEntity } from "home-assistant-js-websocket"; +import "@material/mwc-button"; + +import "../../../components/ha-relative-time"; + +import { HomeAssistant } from "../../../types"; + +@customElement("more-info-automation") +class MoreInfoAutomation extends LitElement { + @property() public hass!: HomeAssistant; + @property() public stateObj?: HassEntity; + + protected render(): TemplateResult { + if (!this.hass || !this.stateObj) { + return html``; + } + + return html` +
+
${this.hass.localize("ui.card.automation.last_triggered")}:
+ +
+ +
+ + ${this.hass.localize("ui.card.automation.trigger")} + +
+ `; + } + + private handleAction() { + this.hass.callService("automation", "trigger", { + entity_id: this.stateObj!.entity_id, + }); + } + + static get styles(): CSSResult { + return css` + .flex { + display: flex; + justify-content: space-between; + } + .actions { + margin: 36px 0 8px 0; + text-align: right; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "more-info-automation": MoreInfoAutomation; + } +} diff --git a/src/dialogs/more-info/controls/more-info-camera.ts b/src/dialogs/more-info/controls/more-info-camera.ts index 07544eeeb2..2d2b3eece7 100644 --- a/src/dialogs/more-info/controls/more-info-camera.ts +++ b/src/dialogs/more-info/controls/more-info-camera.ts @@ -38,7 +38,7 @@ class MoreInfoCamera extends LitElement { this._attached = false; } - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this._attached || !this.hass || !this.stateObj) { return html``; } diff --git a/src/dialogs/more-info/controls/more-info-climate.ts b/src/dialogs/more-info/controls/more-info-climate.ts index abad9c84c2..7837536d14 100644 --- a/src/dialogs/more-info/controls/more-info-climate.ts +++ b/src/dialogs/more-info/controls/more-info-climate.ts @@ -39,7 +39,7 @@ class MoreInfoClimate extends LitElement { @property() public stateObj?: ClimateEntity; private _resizeDebounce?: number; - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this.stateObj) { return html``; } diff --git a/src/dialogs/more-info/controls/more-info-content.ts b/src/dialogs/more-info/controls/more-info-content.ts index 871a2eed0e..c84f4a5156 100644 --- a/src/dialogs/more-info/controls/more-info-content.ts +++ b/src/dialogs/more-info/controls/more-info-content.ts @@ -19,7 +19,6 @@ import "./more-info-media_player"; import "./more-info-script"; import "./more-info-sun"; import "./more-info-timer"; -import "./more-info-updater"; import "./more-info-vacuum"; import "./more-info-water_heater"; import "./more-info-weather"; diff --git a/src/dialogs/more-info/controls/more-info-counter.ts b/src/dialogs/more-info/controls/more-info-counter.ts index 3253dd272a..c9ba166982 100644 --- a/src/dialogs/more-info/controls/more-info-counter.ts +++ b/src/dialogs/more-info/controls/more-info-counter.ts @@ -17,7 +17,7 @@ class MoreInfoCounter extends LitElement { @property() public hass!: HomeAssistant; @property() public stateObj?: HassEntity; - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this.hass || !this.stateObj) { return html``; } diff --git a/src/dialogs/more-info/controls/more-info-default.js b/src/dialogs/more-info/controls/more-info-default.js deleted file mode 100644 index ee1c44a59d..0000000000 --- a/src/dialogs/more-info/controls/more-info-default.js +++ /dev/null @@ -1,22 +0,0 @@ -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -import "../../../components/ha-attributes"; - -class MoreInfoDefault extends PolymerElement { - static get template() { - return html` - - `; - } - - static get properties() { - return { - stateObj: { - type: Object, - }, - }; - } -} - -customElements.define("more-info-default", MoreInfoDefault); diff --git a/src/dialogs/more-info/controls/more-info-default.ts b/src/dialogs/more-info/controls/more-info-default.ts new file mode 100644 index 0000000000..7b30893609 --- /dev/null +++ b/src/dialogs/more-info/controls/more-info-default.ts @@ -0,0 +1,34 @@ +import { + LitElement, + html, + TemplateResult, + property, + customElement, +} from "lit-element"; +import { HassEntity } from "home-assistant-js-websocket"; + +import { HomeAssistant } from "../../../types"; + +import "../../../components/ha-attributes"; + +@customElement("more-info-default") +class MoreInfoDefault extends LitElement { + @property() public hass!: HomeAssistant; + @property() public stateObj?: HassEntity; + + protected render(): TemplateResult { + if (!this.hass || !this.stateObj) { + return html``; + } + + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "more-info-default": MoreInfoDefault; + } +} diff --git a/src/dialogs/more-info/controls/more-info-script.js b/src/dialogs/more-info/controls/more-info-script.js deleted file mode 100644 index fec483e033..0000000000 --- a/src/dialogs/more-info/controls/more-info-script.js +++ /dev/null @@ -1,31 +0,0 @@ -import "@polymer/iron-flex-layout/iron-flex-layout-classes"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import LocalizeMixin from "../../../mixins/localize-mixin"; - -class MoreInfoScript extends LocalizeMixin(PolymerElement) { - static get template() { - return html` - - -
-
-
- [[localize('ui.dialogs.more_info_control.script.last_action')]] -
-
[[stateObj.attributes.last_action]]
-
-
- `; - } - - static get properties() { - return { - stateObj: { - type: Object, - }, - }; - } -} - -customElements.define("more-info-script", MoreInfoScript); diff --git a/src/dialogs/more-info/controls/more-info-script.ts b/src/dialogs/more-info/controls/more-info-script.ts new file mode 100644 index 0000000000..3c8146795f --- /dev/null +++ b/src/dialogs/more-info/controls/more-info-script.ts @@ -0,0 +1,46 @@ +import { + LitElement, + html, + TemplateResult, + property, + customElement, +} from "lit-element"; +import { HassEntity } from "home-assistant-js-websocket"; + +import { HomeAssistant } from "../../../types"; + +import "../../../components/ha-relative-time"; + +@customElement("more-info-script") +class MoreInfoScript extends LitElement { + @property() public hass!: HomeAssistant; + @property() public stateObj?: HassEntity; + + protected render(): TemplateResult { + if (!this.hass || !this.stateObj) { + return html``; + } + + return html` +
+ ${this.hass.localize( + "ui.dialogs.more_info_control.script.last_triggered" + )}: + ${this.stateObj.attributes.last_triggered + ? html` + + ` + : this.hass.localize("ui.components.relative_time.never")} +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "more-info-script": MoreInfoScript; + } +} diff --git a/src/dialogs/more-info/controls/more-info-sun.ts b/src/dialogs/more-info/controls/more-info-sun.ts index 088e686379..f8bd512852 100644 --- a/src/dialogs/more-info/controls/more-info-sun.ts +++ b/src/dialogs/more-info/controls/more-info-sun.ts @@ -19,7 +19,7 @@ class MoreInfoSun extends LitElement { @property() public hass!: HomeAssistant; @property() public stateObj?: HassEntity; - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this.hass || !this.stateObj) { return html``; } diff --git a/src/dialogs/more-info/controls/more-info-timer.ts b/src/dialogs/more-info/controls/more-info-timer.ts index e0ce0c5bb5..414f26ed8b 100644 --- a/src/dialogs/more-info/controls/more-info-timer.ts +++ b/src/dialogs/more-info/controls/more-info-timer.ts @@ -19,7 +19,7 @@ class MoreInfoTimer extends LitElement { @property() public stateObj?: TimerEntity; - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this.hass || !this.stateObj) { return html``; } diff --git a/src/dialogs/more-info/controls/more-info-updater.js b/src/dialogs/more-info/controls/more-info-updater.js deleted file mode 100644 index b3aed3b696..0000000000 --- a/src/dialogs/more-info/controls/more-info-updater.js +++ /dev/null @@ -1,41 +0,0 @@ -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import LocalizeMixin from "../../../mixins/localize-mixin"; - -class MoreInfoUpdater extends LocalizeMixin(PolymerElement) { - static get template() { - return html` - - - - `; - } - - static get properties() { - return { - stateObj: { - type: Object, - }, - }; - } - - computeReleaseNotes(stateObj) { - return ( - stateObj.attributes.release_notes || - "https://www.home-assistant.io/docs/installation/updating/" - ); - } -} - -customElements.define("more-info-updater", MoreInfoUpdater); diff --git a/src/dialogs/more-info/controls/more-info-weather.ts b/src/dialogs/more-info/controls/more-info-weather.ts index 8307ae37fd..7806329424 100644 --- a/src/dialogs/more-info/controls/more-info-weather.ts +++ b/src/dialogs/more-info/controls/more-info-weather.ts @@ -72,7 +72,7 @@ class MoreInfoWeather extends LitElement { return false; } - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this.hass || !this.stateObj) { return html``; } diff --git a/src/dialogs/more-info/more-info-controls.js b/src/dialogs/more-info/more-info-controls.js index e55219d128..c4ff5e4f6f 100644 --- a/src/dialogs/more-info/more-info-controls.js +++ b/src/dialogs/more-info/more-info-controls.js @@ -12,6 +12,7 @@ import "../../state-summary/state-card-content"; import "./controls/more-info-content"; +import { navigate } from "../../common/navigate"; import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; @@ -20,9 +21,13 @@ import { EventsMixin } from "../../mixins/events-mixin"; import LocalizeMixin from "../../mixins/localize-mixin"; import { computeRTL } from "../../common/util/compute_rtl"; import { removeEntityRegistryEntry } from "../../data/entity_registry"; -import { showConfirmationDialog } from "../confirmation/show-dialog-confirmation"; +import { showConfirmationDialog } from "../generic/show-dialog-box"; +import { showEntityRegistryDetailDialog } from "../../panels/config/entities/show-dialog-entity-registry-detail"; const DOMAINS_NO_INFO = ["camera", "configurator", "history_graph"]; +const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"]; +const EDITABLE_DOMAINS = ["script"]; + /* * @appliesMixin EventsMixin */ @@ -83,13 +88,20 @@ class MoreInfoControls extends LocalizeMixin(EventsMixin(PolymerElement)) {
[[_computeStateName(stateObj)]]
- @@ -52,6 +56,7 @@ class HaConfigAutomation extends PolymerElement { hass: Object, route: Object, isWide: Boolean, + narrow: Boolean, _routeData: Object, _routeMatches: Boolean, _creatingNew: Boolean, diff --git a/src/panels/config/automation/thingtalk/dialog-thingtalk.ts b/src/panels/config/automation/thingtalk/dialog-thingtalk.ts index 9aa87d029d..7e585228da 100644 --- a/src/panels/config/automation/thingtalk/dialog-thingtalk.ts +++ b/src/panels/config/automation/thingtalk/dialog-thingtalk.ts @@ -58,7 +58,7 @@ class DialogThingtalk extends LitElement { this._opened = true; } - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this._params) { return html``; } diff --git a/src/panels/config/automation/thingtalk/ha-thingtalk-placeholders.ts b/src/panels/config/automation/thingtalk/ha-thingtalk-placeholders.ts index 4afc2d7840..da21508a6d 100644 --- a/src/panels/config/automation/thingtalk/ha-thingtalk-placeholders.ts +++ b/src/panels/config/automation/thingtalk/ha-thingtalk-placeholders.ts @@ -112,7 +112,7 @@ export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) { } } - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html` ${yamlMode @@ -118,7 +118,7 @@ export default class HaAutomationTriggerRow extends LitElement { "ui.panel.config.automation.editor.triggers.duplicate" )} - + ${this.hass.localize( "ui.panel.config.automation.editor.triggers.delete" )} diff --git a/src/panels/config/cloud/account/cloud-alexa-pref.ts b/src/panels/config/cloud/account/cloud-alexa-pref.ts index 0f0b227871..a2536606d4 100644 --- a/src/panels/config/cloud/account/cloud-alexa-pref.ts +++ b/src/panels/config/cloud/account/cloud-alexa-pref.ts @@ -23,7 +23,7 @@ export class CloudAlexaPref extends LitElement { @property() public cloudStatus?: CloudStatusLoggedIn; @property() private _syncing = false; - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this.cloudStatus) { return html``; } diff --git a/src/panels/config/cloud/account/cloud-google-pref.ts b/src/panels/config/cloud/account/cloud-google-pref.ts index fe765868a7..cc376f5f9e 100644 --- a/src/panels/config/cloud/account/cloud-google-pref.ts +++ b/src/panels/config/cloud/account/cloud-google-pref.ts @@ -24,7 +24,7 @@ export class CloudGooglePref extends LitElement { @property() public hass?: HomeAssistant; @property() public cloudStatus?: CloudStatusLoggedIn; - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this.cloudStatus) { return html``; } diff --git a/src/panels/config/cloud/account/cloud-remote-pref.ts b/src/panels/config/cloud/account/cloud-remote-pref.ts index 1c701487ff..dbe4006ec5 100644 --- a/src/panels/config/cloud/account/cloud-remote-pref.ts +++ b/src/panels/config/cloud/account/cloud-remote-pref.ts @@ -29,7 +29,7 @@ export class CloudRemotePref extends LitElement { @property() public hass?: HomeAssistant; @property() public cloudStatus?: CloudStatusLoggedIn; - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this.cloudStatus) { return html``; } diff --git a/src/panels/config/cloud/alexa/cloud-alexa.ts b/src/panels/config/cloud/alexa/cloud-alexa.ts index b630843afe..84008711d8 100644 --- a/src/panels/config/cloud/alexa/cloud-alexa.ts +++ b/src/panels/config/cloud/alexa/cloud-alexa.ts @@ -72,7 +72,7 @@ class CloudAlexa extends LitElement { ) ); - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (this._entities === undefined) { return html` @@ -346,8 +346,8 @@ class CloudAlexa extends LitElement { } ha-card { margin: 4px; - width: 100%; - max-width: 300px; + width: 300px; + flex-grow: 1; } .card-content { padding-bottom: 12px; 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 819afbc23e..65f26ee711 100644 --- a/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts +++ b/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts @@ -71,7 +71,7 @@ class CloudGoogleAssistant extends LitElement { ) ); - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (this._entities === undefined) { return html` @@ -369,8 +369,8 @@ class CloudGoogleAssistant extends LitElement { } ha-card { margin: 4px; - width: 100%; - max-width: 300px; + width: 300px; + flex-grow: 1; } .card-content { padding-bottom: 12px; diff --git a/src/panels/config/core/ha-config-core-form.ts b/src/panels/config/core/ha-config-core-form.ts index 9f10446924..1d3fc487af 100644 --- a/src/panels/config/core/ha-config-core-form.ts +++ b/src/panels/config/core/ha-config-core-form.ts @@ -33,7 +33,7 @@ class ConfigCoreForm extends LitElement { @property() private _unitSystem!: ConfigUpdateValues["unit_system"]; @property() private _timeZone!: string; - protected render(): TemplateResult | void { + protected render(): TemplateResult { const canEdit = ["storage", "default"].includes( this.hass.config.config_source ); @@ -102,9 +102,13 @@ class ConfigCoreForm extends LitElement { @value-changed=${this._handleChange} > - ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.elevation_meters" - )} + ${this._unitSystem === "metric" + ? this.hass.localize( + "ui.panel.config.core.section.core.core_config.elevation_meters" + ) + : this.hass.localize( + "ui.panel.config.core.section.core.core_config.elevation_feet" + )}
diff --git a/src/panels/config/core/ha-config-core.js b/src/panels/config/core/ha-config-core.js index bcc71ca824..723d74f9f1 100644 --- a/src/panels/config/core/ha-config-core.js +++ b/src/panels/config/core/ha-config-core.js @@ -4,11 +4,13 @@ import "@polymer/paper-icon-button/paper-icon-button"; import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "../../../layouts/hass-subpage"; +import "../../../layouts/hass-tabs-subpage"; import "../../../resources/ha-style"; import "./ha-config-section-core"; +import { configSections } from "../ha-panel-config"; + import LocalizeMixin from "../../../mixins/localize-mixin"; /* @@ -33,7 +35,14 @@ class HaConfigCore extends LocalizeMixin(PolymerElement) { } - +
-
+ `; } @@ -49,10 +58,16 @@ class HaConfigCore extends LocalizeMixin(PolymerElement) { return { hass: Object, isWide: Boolean, + narrow: Boolean, showAdvanced: Boolean, + route: Object, }; } + _computeTabs() { + return configSections.general; + } + computeClasses(isWide) { return isWide ? "content" : "content narrow"; } diff --git a/src/panels/config/core/ha-config-name-form.ts b/src/panels/config/core/ha-config-name-form.ts index 32355e575f..63993c5f23 100644 --- a/src/panels/config/core/ha-config-name-form.ts +++ b/src/panels/config/core/ha-config-name-form.ts @@ -24,7 +24,7 @@ class ConfigNameForm extends LitElement { @property() private _name!: ConfigUpdateValues["location_name"]; - protected render(): TemplateResult | void { + protected render(): TemplateResult { const canEdit = ["storage", "default"].includes( this.hass.config.config_source ); diff --git a/src/panels/config/customize/ha-config-customize.js b/src/panels/config/customize/ha-config-customize.js index 51a60b4909..abd812191f 100644 --- a/src/panels/config/customize/ha-config-customize.js +++ b/src/panels/config/customize/ha-config-customize.js @@ -1,10 +1,8 @@ -import "@polymer/app-layout/app-header-layout/app-header-layout"; -import "@polymer/app-layout/app-header/app-header"; -import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/paper-icon-button/paper-icon-button"; import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; +import "../../../layouts/hass-tabs-subpage"; import "../../../resources/ha-style"; import "../../../components/ha-paper-icon-button-arrow-prev"; @@ -17,26 +15,28 @@ import { computeStateDomain } from "../../../common/entity/compute_state_domain" import { sortStatesByName } from "../../../common/entity/states_sort_by_name"; import LocalizeMixin from "../../../mixins/localize-mixin"; +import { configSections } from "../ha-panel-config"; + /* * @appliesMixin LocalizeMixin */ class HaConfigCustomize extends LocalizeMixin(PolymerElement) { static get template() { return html` - - - - - - -
- [[localize('ui.panel.config.customize.caption')]] -
-
-
+ +
@@ -54,7 +54,7 @@ class HaConfigCustomize extends LocalizeMixin(PolymerElement) {
-
+ `; } @@ -62,7 +62,9 @@ class HaConfigCustomize extends LocalizeMixin(PolymerElement) { return { hass: Object, isWide: Boolean, - + narrow: Boolean, + route: Object, + showAdvanced: Boolean, entities: { type: Array, computed: "computeEntities(hass)", @@ -90,6 +92,10 @@ class HaConfigCustomize extends LocalizeMixin(PolymerElement) { history.back(); } + _computeTabs() { + return configSections.general; + } + computeEntities(hass) { return Object.keys(hass.states) .map((key) => hass.states[key]) diff --git a/src/panels/config/dashboard/ha-config-dashboard.ts b/src/panels/config/dashboard/ha-config-dashboard.ts index c3c3b2dc10..2f3b90ce8b 100644 --- a/src/panels/config/dashboard/ha-config-dashboard.ts +++ b/src/panels/config/dashboard/ha-config-dashboard.ts @@ -15,23 +15,25 @@ import "../../../components/ha-menu-button"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; -import { CloudStatus, CloudStatusLoggedIn } from "../../../data/cloud"; +import { CloudStatus } from "../../../data/cloud"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import "../../../components/ha-card"; +import "../../../components/ha-icon-next"; import "../ha-config-section"; import "./ha-config-navigation"; +import { configSections } from "../ha-panel-config"; @customElement("ha-config-dashboard") class HaConfigDashboard extends LitElement { @property() public hass!: HomeAssistant; @property() public narrow!: boolean; @property() public isWide!: boolean; - @property() public cloudStatus!: CloudStatus; + @property() public cloudStatus?: CloudStatus; @property() public showAdvanced!: boolean; - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html` @@ -40,7 +42,6 @@ class HaConfigDashboard extends LitElement { .hass=${this.hass} .narrow=${this.narrow} > -
${this.hass.localize("panel.config")}
@@ -53,77 +54,45 @@ class HaConfigDashboard extends LitElement { ${this.hass.localize("ui.panel.config.introduction")}
- ${isComponentLoaded(this.hass, "cloud") + ${this.cloudStatus && isComponentLoaded(this.hass, "cloud") ? html` - - - - - ${this.hass.localize("ui.panel.config.cloud.caption")} - ${ - this.cloudStatus.logged_in - ? html` -
- ${this.hass.localize( - "ui.panel.config.cloud.description_login", - "email", - (this.cloudStatus as CloudStatusLoggedIn).email - )} -
- ` - : html` -
- ${this.hass.localize( - "ui.panel.config.cloud.description_features" - )} -
- ` - } -
- -
-
- - ` + + + + ` : ""} - - - - - + ${Object.values(configSections).map( + (section) => html` + + + + ` + )} ${!this.showAdvanced ? html`
${this.hass.localize( - "ui.panel.profile.advanced_mode.hint_enable" + "ui.panel.config.advanced_mode.hint_enable" )} ${this.hass.localize( - "ui.panel.profile.advanced_mode.link_profile_page" + "ui.panel.config.advanced_mode.link_profile_page" )}.
@@ -138,9 +107,15 @@ class HaConfigDashboard extends LitElement { return [ haStyle, css` + app-header { + --app-header-background-color: var(--primary-background-color); + } ha-config-navigation:last-child { margin-bottom: 24px; } + ha-config-section { + margin-top: -20px; + } ha-card { overflow: hidden; } diff --git a/src/panels/config/dashboard/ha-config-navigation.ts b/src/panels/config/dashboard/ha-config-navigation.ts index 3b7ebb889d..3af4194ac9 100644 --- a/src/panels/config/dashboard/ha-config-navigation.ts +++ b/src/panels/config/dashboard/ha-config-navigation.ts @@ -1,6 +1,6 @@ import "@polymer/iron-icon/iron-icon"; import "@polymer/paper-item/paper-item-body"; -import "@polymer/paper-item/paper-item"; +import "@polymer/paper-item/paper-icon-item"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; @@ -16,43 +16,66 @@ import { css, } from "lit-element"; import { HomeAssistant } from "../../../types"; - -export interface ConfigPageNavigation { - page: string; - core?: boolean; - advanced?: boolean; -} +import { CloudStatus, CloudStatusLoggedIn } from "../../../data/cloud"; +import { PageNavigation } from "../../../layouts/hass-tabs-subpage"; @customElement("ha-config-navigation") class HaConfigNavigation extends LitElement { @property() public hass!: HomeAssistant; @property() public showAdvanced!: boolean; - @property() public pages!: ConfigPageNavigation[]; + @property() public pages!: PageNavigation[]; - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html` - - ${this.pages.map(({ page, core, advanced }) => - (core || isComponentLoaded(this.hass, page)) && - (!advanced || this.showAdvanced) - ? html` - - - - ${this.hass.localize(`ui.panel.config.${page}.caption`)} -
- ${this.hass.localize( - `ui.panel.config.${page}.description` - )} -
-
- -
-
- ` - : "" - )} -
+ ${this.pages.map((page) => + (!page.component || + page.core || + isComponentLoaded(this.hass, page.component)) && + (!page.exportOnly || this.showAdvanced) + ? html` + + + + + ${this.hass.localize( + `ui.panel.config.${page.component}.caption` + )} + ${page.component === "cloud" && (page.info as CloudStatus) + ? page.info.logged_in + ? html` +
+ ${this.hass.localize( + "ui.panel.config.cloud.description_login", + "email", + (page.info as CloudStatusLoggedIn).email + )} +
+ ` + : html` +
+ ${this.hass.localize( + "ui.panel.config.cloud.description_features" + )} +
+ ` + : html` +
+ ${this.hass.localize( + `ui.panel.config.${page.component}.description` + )} +
+ `} +
+ +
+
+ ` + : "" + )} `; } @@ -61,6 +84,33 @@ class HaConfigNavigation extends LitElement { a { text-decoration: none; color: var(--primary-text-color); + position: relative; + display: block; + outline: 0; + } + ha-icon, + ha-icon-next { + color: var(--secondary-text-color); + } + .iron-selected paper-item::before, + a:not(.iron-selected):focus::before { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + pointer-events: none; + content: ""; + transition: opacity 15ms linear; + will-change: opacity; + } + a:not(.iron-selected):focus::before { + background-color: currentColor; + opacity: var(--dark-divider-opacity); + } + .iron-selected paper-item:focus::before, + .iron-selected:focus paper-item::before { + opacity: 0.2; } `; } diff --git a/src/panels/config/devices/device-detail/ha-device-automation-card.ts b/src/panels/config/devices/device-detail/ha-device-automation-card.ts index 1cdbe5c6b9..1d512b68a6 100644 --- a/src/panels/config/devices/device-detail/ha-device-automation-card.ts +++ b/src/panels/config/devices/device-detail/ha-device-automation-card.ts @@ -5,12 +5,14 @@ import { DeviceAutomation } from "../../../../data/device_automation"; import "../../../../components/ha-card"; import "../../../../components/ha-chips"; import { showAutomationEditor } from "../../../../data/automation"; +import { showScriptEditor } from "../../../../data/script"; export abstract class HaDeviceAutomationCard< T extends DeviceAutomation > extends LitElement { @property() public hass!: HomeAssistant; @property() public deviceId?: string; + @property() public script = false; @property() public automations: T[] = []; protected headerKey = ""; @@ -46,20 +48,18 @@ export abstract class HaDeviceAutomationCard< return html``; } return html` - -
- ${this.hass.localize(this.headerKey)} -
-
- - this._localizeDeviceAutomation(this.hass, automation) - )} - > - -
-
+

+ ${this.hass.localize(this.headerKey)} +

+
+ + this._localizeDeviceAutomation(this.hass, automation) + )} + > + +
`; } @@ -68,6 +68,10 @@ export abstract class HaDeviceAutomationCard< if (!automation) { return; } + if (this.script) { + showScriptEditor(this, { sequence: [automation] }); + return; + } const data = {}; data[this.type] = [automation]; showAutomationEditor(this, data); diff --git a/src/panels/config/devices/device-detail/ha-device-automation-dialog.ts b/src/panels/config/devices/device-detail/ha-device-automation-dialog.ts new file mode 100644 index 0000000000..d7409e3e4a --- /dev/null +++ b/src/panels/config/devices/device-detail/ha-device-automation-dialog.ts @@ -0,0 +1,184 @@ +import { + LitElement, + html, + css, + CSSResult, + TemplateResult, + property, + customElement, +} from "lit-element"; + +import "../../../../components/ha-dialog"; +import "./ha-device-triggers-card"; +import "./ha-device-conditions-card"; +import "./ha-device-actions-card"; +import { DeviceAutomationDialogParams } from "./show-dialog-device-automation"; +import { HomeAssistant } from "../../../../types"; +import { + DeviceTrigger, + DeviceCondition, + DeviceAction, + fetchDeviceTriggers, + fetchDeviceConditions, + fetchDeviceActions, +} from "../../../../data/device_automation"; + +@customElement("dialog-device-automation") +export class DialogDeviceAutomation extends LitElement { + @property() public hass!: HomeAssistant; + @property() private _triggers: DeviceTrigger[] = []; + @property() private _conditions: DeviceCondition[] = []; + @property() private _actions: DeviceAction[] = []; + @property() private _params?: DeviceAutomationDialogParams; + + public async showDialog(params: DeviceAutomationDialogParams): Promise { + this._params = params; + await this.updateComplete; + } + + protected updated(changedProps): void { + super.updated(changedProps); + + if (!changedProps.has("_params")) { + return; + } + + this._triggers = []; + this._conditions = []; + this._actions = []; + + if (!this._params) { + return; + } + + const { deviceId, script } = this._params; + + fetchDeviceActions(this.hass, deviceId).then( + (actions) => (this._actions = actions) + ); + if (script) { + return; + } + fetchDeviceTriggers(this.hass, deviceId).then( + (triggers) => (this._triggers = triggers) + ); + fetchDeviceConditions(this.hass, deviceId).then( + (conditions) => (this._conditions = conditions) + ); + } + + protected render(): TemplateResult | void { + if (!this._params) { + return html``; + } + + return html` + +
+ ${this._triggers.length || + this._conditions.length || + this._actions.length + ? html` + ${this._triggers.length + ? html` + + ` + : ""} + ${this._conditions.length + ? html` + + ` + : ""} + ${this._actions.length + ? html` + + ` + : ""} + ` + : html``} +
+ + Close + +
+ `; + } + + private _close(): void { + this._params = undefined; + } + + static get styles(): CSSResult[] { + return [ + css` + ha-dialog { + --mdc-dialog-title-ink-color: var(--primary-text-color); + --justify-action-buttons: space-between; + } + @media only screen and (min-width: 600px) { + ha-dialog { + --mdc-dialog-min-width: 600px; + } + } + .form { + padding-bottom: 24px; + } + .location { + display: flex; + } + .location > * { + flex-grow: 1; + min-width: 0; + } + .location > *:first-child { + margin-right: 4px; + } + .location > *:last-child { + margin-left: 4px; + } + ha-location-editor { + margin-top: 16px; + } + ha-user-picker { + margin-top: 16px; + } + mwc-button.warning { + --mdc-theme-primary: var(--google-red-500); + } + .error { + color: var(--google-red-500); + } + a { + color: var(--primary-color); + } + p { + color: var(--primary-text-color); + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-device-automation": DialogDeviceAutomation; + } +} diff --git a/src/panels/config/devices/device-detail/ha-device-card.ts b/src/panels/config/devices/device-detail/ha-device-card.ts index a9b5ad7b89..141e3389d2 100644 --- a/src/panels/config/devices/device-detail/ha-device-card.ts +++ b/src/panels/config/devices/device-detail/ha-device-card.ts @@ -1,5 +1,3 @@ -import "../../../../components/ha-card"; - import { DeviceRegistryEntry, computeDeviceName, @@ -27,59 +25,63 @@ export class HaDeviceCard extends LitElement { protected render(): TemplateResult { return html` - -
-
-
${this.device.model}
-
- ${this.hass.localize( - "ui.panel.config.integrations.config_entry.manuf", - "manufacturer", - this.device.manufacturer - )} -
- ${this.device.area_id - ? html` -
-
- ${this.hass.localize( - "ui.panel.config.integrations.config_entry.area", - "area", - this._computeArea(this.areas, this.device) - )} -
-
- ` - : ""} -
- ${this.device.via_device_id - ? html` +
+ ${this.device.model + ? html` +
${this.device.model}
+ ` + : ""} + ${this.device.manufacturer + ? html` +
+ ${this.hass.localize( + "ui.panel.config.integrations.config_entry.manuf", + "manufacturer", + this.device.manufacturer + )} +
+ ` + : ""} + ${this.device.area_id + ? html` +
${this.hass.localize( - "ui.panel.config.integrations.config_entry.via" - )} - ${this._computeDeviceName( - this.devices, - this.device.via_device_id - )} -
- ` - : ""} - ${this.device.sw_version - ? html` -
- ${this.hass.localize( - "ui.panel.config.integrations.config_entry.firmware", - "version", - this.device.sw_version + "ui.panel.config.integrations.config_entry.area", + "area", + this._computeArea(this.areas, this.device) )}
- ` - : ""} -
- +
+ ` + : ""} + ${this.device.via_device_id + ? html` +
+ ${this.hass.localize( + "ui.panel.config.integrations.config_entry.via" + )} + ${this._computeDeviceName( + this.devices, + this.device.via_device_id + )} +
+ ` + : ""} + ${this.device.sw_version + ? html` +
+ ${this.hass.localize( + "ui.panel.config.integrations.config_entry.firmware", + "version", + this.device.sw_version + )} +
+ ` + : ""} +
`; } diff --git a/src/panels/config/devices/device-detail/ha-device-entities-card.ts b/src/panels/config/devices/device-detail/ha-device-entities-card.ts index abc7d88753..b8540b6309 100644 --- a/src/panels/config/devices/device-detail/ha-device-entities-card.ts +++ b/src/panels/config/devices/device-detail/ha-device-entities-card.ts @@ -6,8 +6,9 @@ import { customElement, css, CSSResult, + queryAll, + PropertyValues, } from "lit-element"; -import { classMap } from "lit-html/directives/class-map"; import { HomeAssistant } from "../../../../types"; @@ -19,15 +20,13 @@ import "@polymer/paper-item/paper-item-body"; import "../../../../components/ha-card"; import "../../../../components/ha-icon"; -import "../../../../components/ha-switch"; -import { showEntityRegistryDetailDialog } from "../../entity_registry/show-dialog-entity-registry-detail"; -import { fireEvent } from "../../../../common/dom/fire_event"; +import { showEntityRegistryDetailDialog } from "../../entities/show-dialog-entity-registry-detail"; import { computeDomain } from "../../../../common/entity/compute_domain"; import { domainIcon } from "../../../../common/entity/domain_icon"; -// tslint:disable-next-line -import { HaSwitch } from "../../../../components/ha-switch"; import { EntityRegistryStateEntry } from "../ha-config-device-page"; import { addEntitiesToLovelaceView } from "../../../lovelace/editor/add-entities-to-view"; +import { createRowElement } from "../../../lovelace/create-element/create-row-element"; +import { LovelaceRow } from "../../../lovelace/entity-rows/types"; @customElement("ha-device-entities-card") export class HaDeviceEntitiesCard extends LitElement { @@ -36,66 +35,61 @@ export class HaDeviceEntitiesCard extends LitElement { @property() public entities!: EntityRegistryStateEntry[]; @property() public narrow!: boolean; @property() private _showDisabled = false; + @queryAll("#entities > *") private _entityRows?: LovelaceRow[]; + + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + if (!changedProps.has("hass")) { + return; + } + this._entityRows?.forEach((element) => { + element.hass = this.hass; + }); + } protected render(): TemplateResult { + const disabledEntities: EntityRegistryStateEntry[] = []; return html` - - - ${this.hass.localize( - "ui.panel.config.entity_registry.picker.show_disabled" - )} - - + ${this.entities.length ? html` - ${this.entities.map((entry: EntityRegistryStateEntry) => { - if (!this._showDisabled && entry.disabled_by) { - return ""; - } - const stateObj = this.hass.states[entry.entity_id]; - return html` - - ${stateObj - ? html` - - ` - : html` - - `} - -
${entry.stateName}
-
${entry.entity_id}
-
-
- ${stateObj - ? html` - - ` - : ""} - -
-
- `; - })} +
+ ${this.entities.map((entry: EntityRegistryStateEntry) => { + if (entry.disabled_by) { + disabledEntities.push(entry); + return ""; + } + return this.hass.states[entry.entity_id] + ? this._renderEntity(entry) + : this._renderEntry(entry); + })} +
+ ${disabledEntities.length + ? !this._showDisabled + ? html` + + ` + : html` + ${disabledEntities.map((entry) => + this._renderEntry(entry) + )} + + ` + : ""}
${this.hass.localize( @@ -119,22 +113,48 @@ export class HaDeviceEntitiesCard extends LitElement { `; } - private _showDisabledChanged(ev: Event) { - this._showDisabled = (ev.target as HaSwitch).checked; + private _toggleShowDisabled() { + this._showDisabled = !this._showDisabled; } - private _openEditEntry(ev: MouseEvent): void { - const entry = (ev.currentTarget! as any).closest("paper-icon-item").entry; + private _renderEntity(entry: EntityRegistryStateEntry): TemplateResult { + const element = createRowElement({ entity: entry.entity_id }); + if (this.hass) { + element.hass = this.hass; + } + // @ts-ignore + element.entry = entry; + element.addEventListener("hass-more-info", (ev) => this._openEditEntry(ev)); + + return html` +
${element}
+ `; + } + + private _renderEntry(entry: EntityRegistryStateEntry): TemplateResult { + return html` + + + +
+ ${entry.stateName || entry.entity_id} +
+
+
+ `; + } + + private _openEditEntry(ev: Event): void { + ev.stopPropagation(); + const entry = (ev.currentTarget! as any).entry; showEntityRegistryDetailDialog(this, { entry, }); } - private _openMoreInfo(ev: MouseEvent) { - const entry = (ev.currentTarget! as any).closest("paper-icon-item").entry; - fireEvent(this, "hass-more-info", { entityId: entry.entity_id }); - } - private _addToLovelaceView(): void { addEntitiesToLovelaceView( this, @@ -158,11 +178,32 @@ export class HaDeviceEntitiesCard extends LitElement { .disabled-entry { color: var(--secondary-text-color); } - state-badge { + #entities > * { + margin: 8px; + } + paper-icon-item { + min-height: 40px; + padding: 0 8px; cursor: pointer; } - paper-icon-item:not(.disabled-entry) paper-item-body { + .name { + font-size: 14px; + } + button.show-more { + color: var(--primary-color); + text-align: left; cursor: pointer; + background: none; + border-width: initial; + border-style: none; + border-color: initial; + border-image: initial; + padding: 16px; + font: inherit; + } + button.show-more:focus { + outline: none; + text-decoration: underline; } `; } diff --git a/src/panels/config/devices/device-detail/show-dialog-device-automation.ts b/src/panels/config/devices/device-detail/show-dialog-device-automation.ts new file mode 100644 index 0000000000..5bd92b0a72 --- /dev/null +++ b/src/panels/config/devices/device-detail/show-dialog-device-automation.ts @@ -0,0 +1,22 @@ +import { fireEvent } from "../../../../common/dom/fire_event"; + +export interface DeviceAutomationDialogParams { + deviceId: string; + script?: boolean; +} + +export const loadDeviceAutomationDialog = () => + import( + /* webpackChunkName: "device-automation-dialog" */ "./ha-device-automation-dialog" + ); + +export const showDeviceAutomationDialog = ( + element: HTMLElement, + detailParams: DeviceAutomationDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-device-automation", + dialogImport: loadDeviceAutomationDialog, + dialogParams: detailParams, + }); +}; diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index 2bc8d4fb0d..c278250d9c 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -9,16 +9,15 @@ import { import memoizeOne from "memoize-one"; -import "../../../layouts/hass-subpage"; +import "@polymer/paper-tooltip/paper-tooltip"; + +import "../../../layouts/hass-tabs-subpage"; import "../../../layouts/hass-error-screen"; import "../ha-config-section"; import "./device-detail/ha-device-card"; -import "./device-detail/ha-device-triggers-card"; -import "./device-detail/ha-device-conditions-card"; -import "./device-detail/ha-device-actions-card"; import "./device-detail/ha-device-entities-card"; -import { HomeAssistant } from "../../../types"; +import { HomeAssistant, Route } from "../../../types"; import { ConfigEntry } from "../../../data/config_entries"; import { EntityRegistryEntry, @@ -33,18 +32,15 @@ import { loadDeviceRegistryDetailDialog, showDeviceRegistryDetailDialog, } from "../../../dialogs/device-registry-detail/show-dialog-device-registry-detail"; - -import { - DeviceTrigger, - DeviceAction, - DeviceCondition, - fetchDeviceTriggers, - fetchDeviceConditions, - fetchDeviceActions, -} from "../../../data/device_automation"; +import "../../../components/ha-icon-next"; import { compare } from "../../../common/string/compare"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { createValidEntityId } from "../../../common/entity/valid_entity_id"; +import { configSections } from "../ha-panel-config"; +import { RelatedResult, findRelated } from "../../../data/search"; +import { SceneEntities, showSceneEditor } from "../../../data/scene"; +import { navigate } from "../../../common/navigate"; +import { showDeviceAutomationDialog } from "./device-detail/show-dialog-device-automation"; export interface EntityRegistryStateEntry extends EntityRegistryEntry { stateName?: string; @@ -58,11 +54,11 @@ export class HaConfigDevicePage extends LitElement { @property() public entities!: EntityRegistryEntry[]; @property() public areas!: AreaRegistryEntry[]; @property() public deviceId!: string; - @property() public narrow!: boolean; + @property({ type: Boolean, reflect: true }) public narrow!: boolean; + @property() public isWide!: boolean; @property() public showAdvanced!: boolean; - @property() private _triggers: DeviceTrigger[] = []; - @property() private _conditions: DeviceCondition[] = []; - @property() private _actions: DeviceAction[] = []; + @property() public route!: Route; + @property() private _related?: RelatedResult; private _device = memoizeOne( ( @@ -95,25 +91,10 @@ export class HaConfigDevicePage extends LitElement { loadDeviceRegistryDetailDialog(); } - protected updated(changedProps): void { + protected updated(changedProps) { super.updated(changedProps); - if (changedProps.has("deviceId")) { - if (this.deviceId) { - fetchDeviceTriggers(this.hass, this.deviceId).then( - (triggers) => (this._triggers = triggers) - ); - fetchDeviceConditions(this.hass, this.deviceId).then( - (conditions) => (this._conditions = conditions) - ); - fetchDeviceActions(this.hass, this.deviceId).then( - (actions) => (this._actions = actions) - ); - } else { - this._triggers = []; - this._conditions = []; - this._actions = []; - } + this._findRelated(); } } @@ -133,76 +114,187 @@ export class HaConfigDevicePage extends LitElement { const entities = this._entities(this.deviceId, this.entities); return html` - + - - ${this.hass.localize("ui.panel.config.devices.info")} - - ${this.hass.localize("ui.panel.config.devices.details")} - - - ${entities.length - ? html` -
+
+
+
+

${device.name_by_user || device.name}

+ +
+ + ${ + entities.length + ? html` + + + ` + : html`` + } +
+
+
+ ${ + this._related?.automation?.length + ? this._related.automation.map((automation) => { + const state = this.hass.states[automation]; + return state + ? html` +
+ + + ${state.attributes.friendly_name || + automation} + + + + ${!state.attributes.id + ? html` + ${this.hass.localize( + "ui.panel.config.devices.cant_edit" + )} + + ` + : ""} +
+ ` + : ""; + }) + : html` + ${this.hass.localize( + "ui.panel.config.devices.automation.no_automations" + )} + ` + } +
+ ${this.hass.localize( - "ui.panel.config.devices.entities.entities" + "ui.panel.config.devices.automation.create" )} +
- - - ` - : html``} - ${this._triggers.length || - this._conditions.length || - this._actions.length - ? html` -
- ${this.hass.localize("ui.panel.config.devices.automations")} + +
+
+ ${ + this._related?.scene?.length + ? this._related.scene.map((scene) => { + const state = this.hass.states[scene]; + return state + ? html` +
+ + + ${state.attributes.friendly_name || scene} + + + + ${!state.attributes.id + ? html` + ${this.hass.localize( + "ui.panel.config.devices.cant_edit" + )} + + ` + : ""} +
+ ` + : ""; + }) + : html` + ${this.hass.localize( + "ui.panel.config.devices.scene.no_scenes" + )} + ` + } +
+ + ${this.hass.localize( + "ui.panel.config.devices.scene.create" + )}
- ${this._triggers.length - ? html` - - ` - : ""} - ${this._conditions.length - ? html` - - ` - : ""} - ${this._actions.length - ? html` - - ` - : ""} - ` - : html``} +
+ ${ + this._related?.script?.length + ? this._related.script.map((script) => { + const state = this.hass.states[script]; + return state + ? html` + + + ${state.attributes.friendly_name || script} + + + + ` + : ""; + }) + : html` + + ${this.hass.localize( + "ui.panel.config.devices.script.no_scripts" + )} + ` + } +
+ + ${this.hass.localize("ui.panel.config.devices.script.create")} + +
+
+
+
+
- - `; + `; } private _computeEntityName(entity) { @@ -213,6 +305,50 @@ export class HaConfigDevicePage extends LitElement { return state ? computeStateName(state) : null; } + private async _findRelated() { + this._related = await findRelated(this.hass, "device", this.deviceId); + } + + private _createScene() { + const entities: SceneEntities = {}; + this._entities(this.deviceId, this.entities).forEach((entity) => { + entities[entity.entity_id] = ""; + }); + showSceneEditor(this, { + entities, + }); + } + + private _openScene(ev: Event) { + const state = (ev.currentTarget as any).scene; + if (state.attributes.id) { + navigate(this, `/config/scene/edit/${state.attributes.id}`); + } + } + + private _openScript(ev: Event) { + const script = (ev.currentTarget as any).script; + navigate(this, `/config/script/edit/${script}`); + } + + private _openAutomation(ev: Event) { + const state = (ev.currentTarget as any).automation; + if (state.attributes.id) { + navigate(this, `/config/automation/edit/${state.attributes.id}`); + } + } + + private _showScriptDialog() { + showDeviceAutomationDialog(this, { deviceId: this.deviceId, script: true }); + } + + private _showAutomationDialog() { + showDeviceAutomationDialog(this, { + deviceId: this.deviceId, + script: false, + }); + } + private async _showSettings() { const device = this._device(this.deviceId, this.devices)!; showDeviceRegistryDetailDialog(this, { @@ -275,20 +411,81 @@ export class HaConfigDevicePage extends LitElement { static get styles(): CSSResult { return css` - .header { - font-family: var(--paper-font-display1_-_font-family); + .container { + display: flex; + flex-wrap: wrap; + margin: auto; + max-width: 1000px; + margin-top: 32px; + margin-bottom: 32px; + } + + .device-info { + padding: 16px; + } + + .show-more { + } + + h1 { + margin-top: 0; + font-family: var(--paper-font-headline_-_font-family); -webkit-font-smoothing: var( - --paper-font-display1_-_-webkit-font-smoothing + --paper-font-headline_-_-webkit-font-smoothing ); - font-size: var(--paper-font-display1_-_font-size); - font-weight: var(--paper-font-display1_-_font-weight); - letter-spacing: var(--paper-font-display1_-_letter-spacing); - line-height: var(--paper-font-display1_-_line-height); + font-size: var(--paper-font-headline_-_font-size); + font-weight: var(--paper-font-headline_-_font-weight); + letter-spacing: var(--paper-font-headline_-_letter-spacing); + line-height: var(--paper-font-headline_-_line-height); opacity: var(--dark-primary-opacity); } - ha-config-section *:last-child { - padding-bottom: 24px; + .left, + .column, + .fullwidth { + padding: 8px; + box-sizing: border-box; + } + + .left { + width: 33.33%; + padding-bottom: 0; + } + + .right { + width: 66.66%; + display: flex; + flex-wrap: wrap; + } + + .fullwidth { + width: 100%; + } + + .column { + width: 50%; + } + + .column > *:not(:first-child) { + margin-top: 16px; + } + + :host([narrow]) .left, + :host([narrow]) .right, + :host([narrow]) .column { + width: 100%; + } + + :host([narrow]) .container { + margin-top: 0; + } + + paper-item { + cursor: pointer; + } + + paper-item.no-link { + cursor: default; } `; } diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts index c60bcabe91..e60b990e92 100644 --- a/src/panels/config/devices/ha-config-devices-dashboard.ts +++ b/src/panels/config/devices/ha-config-devices-dashboard.ts @@ -1,4 +1,4 @@ -import "../../../layouts/hass-subpage"; +import "../../../layouts/hass-tabs-subpage"; import "./ha-devices-data-table"; import { @@ -10,26 +10,33 @@ import { CSSResult, css, } from "lit-element"; -import { HomeAssistant } from "../../../types"; +import { HomeAssistant, Route } from "../../../types"; import { DeviceRegistryEntry } from "../../../data/device_registry"; import { EntityRegistryEntry } from "../../../data/entity_registry"; import { ConfigEntry } from "../../../data/config_entries"; import { AreaRegistryEntry } from "../../../data/area_registry"; +import { configSections } from "../ha-panel-config"; @customElement("ha-config-devices-dashboard") export class HaConfigDeviceDashboard extends LitElement { @property() public hass!: HomeAssistant; @property() public narrow = false; + @property() public isWide = false; @property() public devices!: DeviceRegistryEntry[]; @property() public entries!: ConfigEntry[]; @property() public entities!: EntityRegistryEntry[]; @property() public areas!: AreaRegistryEntry[]; @property() public domain!: string; + @property() public route!: Route; protected render(): TemplateResult { return html` -
-
+ `; } diff --git a/src/panels/config/devices/ha-config-devices.ts b/src/panels/config/devices/ha-config-devices.ts index f0de595083..2db8938019 100644 --- a/src/panels/config/devices/ha-config-devices.ts +++ b/src/panels/config/devices/ha-config-devices.ts @@ -28,6 +28,7 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket"; class HaConfigDevices extends HassRouterPage { @property() public hass!: HomeAssistant; @property() public narrow!: boolean; + @property() public isWide!: boolean; @property() public showAdvanced!: boolean; protected routerOptions: RouterOptions = { @@ -97,7 +98,9 @@ class HaConfigDevices extends HassRouterPage { pageEl.devices = this._deviceRegistryEntries; pageEl.areas = this._areas; pageEl.narrow = this.narrow; + pageEl.isWide = this.isWide; pageEl.showAdvanced = this.showAdvanced; + pageEl.route = this.routeTail; } private _loadData() { diff --git a/src/panels/config/devices/ha-devices-data-table.ts b/src/panels/config/devices/ha-devices-data-table.ts index 3ae4037320..fbed2b5365 100644 --- a/src/panels/config/devices/ha-devices-data-table.ts +++ b/src/panels/config/devices/ha-devices-data-table.ts @@ -145,7 +145,7 @@ export class HaDevicesDataTable extends LitElement { return html` ${name}
${device.area} | ${device.integration}
- ${battery + ${battery && !isNaN(battery.state as any) ? html` ${battery.state}% { + this._params = params; + await this.updateComplete; + } + + public closeDialog(): void { + this._params = undefined; + } + + protected render(): TemplateResult { + if (!this._params) { + return html``; + } + const entry = this._params.entry; + const stateObj: HassEntity | undefined = this.hass.states[entry.entity_id]; + + return html` + + + +
+ ${stateObj + ? computeStateName(stateObj) + : entry.name || entry.entity_id} +
+ ${stateObj + ? html` + + ` + : ""} +
+ + + ${this.hass.localize("ui.dialogs.entity_registry.settings")} + + + ${this.hass.localize("ui.dialogs.entity_registry.related")} + + + ${cache( + this._curTab === "tab-settings" + ? html` + + ` + : this._curTab === "tab-related" + ? html` + + + + ` + : html`` + )} +
+ `; + } + + private _handleTabSelected(ev: CustomEvent): void { + if (!ev.detail.value) { + return; + } + this._curTab = ev.detail.value.id; + this._resizeDialog(); + } + + private async _resizeDialog(): Promise { + await this.updateComplete; + fireEvent(this._dialog as HTMLElement, "iron-resize"); + } + + private _openMoreInfo(): void { + fireEvent(this, "hass-more-info", { + entityId: this._params!.entry.entity_id, + }); + this._params = undefined; + } + + private _closeDialog(): void { + this._params = undefined; + } + + private _openedChanged(ev: PolymerChangedEvent): void { + if (!(ev.detail as any).value) { + this._params = undefined; + } + } + + static get styles(): CSSResult[] { + return [ + haStyleDialog, + css` + app-toolbar { + color: var(--primary-text-color); + background-color: var(--secondary-background-color); + margin: 0; + padding: 0 16px; + } + + app-toolbar [main-title] { + /* Design guideline states 24px, changed to 16 to align with state info */ + margin-left: 16px; + line-height: 1.3em; + max-height: 2.6em; + overflow: hidden; + /* webkit and blink still support simple multiline text-overflow */ + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + text-overflow: ellipsis; + } + + @media all and (min-width: 451px) and (min-height: 501px) { + .main-title { + pointer-events: auto; + cursor: default; + } + } + + ha-paper-dialog { + width: 450px; + } + + /* overrule the ha-style-dialog max-height on small screens */ + @media all and (max-width: 450px), all and (max-height: 500px) { + app-toolbar { + background-color: var(--primary-color); + color: var(--text-primary-color); + } + ha-paper-dialog { + height: 100%; + max-height: 100% !important; + width: 100% !important; + border-radius: 0px; + position: fixed !important; + margin: 0; + } + ha-paper-dialog::before { + content: ""; + position: fixed; + z-index: -1; + top: 0px; + left: 0px; + right: 0px; + bottom: 0px; + background-color: inherit; + } + } + + paper-dialog-scrollable { + padding-bottom: 16px; + } + + mwc-button.warning { + --mdc-theme-primary: var(--google-red-500); + } + + :host([rtl]) app-toolbar { + direction: rtl; + text-align: right; + } + :host { + --paper-font-title_-_white-space: normal; + } + paper-tabs { + --paper-tabs-selection-bar-color: var(--primary-color); + text-transform: uppercase; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + margin-top: 0; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-entity-registry-detail": DialogEntityRegistryDetail; + } +} diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts new file mode 100644 index 0000000000..a0090ff6a4 --- /dev/null +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -0,0 +1,241 @@ +import "@polymer/paper-input/paper-input"; +import { HassEntity } from "home-assistant-js-websocket"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + PropertyValues, + TemplateResult, +} from "lit-element"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { computeDomain } from "../../../common/entity/compute_domain"; +import { computeStateName } from "../../../common/entity/compute_state_name"; +import "../../../components/ha-switch"; +// tslint:disable-next-line: no-duplicate-imports +import { HaSwitch } from "../../../components/ha-switch"; +import { + EntityRegistryEntry, + removeEntityRegistryEntry, + updateEntityRegistryEntry, + EntityRegistryEntryUpdateParams, +} from "../../../data/entity_registry"; +import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; +import { PolymerChangedEvent } from "../../../polymer-types"; +import { HomeAssistant } from "../../../types"; + +@customElement("entity-registry-settings") +export class EntityRegistrySettings extends LitElement { + @property() public hass!: HomeAssistant; + @property() public entry!: EntityRegistryEntry; + @property() public dialogElement!: HTMLElement; + @property() private _name!: string; + @property() private _entityId!: string; + @property() private _disabledBy!: string | null; + @property() private _error?: string; + @property() private _submitting?: boolean; + private _origEntityId!: string; + + protected updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + if (changedProperties.has("entry")) { + this._error = undefined; + this._name = this.entry.name || ""; + this._origEntityId = this.entry.entity_id; + this._entityId = this.entry.entity_id; + this._disabledBy = this.entry.disabled_by; + } + } + + protected render(): TemplateResult { + if (this.entry.entity_id !== this._origEntityId) { + return html``; + } + const stateObj: HassEntity | undefined = this.hass.states[ + this.entry.entity_id + ]; + const invalidDomainUpdate = + computeDomain(this._entityId.trim()) !== + computeDomain(this.entry.entity_id); + + return html` + + ${!stateObj + ? html` +
+ ${this.hass!.localize( + "ui.dialogs.entity_registry.editor.unavailable" + )} +
+ ` + : ""} + ${this._error + ? html` +
${this._error}
+ ` + : ""} +
+ + +
+ +
+
+ ${this.hass.localize( + "ui.dialogs.entity_registry.editor.enabled_label" + )} +
+
+ ${this._disabledBy && this._disabledBy !== "user" + ? this.hass.localize( + "ui.dialogs.entity_registry.editor.enabled_cause", + "cause", + this.hass.localize( + `config_entry.disabled_by.${this._disabledBy}` + ) + ) + : ""} + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.enabled_description" + )} +
${this.hass.localize( + "ui.dialogs.entity_registry.editor.note" + )} +
+
+
+
+
+
+
+ + ${this.hass.localize("ui.dialogs.entity_registry.editor.delete")} + + + ${this.hass.localize("ui.dialogs.entity_registry.editor.update")} + +
+ `; + } + + private _nameChanged(ev: PolymerChangedEvent): void { + this._error = undefined; + this._name = ev.detail.value; + } + + private _entityIdChanged(ev: PolymerChangedEvent): void { + this._error = undefined; + this._entityId = ev.detail.value; + } + + private async _updateEntry(): Promise { + this._submitting = true; + const params: Partial = { + name: this._name.trim() || null, + new_entity_id: this._entityId.trim(), + }; + if (this._disabledBy === null || this._disabledBy === "user") { + params.disabled_by = this._disabledBy; + } + try { + await updateEntityRegistryEntry(this.hass!, this._origEntityId, params); + fireEvent(this as HTMLElement, "close-dialog"); + } catch (err) { + this._error = err.message || "Unknown error"; + } finally { + this._submitting = false; + } + } + + private async _deleteEntry(): Promise { + this._submitting = true; + + try { + await removeEntityRegistryEntry(this.hass!, this._entityId); + fireEvent(this as HTMLElement, "close-dialog"); + } finally { + this._submitting = false; + } + } + + private _confirmDeleteEntry(): void { + showConfirmationDialog(this, { + text: this.hass.localize( + "ui.dialogs.entity_registry.editor.confirm_delete" + ), + confirm: () => this._deleteEntry(), + }); + } + + private _disabledByChanged(ev: Event): void { + this._disabledBy = (ev.target as HaSwitch).checked ? null : "user"; + } + + static get styles(): CSSResult { + return css` + :host { + display: block; + margin-bottom: 0 !important; + padding: 0 !important; + } + .form { + padding-bottom: 24px; + } + .buttons { + display: flex; + justify-content: flex-end; + padding: 8px; + } + mwc-button.warning { + margin-right: auto; + --mdc-theme-primary: var(--google-red-500); + } + .error { + color: var(--google-red-500); + } + .row { + margin-top: 8px; + color: var(--primary-text-color); + } + .secondary { + color: var(--secondary-text-color); + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "entity-registry-settings": EntityRegistrySettings; + } +} diff --git a/src/panels/config/entity_registry/ha-config-entity-registry.ts b/src/panels/config/entities/ha-config-entities.ts similarity index 75% rename from src/panels/config/entity_registry/ha-config-entity-registry.ts rename to src/panels/config/entities/ha-config-entities.ts index 3b3fec5eff..f16cfd5fb2 100644 --- a/src/panels/config/entity_registry/ha-config-entity-registry.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -1,63 +1,66 @@ -import { - LitElement, - TemplateResult, - html, - css, - CSSResult, - property, - query, -} from "lit-element"; -import { styleMap } from "lit-html/directives/style-map"; - 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 { HomeAssistant } from "../../../types"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { - EntityRegistryEntry, - computeEntityRegistryName, - subscribeEntityRegistry, - removeEntityRegistryEntry, - updateEntityRegistryEntry, -} from "../../../data/entity_registry"; -import "../../../layouts/hass-subpage"; -import "../../../layouts/hass-loading-screen"; -import "../../../components/data-table/ha-data-table"; -import "../../../components/ha-icon"; + css, + CSSResult, + customElement, + html, + LitElement, + property, + query, + TemplateResult, +} from "lit-element"; +import { styleMap } from "lit-html/directives/style-map"; +import memoize from "memoize-one"; +import { computeDomain } from "../../../common/entity/compute_domain"; import { domainIcon } from "../../../common/entity/domain_icon"; import { stateIcon } from "../../../common/entity/state_icon"; -import { computeDomain } from "../../../common/entity/compute_domain"; -import { - showEntityRegistryDetailDialog, - loadEntityRegistryDetailDialog, -} from "./show-dialog-entity-registry-detail"; -import { UnsubscribeFunc } from "home-assistant-js-websocket"; -import memoize from "memoize-one"; +import "../../../components/data-table/ha-data-table"; // tslint:disable-next-line import { DataTableColumnContainer, + DataTableColumnData, + HaDataTable, RowClickedEvent, SelectionChangedEvent, - HaDataTable, - DataTableColumnData, } from "../../../components/data-table/ha-data-table"; -import { showConfirmationDialog } from "../../../dialogs/confirmation/show-dialog-confirmation"; +import "../../../components/ha-icon"; +import { + computeEntityRegistryName, + EntityRegistryEntry, + removeEntityRegistryEntry, + subscribeEntityRegistry, + updateEntityRegistryEntry, +} from "../../../data/entity_registry"; +import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; +import "../../../layouts/hass-loading-screen"; +import "../../../layouts/hass-tabs-subpage"; +import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; +import { HomeAssistant, Route } from "../../../types"; +import { DialogEntityRegistryDetail } from "./dialog-entity-registry-detail"; +import { + loadEntityRegistryDetailDialog, + showEntityRegistryDetailDialog, +} from "./show-dialog-entity-registry-detail"; +import { configSections } from "../ha-panel-config"; -class HaConfigEntityRegistry extends LitElement { +@customElement("ha-config-entities") +export class HaConfigEntities extends SubscribeMixin(LitElement) { @property() public hass!: HomeAssistant; @property() public isWide!: boolean; @property() public narrow!: boolean; + @property() public route!: Route; @property() private _entities?: EntityRegistryEntry[]; @property() private _showDisabled = false; @property() private _showUnavailable = true; @property() private _filter = ""; @property() private _selectedEntities: string[] = []; @query("ha-data-table") private _dataTable!: HaDataTable; - - private _unsubEntities?: UnsubscribeFunc; + private getDialog?: () => DialogEntityRegistryDetail | undefined; private _columns = memoize( (narrow, _language): DataTableColumnContainer => { @@ -71,7 +74,7 @@ class HaConfigEntityRegistry extends LitElement { }, name: { title: this.hass.localize( - "ui.panel.config.entity_registry.picker.headers.name" + "ui.panel.config.entities.picker.headers.name" ), sortable: true, filterable: true, @@ -81,7 +84,7 @@ class HaConfigEntityRegistry extends LitElement { const statusColumn: DataTableColumnData = { title: this.hass.localize( - "ui.panel.config.entity_registry.picker.headers.status" + "ui.panel.config.entities.picker.headers.status" ), type: "icon", sortable: true, @@ -104,10 +107,10 @@ class HaConfigEntityRegistry extends LitElement { ${entity.unavailable ? this.hass.localize( - "ui.panel.config.entity_registry.picker.status.unavailable" + "ui.panel.config.entities.picker.status.unavailable" ) : this.hass.localize( - "ui.panel.config.entity_registry.picker.status.disabled" + "ui.panel.config.entities.picker.status.disabled" )}
@@ -130,14 +133,14 @@ class HaConfigEntityRegistry extends LitElement { columns.entity_id = { title: this.hass.localize( - "ui.panel.config.entity_registry.picker.headers.entity_id" + "ui.panel.config.entities.picker.headers.entity_id" ), sortable: true, filterable: true, }; columns.platform = { title: this.hass.localize( - "ui.panel.config.entity_registry.picker.headers.integration" + "ui.panel.config.entities.picker.headers.integration" ), sortable: true, filterable: true, @@ -181,61 +184,72 @@ class HaConfigEntityRegistry extends LitElement { unavailable, status: unavailable ? this.hass.localize( - "ui.panel.config.entity_registry.picker.status.unavailable" + "ui.panel.config.entities.picker.status.unavailable" ) : entry.disabled_by ? this.hass.localize( - "ui.panel.config.entity_registry.picker.status.disabled" + "ui.panel.config.entities.picker.status.disabled" ) - : this.hass.localize( - "ui.panel.config.entity_registry.picker.status.ok" - ), + : this.hass.localize("ui.panel.config.entities.picker.status.ok"), }); return result; }, [] as any); } ); - public disconnectedCallback() { - super.disconnectedCallback(); - if (this._unsubEntities) { - this._unsubEntities(); - } + public hassSubscribe(): UnsubscribeFunc[] { + return [ + subscribeEntityRegistry(this.hass.connection!, (entities) => { + this._entities = entities; + }), + ]; } - protected render(): TemplateResult | void { + public disconnectedCallback() { + super.disconnectedCallback(); + if (!this.getDialog) { + return; + } + const dialog = this.getDialog(); + if (!dialog) { + return; + } + dialog.closeDialog(); + } + + protected render(): TemplateResult { if (!this.hass || this._entities === undefined) { return html` `; } return html` -

- ${this.hass.localize( - "ui.panel.config.entity_registry.picker.header" - )} + ${this.hass.localize("ui.panel.config.entities.picker.header")}

${this.hass.localize( - "ui.panel.config.entity_registry.picker.introduction" + "ui.panel.config.entities.picker.introduction" )}

${this.hass.localize( - "ui.panel.config.entity_registry.picker.introduction2" + "ui.panel.config.entities.picker.introduction2" )}

${this.hass.localize( - "ui.panel.config.entity_registry.picker.integrations_page" + "ui.panel.config.entities.picker.integrations_page" )}
@@ -257,7 +271,7 @@ class HaConfigEntityRegistry extends LitElement { ? html`

${this.hass.localize( - "ui.panel.config.entity_registry.picker.selected", + "ui.panel.config.entities.picker.selected", "number", this._selectedEntities.length )} @@ -267,17 +281,17 @@ class HaConfigEntityRegistry extends LitElement { ? html` ${this.hass.localize( - "ui.panel.config.entity_registry.picker.enable_selected.button" + "ui.panel.config.entities.picker.enable_selected.button" )} ${this.hass.localize( - "ui.panel.config.entity_registry.picker.disable_selected.button" + "ui.panel.config.entities.picker.disable_selected.button" )} ${this.hass.localize( - "ui.panel.config.entity_registry.picker.remove_selected.button" + "ui.panel.config.entities.picker.remove_selected.button" )} ` @@ -289,7 +303,7 @@ class HaConfigEntityRegistry extends LitElement { > ${this.hass.localize( - "ui.panel.config.entity_registry.picker.enable_selected.button" + "ui.panel.config.entities.picker.enable_selected.button" )} ${this.hass.localize( - "ui.panel.config.entity_registry.picker.disable_selected.button" + "ui.panel.config.entities.picker.disable_selected.button" )} ${this.hass.localize( - "ui.panel.config.entity_registry.picker.remove_selected.button" + "ui.panel.config.entities.picker.remove_selected.button" )} `} @@ -323,31 +337,31 @@ class HaConfigEntityRegistry extends LitElement { - + ${this.hass!.localize( - "ui.panel.config.entity_registry.picker.filter.show_disabled" + "ui.panel.config.entities.picker.filter.show_disabled" )} - + ${this.hass!.localize( - "ui.panel.config.entity_registry.picker.filter.show_unavailable" + "ui.panel.config.entities.picker.filter.show_unavailable" )} @@ -356,7 +370,7 @@ class HaConfigEntityRegistry extends LitElement {

-
+ `; } @@ -365,18 +379,6 @@ class HaConfigEntityRegistry extends LitElement { loadEntityRegistryDetailDialog(); } - protected updated(changedProps) { - super.updated(changedProps); - if (!this._unsubEntities) { - this._unsubEntities = subscribeEntityRegistry( - this.hass.connection, - (entities) => { - this._entities = entities; - } - ); - } - } - private _showDisabledChanged() { this._showDisabled = !this._showDisabled; } @@ -404,15 +406,15 @@ class HaConfigEntityRegistry extends LitElement { private _enableSelected() { showConfirmationDialog(this, { title: this.hass.localize( - "ui.panel.config.entity_registry.picker.enable_selected.confirm_title", + "ui.panel.config.entities.picker.enable_selected.confirm_title", "number", this._selectedEntities.length ), text: this.hass.localize( - "ui.panel.config.entity_registry.picker.enable_selected.confirm_text" + "ui.panel.config.entities.picker.enable_selected.confirm_text" ), - confirmBtnText: this.hass.localize("ui.common.yes"), - cancelBtnText: this.hass.localize("ui.common.no"), + confirmText: this.hass.localize("ui.common.yes"), + dismissText: this.hass.localize("ui.common.no"), confirm: () => { this._selectedEntities.forEach((entity) => updateEntityRegistryEntry(this.hass, entity, { @@ -427,15 +429,15 @@ class HaConfigEntityRegistry extends LitElement { private _disableSelected() { showConfirmationDialog(this, { title: this.hass.localize( - "ui.panel.config.entity_registry.picker.disable_selected.confirm_title", + "ui.panel.config.entities.picker.disable_selected.confirm_title", "number", this._selectedEntities.length ), text: this.hass.localize( - "ui.panel.config.entity_registry.picker.disable_selected.confirm_text" + "ui.panel.config.entities.picker.disable_selected.confirm_text" ), - confirmBtnText: this.hass.localize("ui.common.yes"), - cancelBtnText: this.hass.localize("ui.common.no"), + confirmText: this.hass.localize("ui.common.yes"), + dismissText: this.hass.localize("ui.common.no"), confirm: () => { this._selectedEntities.forEach((entity) => updateEntityRegistryEntry(this.hass, entity, { @@ -454,15 +456,15 @@ class HaConfigEntityRegistry extends LitElement { }); showConfirmationDialog(this, { title: this.hass.localize( - "ui.panel.config.entity_registry.picker.remove_selected.confirm_title", + "ui.panel.config.entities.picker.remove_selected.confirm_title", "number", removeableEntities.length ), text: this.hass.localize( - "ui.panel.config.entity_registry.picker.remove_selected.confirm_text" + "ui.panel.config.entities.picker.remove_selected.confirm_text" ), - confirmBtnText: this.hass.localize("ui.common.yes"), - cancelBtnText: this.hass.localize("ui.common.no"), + confirmText: this.hass.localize("ui.common.yes"), + dismissText: this.hass.localize("ui.common.no"), confirm: () => { removeableEntities.forEach((entity) => removeEntityRegistryEntry(this.hass, entity) @@ -484,26 +486,30 @@ class HaConfigEntityRegistry extends LitElement { if (!entry) { return; } - showEntityRegistryDetailDialog(this, { + this.getDialog = showEntityRegistryDetailDialog(this, { entry, }); } static get styles(): CSSResult { return css` + hass-loading-screen { + --app-header-background-color: var(--sidebar-background-color); + --app-header-text-color: var(--sidebar-text-color); + } a { color: var(--primary-color); } h2 { margin-top: 0; - font-family: var(--paper-font-display1_-_font-family); + font-family: var(--paper-font-headline_-_font-family); -webkit-font-smoothing: var( - --paper-font-display1_-_-webkit-font-smoothing + --paper-font-headline_-_-webkit-font-smoothing ); - font-size: var(--paper-font-display1_-_font-size); - font-weight: var(--paper-font-display1_-_font-weight); - letter-spacing: var(--paper-font-display1_-_letter-spacing); - line-height: var(--paper-font-display1_-_line-height); + font-size: var(--paper-font-headline_-_font-size); + font-weight: var(--paper-font-headline_-_font-weight); + letter-spacing: var(--paper-font-headline_-_letter-spacing); + line-height: var(--paper-font-headline_-_line-height); opacity: var(--dark-primary-opacity); } p { @@ -511,10 +517,8 @@ class HaConfigEntityRegistry extends LitElement { -webkit-font-smoothing: var( --paper-font-subhead_-_-webkit-font-smoothing ); - font-size: var(--paper-font-subhead_-_font-size); font-weight: var(--paper-font-subhead_-_font-weight); line-height: var(--paper-font-subhead_-_line-height); - opacity: var(--dark-primary-opacity); } .intro { padding: 24px 16px; @@ -549,5 +553,3 @@ class HaConfigEntityRegistry extends LitElement { `; } } - -customElements.define("ha-config-entity-registry", HaConfigEntityRegistry); diff --git a/src/panels/config/entity_registry/show-dialog-entity-registry-detail.ts b/src/panels/config/entities/show-dialog-entity-registry-detail.ts similarity index 66% rename from src/panels/config/entity_registry/show-dialog-entity-registry-detail.ts rename to src/panels/config/entities/show-dialog-entity-registry-detail.ts index dcf295380e..4549579af4 100644 --- a/src/panels/config/entity_registry/show-dialog-entity-registry-detail.ts +++ b/src/panels/config/entities/show-dialog-entity-registry-detail.ts @@ -1,5 +1,6 @@ import { fireEvent } from "../../../common/dom/fire_event"; import { EntityRegistryEntry } from "../../../data/entity_registry"; +import { DialogEntityRegistryDetail } from "./dialog-entity-registry-detail"; export interface EntityRegistryDetailDialogParams { entry: EntityRegistryEntry; @@ -10,13 +11,22 @@ export const loadEntityRegistryDetailDialog = () => /* webpackChunkName: "entity-registry-detail-dialog" */ "./dialog-entity-registry-detail" ); +const getDialog = () => { + return document + .querySelector("home-assistant")! + .shadowRoot!.querySelector("dialog-entity-registry-detail") as + | DialogEntityRegistryDetail + | undefined; +}; + export const showEntityRegistryDetailDialog = ( element: HTMLElement, systemLogDetailParams: EntityRegistryDetailDialogParams -): void => { +): (() => DialogEntityRegistryDetail | undefined) => { fireEvent(element, "show-dialog", { dialogTag: "dialog-entity-registry-detail", dialogImport: loadEntityRegistryDetailDialog, dialogParams: systemLogDetailParams, }); + return getDialog; }; diff --git a/src/panels/config/entity_registry/dialog-entity-registry-detail.ts b/src/panels/config/entity_registry/dialog-entity-registry-detail.ts deleted file mode 100644 index 5b4a0f32de..0000000000 --- a/src/panels/config/entity_registry/dialog-entity-registry-detail.ts +++ /dev/null @@ -1,260 +0,0 @@ -import { - LitElement, - html, - css, - CSSResult, - TemplateResult, - property, -} from "lit-element"; -import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; -import "@polymer/paper-input/paper-input"; - -import "../../../components/dialog/ha-paper-dialog"; -import "../../../components/ha-switch"; - -import { EntityRegistryDetailDialogParams } from "./show-dialog-entity-registry-detail"; -import { PolymerChangedEvent } from "../../../polymer-types"; -import { haStyleDialog } from "../../../resources/styles"; -import { HomeAssistant } from "../../../types"; -import { HassEntity } from "home-assistant-js-websocket"; -// tslint:disable-next-line: no-duplicate-imports -import { HaSwitch } from "../../../components/ha-switch"; - -import { computeDomain } from "../../../common/entity/compute_domain"; -import { computeStateName } from "../../../common/entity/compute_state_name"; -import { - updateEntityRegistryEntry, - removeEntityRegistryEntry, -} from "../../../data/entity_registry"; -import { showConfirmationDialog } from "../../../dialogs/confirmation/show-dialog-confirmation"; - -class DialogEntityRegistryDetail extends LitElement { - @property() public hass!: HomeAssistant; - @property() private _name!: string; - @property() private _entityId!: string; - @property() private _disabledBy!: string | null; - @property() private _error?: string; - @property() private _params?: EntityRegistryDetailDialogParams; - @property() private _submitting?: boolean; - private _origEntityId!: string; - - public async showDialog( - params: EntityRegistryDetailDialogParams - ): Promise { - this._params = params; - this._error = undefined; - this._name = this._params.entry.name || ""; - this._origEntityId = this._params.entry.entity_id; - this._entityId = this._params.entry.entity_id; - this._disabledBy = this._params.entry.disabled_by; - await this.updateComplete; - } - - protected render(): TemplateResult | void { - if (!this._params) { - return html``; - } - const entry = this._params.entry; - const stateObj: HassEntity | undefined = this.hass.states[entry.entity_id]; - const invalidDomainUpdate = - computeDomain(this._entityId.trim()) !== - computeDomain(this._params.entry.entity_id); - - return html` - -

- ${stateObj - ? computeStateName(stateObj) - : entry.name || entry.entity_id} -

- - ${!stateObj - ? html` -
- ${this.hass!.localize( - "ui.panel.config.entity_registry.editor.unavailable" - )} -
- ` - : ""} - ${this._error - ? html` -
${this._error}
- ` - : ""} -
- - -
- -
-
- ${this.hass.localize( - "ui.panel.config.entity_registry.editor.enabled_label" - )} -
-
- ${this._disabledBy && this._disabledBy !== "user" - ? this.hass.localize( - "ui.panel.config.entity_registry.editor.enabled_cause", - "cause", - this.hass.localize( - `config_entry.disabled_by.${this._disabledBy}` - ) - ) - : ""} - ${this.hass.localize( - "ui.panel.config.entity_registry.editor.enabled_description" - )} -
${this.hass.localize( - "ui.panel.config.entity_registry.editor.note" - )} -
-
-
-
-
-
-
- - ${this.hass.localize( - "ui.panel.config.entity_registry.editor.delete" - )} - - - ${this.hass.localize( - "ui.panel.config.entity_registry.editor.update" - )} - -
-
- `; - } - - private _nameChanged(ev: PolymerChangedEvent): void { - this._error = undefined; - this._name = ev.detail.value; - } - - private _entityIdChanged(ev: PolymerChangedEvent): void { - this._error = undefined; - this._entityId = ev.detail.value; - } - - private async _updateEntry(): Promise { - this._submitting = true; - try { - await updateEntityRegistryEntry(this.hass!, this._origEntityId, { - name: this._name.trim() || null, - disabled_by: this._disabledBy, - new_entity_id: this._entityId.trim(), - }); - this._params = undefined; - } catch (err) { - this._error = err.message || "Unknown error"; - } finally { - this._submitting = false; - } - } - - private async _deleteEntry(): Promise { - this._submitting = true; - - try { - await removeEntityRegistryEntry(this.hass!, this._entityId); - this._params = undefined; - } finally { - this._submitting = false; - } - } - - private _confirmDeleteEntry(): void { - showConfirmationDialog(this, { - text: this.hass.localize( - "ui.panel.config.entity_registry.editor.confirm_delete" - ), - confirm: () => this._deleteEntry(), - }); - } - - private _openedChanged(ev: PolymerChangedEvent): void { - if (!(ev.detail as any).value) { - this._params = undefined; - } - } - private _disabledByChanged(ev: Event): void { - this._disabledBy = (ev.target as HaSwitch).checked ? null : "user"; - } - - static get styles(): CSSResult[] { - return [ - haStyleDialog, - css` - :host { - --paper-font-title_-_white-space: normal; - } - ha-paper-dialog { - min-width: 400px; - max-width: 450px; - } - .form { - padding-bottom: 24px; - } - mwc-button.warning { - margin-right: auto; - } - .error { - color: var(--google-red-500); - } - .row { - margin-top: 8px; - color: var(--primary-text-color); - } - .secondary { - color: var(--secondary-text-color); - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "dialog-entity-registry-detail": DialogEntityRegistryDetail; - } -} - -customElements.define( - "dialog-entity-registry-detail", - DialogEntityRegistryDetail -); diff --git a/src/panels/config/ha-config-section.js b/src/panels/config/ha-config-section.js deleted file mode 100644 index 800eda13c8..0000000000 --- a/src/panels/config/ha-config-section.js +++ /dev/null @@ -1,98 +0,0 @@ -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -import "../../resources/ha-style"; - -class HaConfigSection extends PolymerElement { - static get template() { - return html` - -
-
-
-
-
-
-
- `; - } - - static get properties() { - return { - hass: { - type: Object, - }, - - narrow: { - type: Boolean, - }, - - isWide: { - type: Boolean, - value: false, - }, - }; - } - - computeContentClasses(isWide) { - var classes = "content "; - - return isWide ? classes : classes + "narrow"; - } - - computeClasses(isWide) { - var classes = "together layout "; - - return classes + (isWide ? "horizontal" : "vertical narrow"); - } -} - -customElements.define("ha-config-section", HaConfigSection); diff --git a/src/panels/config/ha-config-section.ts b/src/panels/config/ha-config-section.ts new file mode 100644 index 0000000000..2e4e70db3c --- /dev/null +++ b/src/panels/config/ha-config-section.ts @@ -0,0 +1,110 @@ +import { customElement, LitElement, html, css, property } from "lit-element"; +import { classMap } from "lit-html/directives/class-map"; + +@customElement("ha-config-section") +export class HaConfigSection extends LitElement { + @property() public isWide: boolean = false; + + protected render() { + return html` +
+
+
+
+
+
+
+ `; + } + + static get styles() { + return css` + :host { + display: block; + } + .content { + padding: 28px 20px 0; + max-width: 1040px; + margin: 0 auto; + } + + .layout { + display: flex; + } + + .horizontal { + flex-direction: row; + } + + .vertical { + flex-direction: column; + } + + .flex-auto { + flex: 1 1 auto; + } + + .header { + font-family: var(--paper-font-headline_-_font-family); + -webkit-font-smoothing: var( + --paper-font-headline_-_-webkit-font-smoothing + ); + font-size: var(--paper-font-headline_-_font-size); + font-weight: var(--paper-font-headline_-_font-weight); + letter-spacing: var(--paper-font-headline_-_letter-spacing); + line-height: var(--paper-font-headline_-_line-height); + opacity: var(--dark-primary-opacity); + } + + .together { + margin-top: 32px; + } + + .intro { + font-family: var(--paper-font-subhead_-_font-family); + -webkit-font-smoothing: var( + --paper-font-subhead_-_-webkit-font-smoothing + ); + font-weight: var(--paper-font-subhead_-_font-weight); + line-height: var(--paper-font-subhead_-_line-height); + width: 100%; + max-width: 400px; + margin-right: 40px; + opacity: var(--dark-primary-opacity); + font-size: 14px; + padding-bottom: 20px; + } + + .panel { + margin-top: -24px; + } + + .panel ::slotted(*) { + margin-top: 24px; + display: block; + } + + .narrow.content { + max-width: 640px; + } + .narrow .together { + margin-top: 20px; + } + .narrow .intro { + padding-bottom: 20px; + margin-right: 0; + max-width: 500px; + } + `; + } +} diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index 213977a577..f5c31b369a 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -1,15 +1,18 @@ import { property, PropertyValues, customElement } from "lit-element"; +import "@polymer/paper-item/paper-item-body"; +import "@polymer/paper-item/paper-item"; import "../../layouts/hass-loading-screen"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; -import { HomeAssistant } from "../../types"; +import { HomeAssistant, Route } from "../../types"; import { CloudStatus, fetchCloudStatus } from "../../data/cloud"; import { listenMediaQuery } from "../../common/dom/media_query"; -import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page"; import { - CoreFrontendUserData, getOptimisticFrontendUserDataCollection, + CoreFrontendUserData, } from "../../data/frontend"; +import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page"; import { PolymerElement } from "@polymer/polymer"; +import { PageNavigation } from "../../layouts/hass-tabs-subpage"; declare global { // for fire event @@ -18,21 +21,135 @@ declare global { } } +export const configSections: { [name: string]: PageNavigation[] } = { + integrations: [ + { + component: "integrations", + path: "/config/integrations", + translationKey: "ui.panel.config.integrations.caption", + icon: "hass:puzzle", + core: true, + }, + { + component: "devices", + path: "/config/devices", + translationKey: "ui.panel.config.devices.caption", + icon: "hass:devices", + core: true, + }, + { + component: "entities", + path: "/config/entities", + translationKey: "ui.panel.config.entities.caption", + icon: "hass:shape", + core: true, + }, + { + component: "areas", + path: "/config/areas", + translationKey: "ui.panel.config.areas.caption", + icon: "hass:sofa", + core: true, + }, + ], + automation: [ + { + component: "automation", + path: "/config/automation", + translationKey: "ui.panel.config.automation.caption", + icon: "hass:robot", + }, + { + component: "scene", + path: "/config/scene", + translationKey: "ui.panel.config.scene.caption", + icon: "hass:palette", + }, + { + component: "script", + path: "/config/script", + translationKey: "ui.panel.config.script.caption", + icon: "hass:script-text", + }, + ], + persons: [ + { + component: "person", + path: "/config/person", + translationKey: "ui.panel.config.person.caption", + icon: "hass:account", + }, + { + component: "zone", + path: "/config/zone", + translationKey: "ui.panel.config.zone.caption", + icon: "hass:map-marker-radius", + core: true, + }, + { + component: "users", + path: "/config/users", + translationKey: "ui.panel.config.users.caption", + icon: "hass:account-badge-horizontal", + core: true, + }, + ], + general: [ + { + component: "core", + path: "/config/core", + translationKey: "ui.panel.config.core.caption", + icon: "hass:home-assistant", + core: true, + }, + { + component: "server_control", + path: "/config/server_control", + translationKey: "ui.panel.config.server_control.caption", + icon: "hass:server", + core: true, + }, + { + component: "customize", + path: "/config/customize", + translationKey: "ui.panel.config.customize.caption", + icon: "hass:pencil", + core: true, + exportOnly: true, + }, + ], + other: [ + { + component: "zha", + path: "/config/zha", + translationKey: "ui.panel.config.zha.caption", + icon: "hass:zigbee", + }, + { + component: "zwave", + path: "/config/zwave", + translationKey: "ui.panel.config.zwave.caption", + icon: "hass:z-wave", + }, + ], +}; + @customElement("ha-panel-config") class HaPanelConfig extends HassRouterPage { @property() public hass!: HomeAssistant; @property() public narrow!: boolean; + @property() public route!: Route; protected routerOptions: RouterOptions = { defaultPage: "dashboard", cacheAll: true, preloadAll: true, routes: { - area_registry: { - tag: "ha-config-area-registry", + areas: { + tag: "ha-config-areas", load: () => import( - /* webpackChunkName: "panel-config-area-registry" */ "./area_registry/ha-config-area-registry" + /* webpackChunkName: "panel-config-areas" */ "./areas/ha-config-areas" ), }, automation: { @@ -84,11 +201,11 @@ class HaPanelConfig extends HassRouterPage { /* webpackChunkName: "panel-config-dashboard" */ "./dashboard/ha-config-dashboard" ), }, - entity_registry: { - tag: "ha-config-entity-registry", + entities: { + tag: "ha-config-entities", load: () => import( - /* webpackChunkName: "panel-config-entity-registry" */ "./entity_registry/ha-config-entity-registry" + /* webpackChunkName: "panel-config-entities" */ "./entities/ha-config-entities" ), }, integrations: { @@ -126,6 +243,13 @@ class HaPanelConfig extends HassRouterPage { /* webpackChunkName: "panel-config-users" */ "./users/ha-config-users" ), }, + zone: { + tag: "ha-config-zone", + load: () => + import( + /* webpackChunkName: "panel-config-zone" */ "./zone/ha-config-zone" + ), + }, zha: { tag: "zha-config-dashboard-router", load: () => @@ -146,6 +270,7 @@ class HaPanelConfig extends HassRouterPage { @property() private _wideSidebar: boolean = false; @property() private _wide: boolean = false; @property() private _coreUserData?: CoreFrontendUserData; + @property() private _showAdvanced = false; @property() private _cloudStatus?: CloudStatus; private _listeners: Array<() => void> = []; @@ -168,6 +293,9 @@ class HaPanelConfig extends HassRouterPage { "core" ).subscribe((coreUserData) => { this._coreUserData = coreUserData || {}; + this._showAdvanced = !!( + this._coreUserData && this._coreUserData.showAdvanced + ); }) ); } @@ -187,12 +315,21 @@ class HaPanelConfig extends HassRouterPage { this.addEventListener("ha-refresh-cloud-status", () => this._updateCloudStatus() ); + this.style.setProperty( + "--app-header-background-color", + "var(--sidebar-background-color)" + ); + this.style.setProperty( + "--app-header-text-color", + "var(--sidebar-text-color)" + ); + this.style.setProperty( + "--app-header-border-bottom", + "1px solid var(--divider-color)" + ); } protected updatePageEl(el) { - const showAdvanced = !!( - this._coreUserData && this._coreUserData.showAdvanced - ); const isWide = this.hass.dockedSidebar === "docked" ? this._wideSidebar : this._wide; @@ -201,7 +338,7 @@ class HaPanelConfig extends HassRouterPage { (el as PolymerElement).setProperties({ route: this.routeTail, hass: this.hass, - showAdvanced, + showAdvanced: this._showAdvanced, isWide, narrow: this.narrow, cloudStatus: this._cloudStatus, @@ -209,7 +346,7 @@ class HaPanelConfig extends HassRouterPage { } else { el.route = this.routeTail; el.hass = this.hass; - el.showAdvanced = showAdvanced; + el.showAdvanced = this._showAdvanced; el.isWide = isWide; el.narrow = this.narrow; el.cloudStatus = this._cloudStatus; diff --git a/src/panels/config/integrations/config-entry/ha-config-entry-page.ts b/src/panels/config/integrations/config-entry/ha-config-entry-page.ts index 3dc2dbb3b6..bc2923aed4 100755 --- a/src/panels/config/integrations/config-entry/ha-config-entry-page.ts +++ b/src/panels/config/integrations/config-entry/ha-config-entry-page.ts @@ -17,7 +17,10 @@ import { DeviceRegistryEntry } from "../../../../data/device_registry"; import { AreaRegistryEntry } from "../../../../data/area_registry"; import { fireEvent } from "../../../../common/dom/fire_event"; import { showConfigEntrySystemOptionsDialog } from "../../../../dialogs/config-entry-system-options/show-dialog-config-entry-system-options"; -import { showConfirmationDialog } from "../../../../dialogs/confirmation/show-dialog-confirmation"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../../dialogs/generic/show-dialog-box"; class HaConfigEntryPage extends LitElement { @property() public hass!: HomeAssistant; @@ -177,11 +180,11 @@ class HaConfigEntryPage extends LitElement { deleteConfigEntry(this.hass, this.configEntryId).then((result) => { fireEvent(this, "hass-reload-entries"); if (result.require_restart) { - alert( - this.hass.localize( + showAlertDialog(this, { + text: this.hass.localize( "ui.panel.config.integrations.config_entry.restart_confirm" - ) - ); + ), + }); } navigate(this, "/config/integrations/dashboard", true); }); diff --git a/src/panels/config/integrations/ha-config-entries-dashboard.ts b/src/panels/config/integrations/ha-config-entries-dashboard.ts index 3c7f4c3370..6aa95b96e5 100644 --- a/src/panels/config/integrations/ha-config-entries-dashboard.ts +++ b/src/panels/config/integrations/ha-config-entries-dashboard.ts @@ -12,7 +12,7 @@ import "../../../components/ha-card"; import "../../../components/ha-icon-next"; import "../../../components/ha-fab"; import "../../../components/entity/ha-state-icon"; -import "../../../layouts/hass-subpage"; +import "../../../layouts/hass-tabs-subpage"; import "../../../resources/ha-style"; import "../../../components/ha-icon"; @@ -38,17 +38,21 @@ import { css, CSSResult, } from "lit-element"; -import { HomeAssistant } from "../../../types"; +import { HomeAssistant, Route } from "../../../types"; import { ConfigEntry, deleteConfigEntry } from "../../../data/config_entries"; import { fireEvent } from "../../../common/dom/fire_event"; import { EntityRegistryEntry } from "../../../data/entity_registry"; import { DataEntryFlowProgress } from "../../../data/data_entry_flow"; -import { showConfirmationDialog } from "../../../dialogs/confirmation/show-dialog-confirmation"; +import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; +import { configSections } from "../ha-panel-config"; @customElement("ha-config-entries-dashboard") export class HaConfigManagerDashboard extends LitElement { @property() public hass!: HomeAssistant; @property() public showAdvanced!: boolean; + @property() public isWide!: boolean; + @property() public narrow!: boolean; + @property() public route!: Route; @property() private configEntries!: ConfigEntry[]; @@ -71,8 +75,12 @@ export class HaConfigManagerDashboard extends LitElement { protected render(): TemplateResult { return html` - - + ${this.hass.localize( this._showIgnored ? "ui.panel.config.integrations.ignore.hide_ignored" @@ -245,8 +253,9 @@ export class HaConfigManagerDashboard extends LitElement { title=${this.hass.localize("ui.panel.config.integrations.new")} @click=${this._createFlow} ?rtl=${computeRTL(this.hass!)} + ?narrow=${this.narrow} > - + `; } @@ -275,7 +284,7 @@ export class HaConfigManagerDashboard extends LitElement { text: this.hass!.localize( "ui.panel.config.integrations.ignore.confirm_ignore" ), - confirmBtnText: this.hass!.localize( + confirmText: this.hass!.localize( "ui.panel.config.integrations.ignore.ignore" ), confirm: () => { @@ -300,7 +309,7 @@ export class HaConfigManagerDashboard extends LitElement { text: this.hass!.localize( "ui.panel.config.integrations.ignore.confirm_delete_ignore" ), - confirmBtnText: this.hass!.localize( + confirmText: this.hass!.localize( "ui.panel.config.integrations.ignore.stop_ignore" ), confirm: async () => { @@ -332,6 +341,7 @@ export class HaConfigManagerDashboard extends LitElement { }); return states; } + static get styles(): CSSResult { return css` ha-card { @@ -358,7 +368,9 @@ export class HaConfigManagerDashboard extends LitElement { right: 16px; z-index: 1; } - + ha-fab[narrow] { + bottom: 84px; + } ha-fab[rtl] { right: auto; left: 16px; diff --git a/src/panels/config/integrations/ha-config-integrations.ts b/src/panels/config/integrations/ha-config-integrations.ts index ac30b659a1..511f4e6390 100644 --- a/src/panels/config/integrations/ha-config-integrations.ts +++ b/src/panels/config/integrations/ha-config-integrations.ts @@ -39,6 +39,7 @@ declare global { class HaConfigIntegrations extends HassRouterPage { @property() public hass!: HomeAssistant; @property() public narrow!: boolean; + @property() public isWide!: boolean; @property() public showAdvanced!: boolean; protected routerOptions: RouterOptions = { @@ -101,8 +102,9 @@ class HaConfigIntegrations extends HassRouterPage { pageEl.entityRegistryEntries = this._entityRegistryEntries; pageEl.configEntries = this._configEntries; pageEl.narrow = this.narrow; + pageEl.isWide = this.isWide; pageEl.showAdvanced = this.showAdvanced; - + pageEl.route = this.routeTail; if (this._currentPage === "dashboard") { pageEl.configEntriesInProgress = this._configEntriesInProgress; return; diff --git a/src/panels/config/person/dialog-person-detail.ts b/src/panels/config/person/dialog-person-detail.ts index c124de41ad..10e995cf8f 100644 --- a/src/panels/config/person/dialog-person-detail.ts +++ b/src/panels/config/person/dialog-person-detail.ts @@ -10,10 +10,10 @@ import memoizeOne from "memoize-one"; import "@polymer/paper-input/paper-input"; import "@material/mwc-button"; -import "@material/mwc-dialog"; import "../../../components/entity/ha-entities-picker"; import "../../../components/user/ha-user-picker"; +import "../../../components/ha-dialog"; import { PersonDetailDialogParams } from "./show-dialog-person-detail"; import { PolymerChangedEvent } from "../../../polymer-types"; import { HomeAssistant } from "../../../types"; @@ -50,18 +50,31 @@ class DialogPersonDetail extends LitElement { await this.updateComplete; } - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this._params) { return html``; } const nameInvalid = this._name.trim() === ""; + const title = html` + ${this._params.entry + ? this._params.entry.name + : this.hass!.localize("ui.panel.config.person.detail.new_person")} + + `; return html` -
${this._error @@ -162,7 +175,7 @@ class DialogPersonDetail extends LitElement { ? this.hass!.localize("ui.panel.config.person.detail.update") : this.hass!.localize("ui.panel.config.person.detail.create")} - + `; } @@ -224,9 +237,20 @@ class DialogPersonDetail extends LitElement { static get styles(): CSSResult[] { return [ css` - mwc-dialog { - min-width: 400px; - max-width: 600px; + ha-dialog { + --mdc-dialog-min-width: 400px; + --mdc-dialog-max-width: 600px; + --mdc-dialog-title-ink-color: var(--primary-text-color); + --justify-action-buttons: space-between; + } + /* make dialog fullscreen on small screens */ + @media all and (max-width: 450px), all and (max-height: 500px) { + ha-dialog { + --mdc-dialog-min-width: 100vw; + --mdc-dialog-max-height: 100vh; + --mdc-dialog-shape-radius: 0px; + --vertial-align-dialog: flex-end; + } } .form { padding-bottom: 24px; @@ -243,6 +267,9 @@ class DialogPersonDetail extends LitElement { a { color: var(--primary-color); } + p { + color: var(--primary-text-color); + } `, ]; } diff --git a/src/panels/config/person/ha-config-person.ts b/src/panels/config/person/ha-config-person.ts index c954d035a9..59aa055f0a 100644 --- a/src/panels/config/person/ha-config-person.ts +++ b/src/panels/config/person/ha-config-person.ts @@ -9,7 +9,7 @@ import { import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item-body"; -import { HomeAssistant } from "../../../types"; +import { HomeAssistant, Route } from "../../../types"; import { Person, fetchPersons, @@ -19,7 +19,7 @@ import { } from "../../../data/person"; import "../../../components/ha-card"; import "../../../components/ha-fab"; -import "../../../layouts/hass-subpage"; +import "../../../layouts/hass-tabs-subpage"; import "../../../layouts/hass-loading-screen"; import { compare } from "../../../common/string/compare"; import "../ha-config-section"; @@ -28,15 +28,18 @@ import { loadPersonDetailDialog, } from "./show-dialog-person-detail"; import { User, fetchUsers } from "../../../data/user"; +import { configSections } from "../ha-panel-config"; class HaConfigPerson extends LitElement { @property() public hass?: HomeAssistant; @property() public isWide?: boolean; + @property() public narrow?: boolean; + @property() public route!: Route; @property() private _storageItems?: Person[]; @property() private _configItems?: Person[]; private _usersLoad?: Promise; - protected render(): TemplateResult | void { + protected render(): TemplateResult { if ( !this.hass || this._storageItems === undefined || @@ -48,7 +51,13 @@ class HaConfigPerson extends LitElement { } const hass = this.hass; return html` - + ${hass.localize("ui.panel.config.person.caption")} - + - +
${this.hass.localize("ui.panel.config.scene.picker.header")}
@@ -70,8 +77,7 @@ class HaSceneDashboard extends LitElement { ` : this.scenes.map( (scene) => html` - -
+ ` )} - - + `; } @@ -150,13 +154,11 @@ class HaSceneDashboard extends LitElement { css` :host { display: block; - } - - hass-subpage { - min-height: 100vh; + height: 100%; } ha-card { + padding-bottom: 8px; margin-bottom: 56px; } @@ -171,6 +173,10 @@ class HaSceneDashboard extends LitElement { color: var(--primary-text-color); } + .actions { + display: flex; + } + ha-entity-toggle { margin-right: 16px; } @@ -186,7 +192,9 @@ class HaSceneDashboard extends LitElement { bottom: 24px; right: 24px; } - + ha-fab[narrow] { + bottom: 84px; + } ha-fab[rtl] { right: auto; left: 16px; diff --git a/src/panels/config/scene/ha-scene-editor.ts b/src/panels/config/scene/ha-scene-editor.ts index a1407ee4f6..f63dce96b9 100644 --- a/src/panels/config/scene/ha-scene-editor.ts +++ b/src/panels/config/scene/ha-scene-editor.ts @@ -26,7 +26,7 @@ import "../../../layouts/ha-app-layout"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { haStyle } from "../../../resources/styles"; -import { HomeAssistant } from "../../../types"; +import { HomeAssistant, Route } from "../../../types"; import { navigate } from "../../../common/navigate"; import { computeRTL } from "../../../common/util/compute_rtl"; import { @@ -39,6 +39,7 @@ import { SceneEntities, applyScene, activateScene, + getSceneEditorInitData, } from "../../../data/scene"; import { fireEvent } from "../../../common/dom/fire_event"; import { @@ -54,7 +55,8 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import memoizeOne from "memoize-one"; import { computeDomain } from "../../../common/entity/compute_domain"; import { HassEvent } from "home-assistant-js-websocket"; -import { showConfirmationDialog } from "../../../dialogs/confirmation/show-dialog-confirmation"; +import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; +import { configSections } from "../ha-panel-config"; interface DeviceEntities { id: string; @@ -69,7 +71,9 @@ interface DeviceEntitiesLookup { @customElement("ha-scene-editor") export class HaSceneEditor extends SubscribeMixin(LitElement) { @property() public hass!: HomeAssistant; - @property() public narrow?: boolean; + @property() public narrow!: boolean; + @property() public isWide!: boolean; + @property() public route!: Route; @property() public scene?: SceneEntity; @property() public creatingNew?: boolean; @property() public showAdvanced!: boolean; @@ -146,9 +150,9 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) { ]; } - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this.hass) { - return; + return html``; } const { devices, entities } = this._getEntitiesDevices( this._entities, @@ -157,52 +161,51 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) { this._deviceRegistryEntries ); return html` - - - - -
- ${this.scene - ? computeStateName(this.scene) - : this.hass.localize( - "ui.panel.config.scene.editor.default_name" - )} -
- ${this.creatingNew - ? "" - : html` - - `} -
-
+ this._backTapped()} + .tabs=${configSections.automation} + > -
- ${this._errors - ? html` -
${this._errors}
- ` - : ""} + ${ + this.creatingNew + ? "" + : html` + + ` + } + + ${ + this._errors + ? html` +
${this._errors}
+ ` + : "" + }
- +
- ${this.scene - ? computeStateName(this.scene) - : this.hass.localize( - "ui.panel.config.scene.editor.default_name" - )} + ${ + this.scene + ? computeStateName(this.scene) + : this.hass.localize( + "ui.panel.config.scene.editor.default_name" + ) + }
${this.hass.localize( @@ -222,7 +225,7 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) { - +
${this.hass.localize( "ui.panel.config.scene.editor.devices.header" @@ -291,87 +294,88 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) { - ${this.showAdvanced - ? html` - -
- ${this.hass.localize( - "ui.panel.config.scene.editor.entities.header" - )} -
-
- ${this.hass.localize( - "ui.panel.config.scene.editor.entities.introduction" - )} -
- ${entities.length - ? html` - - ${entities.map((entityId) => { - const stateObj = this.hass.states[entityId]; - if (!stateObj) { - return html``; - } - return html` - - - - ${computeStateName(stateObj)} - - - - `; - })} - - ` - : ""} - - -
+ ${ + this.showAdvanced + ? html` + +
${this.hass.localize( - "ui.panel.config.scene.editor.entities.device_entities" + "ui.panel.config.scene.editor.entities.header" )} -
- -
- ` - : ""} +
+ ${this.hass.localize( + "ui.panel.config.scene.editor.entities.introduction" + )} +
+ ${entities.length + ? html` + + ${entities.map((entityId) => { + const stateObj = this.hass.states[entityId]; + if (!stateObj) { + return html``; + } + return html` + + + + ${computeStateName(stateObj)} + + + + `; + })} + + ` + : ""} + + +
+ ${this.hass.localize( + "ui.panel.config.scene.editor.entities.device_entities" + )} + +
+
+ + ` + : "" + }
-
{ - this._storeState(entity); - }); - - const filteredEntityReg = this._entityRegistryEntries.filter((entityReg) => - this._entities.includes(entityReg.entity_id) - ); - - for (const entityReg of filteredEntityReg) { - if (!entityReg.device_id) { - continue; - } - if (!this._devices.includes(entityReg.device_id)) { - this._devices = [...this._devices, entityReg.device_id]; - } - } + this._initEntities(config); const { context } = await activateScene(this.hass, this.scene!.entity_id); @@ -486,6 +480,25 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) { this._config = config; } + private _initEntities(config: SceneConfig) { + this._entities = Object.keys(config.entities); + this._entities.forEach((entity) => this._storeState(entity)); + + const filteredEntityReg = this._entityRegistryEntries.filter((entityReg) => + this._entities.includes(entityReg.entity_id) + ); + this._devices = []; + + for (const entityReg of filteredEntityReg) { + if (!entityReg.device_id) { + continue; + } + if (!this._devices.includes(entityReg.device_id)) { + this._devices = [...this._devices, entityReg.device_id]; + } + } + } + private _entityPicked(ev: CustomEvent) { const entityId = ev.detail.value; (ev.target as any).value = ""; @@ -560,8 +573,8 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) { text: this.hass!.localize( "ui.panel.config.scene.editor.unsaved_confirm" ), - confirmBtnText: this.hass!.localize("ui.common.yes"), - cancelBtnText: this.hass!.localize("ui.common.no"), + confirmText: this.hass!.localize("ui.common.yes"), + dismissText: this.hass!.localize("ui.common.no"), confirm: () => this._goBack(), }); } else { @@ -577,8 +590,8 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) { private _deleteTapped(): void { showConfirmationDialog(this, { text: this.hass!.localize("ui.panel.config.scene.picker.delete_confirm"), - confirmBtnText: this.hass!.localize("ui.common.yes"), - cancelBtnText: this.hass!.localize("ui.common.no"), + confirmText: this.hass!.localize("ui.common.yes"), + dismissText: this.hass!.localize("ui.common.no"), confirm: () => this._delete(), }); } @@ -706,7 +719,10 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) { bottom: 24px; right: 24px; } - + ha-fab[narrow] { + bottom: 84px; + margin-bottom: -140px; + } ha-fab[dirty] { margin-bottom: 0; } diff --git a/src/panels/config/script/ha-config-script.js b/src/panels/config/script/ha-config-script.js index a0e7fcf1ed..08e809219e 100644 --- a/src/panels/config/script/ha-config-script.js +++ b/src/panels/config/script/ha-config-script.js @@ -34,6 +34,8 @@ class HaConfigScript extends PolymerElement { hass="[[hass]]" scripts="[[scripts]]" is-wide="[[isWide]]" + narrow="[[narrow]]" + route="[[route]]" > @@ -42,6 +44,8 @@ class HaConfigScript extends PolymerElement { hass="[[hass]]" script="[[script]]" is-wide="[[isWide]]" + narrow="[[narrow]]" + route="[[route]]" creating-new="[[_creatingNew]]" > @@ -53,6 +57,7 @@ class HaConfigScript extends PolymerElement { hass: Object, route: Object, isWide: Boolean, + narrow: Boolean, _routeData: Object, _routeMatches: Boolean, _creatingNew: Boolean, diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index 9787783706..b6134d7de3 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -11,7 +11,6 @@ import { TemplateResult, } from "lit-element"; import { classMap } from "lit-html/directives/class-map"; -import { computeStateName } from "../../../common/entity/compute_state_name"; import { navigate } from "../../../common/navigate"; import { computeRTL } from "../../../common/util/compute_rtl"; import "../../../components/ha-fab"; @@ -21,51 +20,48 @@ import { ScriptEntity, ScriptConfig, deleteScript, + getScriptEditorInitData, } from "../../../data/script"; -import { showConfirmationDialog } from "../../../dialogs/confirmation/show-dialog-confirmation"; +import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/ha-app-layout"; import { haStyle } from "../../../resources/styles"; -import { HomeAssistant } from "../../../types"; +import { HomeAssistant, Route } from "../../../types"; import "../automation/action/ha-automation-action"; import { computeObjectId } from "../../../common/entity/compute_object_id"; +import { configSections } from "../ha-panel-config"; export class HaScriptEditor extends LitElement { @property() public hass!: HomeAssistant; @property() public script!: ScriptEntity; @property() public isWide?: boolean; + @property() public narrow!: boolean; + @property() public route!: Route; @property() public creatingNew?: boolean; @property() private _config?: ScriptConfig; @property() private _dirty?: boolean; @property() private _errors?: string; - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html` - - - - -
- ${this.script - ? computeStateName(this.script) - : this.hass.localize( - "ui.panel.config.script.editor.default_name" - )} -
- ${this.creatingNew - ? "" - : html` - - `} -
-
+ this._backTapped()} + .tabs=${configSections.automation} + > + ${this.creatingNew + ? "" + : html` + + `}
${this._errors @@ -134,9 +130,9 @@ export class HaScriptEditor extends LitElement {
- + `; } @@ -193,10 +189,12 @@ export class HaScriptEditor extends LitElement { } if (changedProps.has("creatingNew") && this.creatingNew && this.hass) { - this._dirty = false; + const initData = getScriptEditorInitData(); + this._dirty = initData ? true : false; this._config = { alias: this.hass.localize("ui.panel.config.script.editor.default_name"), sequence: [{ service: "" }], + ...initData, }; } } @@ -228,8 +226,8 @@ export class HaScriptEditor extends LitElement { text: this.hass!.localize( "ui.panel.config.common.editor.confirm_unsaved" ), - confirmBtnText: this.hass!.localize("ui.common.yes"), - cancelBtnText: this.hass!.localize("ui.common.no"), + confirmText: this.hass!.localize("ui.common.yes"), + dismissText: this.hass!.localize("ui.common.no"), confirm: () => history.back(), }); } else { @@ -237,14 +235,16 @@ export class HaScriptEditor extends LitElement { } } + private async _deleteConfirm() { + showConfirmationDialog(this, { + text: this.hass.localize("ui.panel.config.script.editor.delete_confirm"), + confirmText: this.hass!.localize("ui.common.yes"), + dismissText: this.hass!.localize("ui.common.no"), + confirm: () => this._delete(), + }); + } + private async _delete() { - if ( - !confirm( - this.hass.localize("ui.panel.config.script.editor.delete_confirm") - ) - ) { - return; - } await deleteScript(this.hass, computeObjectId(this.script.entity_id)); history.back(); } @@ -299,7 +299,10 @@ export class HaScriptEditor extends LitElement { bottom: 24px; right: 24px; } - + ha-fab[narrow] { + bottom: 84px; + margin-bottom: -140px; + } ha-fab[dirty] { margin-bottom: 0; } diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts index 6373cf1407..836cf63f1a 100644 --- a/src/panels/config/script/ha-script-picker.ts +++ b/src/panels/config/script/ha-script-picker.ts @@ -11,7 +11,7 @@ import "@polymer/paper-icon-button/paper-icon-button"; import "@polymer/paper-item/paper-item-body"; import { HassEntity } from "home-assistant-js-websocket"; -import "../../../layouts/hass-subpage"; +import "../../../layouts/hass-tabs-subpage"; import { computeRTL } from "../../../common/util/compute_rtl"; @@ -22,20 +22,27 @@ import "../ha-config-section"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { haStyle } from "../../../resources/styles"; -import { HomeAssistant } from "../../../types"; +import { HomeAssistant, Route } from "../../../types"; import { triggerScript } from "../../../data/script"; import { showToast } from "../../../util/toast"; +import { configSections } from "../ha-panel-config"; @customElement("ha-script-picker") class HaScriptPicker extends LitElement { @property() public hass!: HomeAssistant; @property() public scripts!: HassEntity[]; @property() public isWide!: boolean; + @property() public narrow!: boolean; + @property() public route!: Route; - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html` -
@@ -77,7 +84,7 @@ class HaScriptPicker extends LitElement { )}" @click=${this._runScript} > - +
${computeStateName(script)}
@@ -98,8 +105,8 @@ class HaScriptPicker extends LitElement { - + `; } @@ -168,7 +175,9 @@ class HaScriptPicker extends LitElement { bottom: 24px; right: 24px; } - + ha-fab[narrow] { + bottom: 84px; + } ha-fab[rtl] { right: auto; left: 16px; diff --git a/src/panels/config/server_control/ha-config-section-server-control.js b/src/panels/config/server_control/ha-config-section-server-control.js index eac4ace9b5..3e44176e9e 100644 --- a/src/panels/config/server_control/ha-config-section-server-control.js +++ b/src/panels/config/server_control/ha-config-section-server-control.js @@ -148,6 +148,24 @@ class HaConfigSectionServerControl extends LocalizeMixin(PolymerElement) {
+ +
+ [[localize('ui.panel.config.server_control.section.reloading.zone')]] + +
-
-
+ `; } @@ -51,10 +57,16 @@ class HaConfigServerControl extends LocalizeMixin(PolymerElement) { return { hass: Object, isWide: Boolean, + narrow: Boolean, + route: Object, showAdvanced: Boolean, }; } + _computeTabs() { + return configSections.general; + } + computeClasses(isWide) { return isWide ? "content" : "content narrow"; } diff --git a/src/panels/config/users/ha-config-user-picker.js b/src/panels/config/users/ha-config-user-picker.js index e521772197..c7a78b581e 100644 --- a/src/panels/config/users/ha-config-user-picker.js +++ b/src/panels/config/users/ha-config-user-picker.js @@ -3,7 +3,7 @@ import "@polymer/paper-item/paper-item-body"; import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "../../../layouts/hass-subpage"; +import "../../../layouts/hass-tabs-subpage"; import "../../../components/ha-icon-next"; import "../../../components/ha-card"; import "../../../components/ha-fab"; @@ -13,6 +13,7 @@ import NavigateMixin from "../../../mixins/navigate-mixin"; import { EventsMixin } from "../../../mixins/events-mixin"; import { computeRTL } from "../../../common/util/compute_rtl"; +import { configSections } from "../ha-panel-config"; let registeredDialog = false; @@ -41,6 +42,9 @@ class HaUserPicker extends EventsMixin( right: auto; left: 16px; } + ha-fab[narrow] { + bottom: 84px; + } ha-fab[rtl][is-wide] { bottom: 24px; right: auto; @@ -58,7 +62,13 @@ class HaUserPicker extends EventsMixin( } - + `; @@ -45,6 +50,8 @@ class HaConfigUsers extends NavigateMixin(PolymerElement) { static get properties() { return { hass: Object, + isWide: Boolean, + narrow: Boolean, route: { type: Object, observer: "_checkRoute", diff --git a/src/panels/config/users/ha-user-editor.ts b/src/panels/config/users/ha-user-editor.ts index f45f8a08c5..2aa50c413c 100644 --- a/src/panels/config/users/ha-user-editor.ts +++ b/src/panels/config/users/ha-user-editor.ts @@ -10,10 +10,10 @@ import { import { until } from "lit-html/directives/until"; import "@material/mwc-button"; -import "../../../layouts/hass-subpage"; +import "../../../layouts/hass-tabs-subpage"; import { haStyle } from "../../../resources/styles"; import "../../../components/ha-card"; -import { HomeAssistant } from "../../../types"; +import { HomeAssistant, Route } from "../../../types"; import { fireEvent } from "../../../common/dom/fire_event"; import { navigate } from "../../../common/navigate"; import { @@ -24,6 +24,12 @@ import { SYSTEM_GROUP_ID_ADMIN, } from "../../../data/user"; import { showSaveSuccessToast } from "../../../util/toast-saved-success"; +import { + showAlertDialog, + showConfirmationDialog, + showPromptDialog, +} from "../../../dialogs/generic/show-dialog-box"; +import { configSections } from "../ha-panel-config"; declare global { interface HASSDomEvents { @@ -37,8 +43,10 @@ const GROUPS = [SYSTEM_GROUP_ID_USER, SYSTEM_GROUP_ID_ADMIN]; class HaUserEditor extends LitElement { @property() public hass?: HomeAssistant; @property() public user?: User; + @property() public narrow?: boolean; + @property() public route!: Route; - protected render(): TemplateResult | void { + protected render(): TemplateResult { const hass = this.hass; const user = this.user; if (!hass || !user) { @@ -46,8 +54,11 @@ class HaUserEditor extends LitElement { } return html` - @@ -106,12 +117,12 @@ class HaUserEditor extends LitElement {
- + ${hass.localize("ui.panel.config.users.editor.rename_user")} ${hass.localize("ui.panel.config.users.editor.delete_user")} @@ -125,7 +136,7 @@ class HaUserEditor extends LitElement { : ""}
-
+
`; } @@ -137,12 +148,7 @@ class HaUserEditor extends LitElement { ); } - private async _handleRenameUser(ev): Promise { - ev.currentTarget.blur(); - const newName = prompt( - this.hass!.localize("ui.panel.config.users.editor.enter_new_name"), - this.user!.name - ); + private async _handleRenameUser(newName?: string) { if (newName === null || newName === this.user!.name) { return; } @@ -153,14 +159,24 @@ class HaUserEditor extends LitElement { }); fireEvent(this, "reload-users"); } catch (err) { - alert( - `${this.hass!.localize( + showAlertDialog(this, { + text: `${this.hass!.localize( "ui.panel.config.users.editor.user_rename_failed" - )} ${err.message}` - ); + )} ${err.message}`, + }); } } + private async _handlePromptRenameUser(ev): Promise { + ev.currentTarget.blur(); + showPromptDialog(this, { + title: this.hass!.localize("ui.panel.config.users.editor.enter_new_name"), + defaultValue: this.user!.name, + inputLabel: this.hass!.localize("ui.panel.config.users.add_user.name"), + confirm: (text) => this._handleRenameUser(text), + }); + } + private async _handleGroupChange(ev): Promise { const selectEl = ev.currentTarget as HTMLSelectElement; const newGroup = selectEl.value; @@ -171,38 +187,39 @@ class HaUserEditor extends LitElement { showSaveSuccessToast(this, this.hass!); fireEvent(this, "reload-users"); } catch (err) { - alert( - `${this.hass!.localize( + showAlertDialog(this, { + text: `${this.hass!.localize( "ui.panel.config.users.editor.group_update_failed" - )} ${err.message}` - ); + )} ${err.message}`, + }); selectEl.value = this.user!.group_ids[0]; } } - private async _deleteUser(ev): Promise { - if ( - !confirm( - this.hass!.localize( - "ui.panel.config.users.editor.confirm_user_deletion", - "name", - this._name - ) - ) - ) { - ev.target.blur(); - return; - } + private async _deleteUser() { try { await deleteUser(this.hass!, this.user!.id); } catch (err) { - alert(err.code); + showAlertDialog(this, { + text: err.code, + }); return; } fireEvent(this, "reload-users"); navigate(this, "/config/users"); } + private async _promptDeleteUser(_ev): Promise { + showConfirmationDialog(this, { + text: this.hass!.localize( + "ui.panel.config.users.editor.confirm_user_deletion", + "name", + this._name + ), + confirm: () => this._deleteUser(), + }); + } + static get styles(): CSSResultArray { return [ haStyle, @@ -214,7 +231,7 @@ class HaUserEditor extends LitElement { } ha-card { max-width: 600px; - margin: 0 auto 16px; + margin: 16px auto 16px; } hass-subpage ha-card:first-of-type { direction: ltr; diff --git a/src/panels/config/zha/functions.ts b/src/panels/config/zha/functions.ts index 1e7a2858de..065eb9867e 100644 --- a/src/panels/config/zha/functions.ts +++ b/src/panels/config/zha/functions.ts @@ -1,4 +1,4 @@ -import { ZHADevice, ZHAGroup } from "../../../data/zha"; +import { ZHADevice, ZHAGroup, Cluster } from "../../../data/zha"; export const formatAsPaddedHex = (value: string | number): string => { let hex = value; @@ -19,3 +19,9 @@ export const sortZHAGroups = (a: ZHAGroup, b: ZHAGroup): number => { const nameb = b.name; return nameA.localeCompare(nameb); }; + +export const computeClusterKey = (cluster: Cluster): string => { + return `${cluster.name} (Endpoint id: ${ + cluster.endpoint_id + }, Id: ${formatAsPaddedHex(cluster.id)}, Type: ${cluster.type})`; +}; diff --git a/src/panels/config/zha/zha-add-devices-page.ts b/src/panels/config/zha/zha-add-devices-page.ts index 04e7532518..76f7ae403e 100644 --- a/src/panels/config/zha/zha-add-devices-page.ts +++ b/src/panels/config/zha/zha-add-devices-page.ts @@ -50,7 +50,7 @@ class ZHAAddDevicesPage extends LitElement { this._formattedEvents = ""; } - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html`
@@ -279,8 +279,7 @@ export class ZHAClusterAttributes extends LitElement { } ha-card { - margin: 0 auto; - max-width: 600px; + max-width: 680px; } .card-actions.warning ha-call-service-button { @@ -308,6 +307,7 @@ export class ZHAClusterAttributes extends LitElement { float: right; top: -6px; right: 0; + padding-right: 0px; color: var(--primary-color); } diff --git a/src/panels/config/zha/zha-cluster-commands.ts b/src/panels/config/zha/zha-cluster-commands.ts index 233f231735..70416983ee 100644 --- a/src/panels/config/zha/zha-cluster-commands.ts +++ b/src/panels/config/zha/zha-cluster-commands.ts @@ -53,7 +53,7 @@ export class ZHAClusterCommands extends LitElement { super.update(changedProperties); } - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html`
@@ -212,8 +212,7 @@ export class ZHAClusterCommands extends LitElement { } ha-card { - margin: 0 auto; - max-width: 600px; + max-width: 680px; } .card-actions.warning ha-call-service-button { @@ -253,6 +252,7 @@ export class ZHAClusterCommands extends LitElement { float: right; top: -6px; right: 0; + padding-right: 0px; color: var(--primary-color); } diff --git a/src/panels/config/zha/zha-clusters-data-table.ts b/src/panels/config/zha/zha-clusters-data-table.ts new file mode 100644 index 0000000000..db28edad38 --- /dev/null +++ b/src/panels/config/zha/zha-clusters-data-table.ts @@ -0,0 +1,92 @@ +import "../../../components/data-table/ha-data-table"; +import "../../../components/entity/ha-state-icon"; + +import memoizeOne from "memoize-one"; + +import { + LitElement, + html, + TemplateResult, + property, + customElement, +} from "lit-element"; +import { HomeAssistant } from "../../../types"; +// tslint:disable-next-line +import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table"; +// tslint:disable-next-line +import { Cluster } from "../../../data/zha"; +import { formatAsPaddedHex } from "./functions"; + +export interface ClusterRowData extends Cluster { + cluster?: Cluster; + cluster_id?: string; +} + +@customElement("zha-clusters-data-table") +export class ZHAClustersDataTable extends LitElement { + @property() public hass!: HomeAssistant; + @property() public narrow = false; + @property() public clusters: Cluster[] = []; + + private _clusters = memoizeOne((clusters: Cluster[]) => { + let outputClusters: ClusterRowData[] = clusters; + + outputClusters = outputClusters.map((cluster) => { + return { + ...cluster, + cluster_id: cluster.endpoint_id + "-" + cluster.id, + }; + }); + + return outputClusters; + }); + + private _columns = memoizeOne( + (narrow: boolean): DataTableColumnContainer => + narrow + ? { + name: { + title: "Name", + sortable: true, + direction: "asc", + }, + } + : { + name: { + title: "Name", + sortable: true, + direction: "asc", + }, + id: { + title: "ID", + template: (id: number) => { + return html` + ${formatAsPaddedHex(id)} + `; + }, + sortable: true, + }, + endpoint_id: { + title: "Endpoint ID", + sortable: true, + }, + } + ); + + protected render(): TemplateResult { + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "zha-clusters-data-table": ZHAClustersDataTable; + } +} diff --git a/src/panels/config/zha/zha-clusters.ts b/src/panels/config/zha/zha-clusters.ts index 5b3f6d56f7..86c6f2d8ef 100644 --- a/src/panels/config/zha/zha-clusters.ts +++ b/src/panels/config/zha/zha-clusters.ts @@ -21,7 +21,7 @@ import { fireEvent } from "../../../common/dom/fire_event"; import { Cluster, fetchClustersForZhaNode, ZHADevice } from "../../../data/zha"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; -import { formatAsPaddedHex } from "./functions"; +import { computeClusterKey } from "./functions"; import { ItemSelectedEvent } from "./types"; declare global { @@ -33,12 +33,6 @@ declare global { } } -const computeClusterKey = (cluster: Cluster): string => { - return `${cluster.name} (Endpoint id: ${ - cluster.endpoint_id - }, Id: ${formatAsPaddedHex(cluster.id)}, Type: ${cluster.type})`; -}; - export class ZHAClusters extends LitElement { @property() public hass?: HomeAssistant; @property() public isWide?: boolean; @@ -59,7 +53,7 @@ export class ZHAClusters extends LitElement { super.update(changedProperties); } - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html`
@@ -152,8 +146,7 @@ export class ZHAClusters extends LitElement { } ha-card { - margin: 0 auto; - max-width: 600px; + max-width: 680px; } .node-picker { @@ -167,6 +160,7 @@ export class ZHAClusters extends LitElement { float: right; top: -6px; right: 0; + padding-right: 0px; color: var(--primary-color); } diff --git a/src/panels/config/zha/zha-config-dashboard.ts b/src/panels/config/zha/zha-config-dashboard.ts index 2a280551ca..b492ed230c 100644 --- a/src/panels/config/zha/zha-config-dashboard.ts +++ b/src/panels/config/zha/zha-config-dashboard.ts @@ -48,7 +48,6 @@ class ZHAConfigDashboard extends LitElement { ...device, name: device.user_given_name ? device.user_given_name : device.name, nwk: formatAsPaddedHex(device.nwk), - id: device.ieee, }; }); @@ -101,9 +100,12 @@ class ZHAConfigDashboard extends LitElement { this._firstUpdatedCalled = true; } - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html` - +
${this.hass.localize("ui.panel.config.zha.header")} @@ -139,6 +141,7 @@ class ZHAConfigDashboard extends LitElement { .columns=${this._columns(this.narrow)} .data=${this._memoizeDevices(this._devices)} @row-click=${this._handleDeviceClicked} + .id=${"ieee"} > diff --git a/src/panels/config/zha/zha-binding.ts b/src/panels/config/zha/zha-device-binding.ts similarity index 93% rename from src/panels/config/zha/zha-binding.ts rename to src/panels/config/zha/zha-device-binding.ts index 06a3d1b2dc..5aeaf74c7d 100644 --- a/src/panels/config/zha/zha-binding.ts +++ b/src/panels/config/zha/zha-device-binding.ts @@ -24,8 +24,8 @@ import { HomeAssistant } from "../../../types"; import { ItemSelectedEvent } from "./types"; import "@polymer/paper-item/paper-item"; -@customElement("zha-binding-control") -export class ZHABindingControl extends LitElement { +@customElement("zha-device-binding-control") +export class ZHADeviceBindingControl extends LitElement { @property() public hass?: HomeAssistant; @property() public isWide?: boolean; @property() public selectedDevice?: ZHADevice; @@ -41,7 +41,7 @@ export class ZHABindingControl extends LitElement { super.update(changedProperties); } - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html`
@@ -158,8 +158,7 @@ export class ZHABindingControl extends LitElement { } ha-card { - margin: 0 auto; - max-width: 600px; + max-width: 680px; } .card-actions.warning ha-call-service-button { @@ -175,7 +174,9 @@ export class ZHABindingControl extends LitElement { .helpText { color: grey; - padding: 16px; + padding-left: 28px; + padding-right: 28px; + padding-bottom: 10px; } .header { @@ -186,6 +187,7 @@ export class ZHABindingControl extends LitElement { float: right; top: -6px; right: 0; + padding-right: 0px; color: var(--primary-color); } @@ -204,6 +206,6 @@ export class ZHABindingControl extends LitElement { declare global { interface HTMLElementTagNameMap { - "zha-binding-control": ZHABindingControl; + "zha-device-binding-control": ZHADeviceBindingControl; } } diff --git a/src/panels/config/zha/zha-device-card.ts b/src/panels/config/zha/zha-device-card.ts index ef96c070dc..3c0ab62411 100644 --- a/src/panels/config/zha/zha-device-card.ts +++ b/src/panels/config/zha/zha-device-card.ts @@ -42,6 +42,7 @@ import { navigate } from "../../../common/navigate"; import { UnsubscribeFunc, HassEvent } from "home-assistant-js-websocket"; import { formatAsPaddedHex } from "./functions"; import { computeStateName } from "../../../common/entity/compute_state_name"; +import { addEntitiesToLovelaceView } from "../../lovelace/editor/add-entities-to-view"; declare global { // for fire event @@ -58,8 +59,11 @@ class ZHADeviceCard extends LitElement { @property() public device?: ZHADevice; @property({ type: Boolean }) public narrow?: boolean; @property({ type: Boolean }) public showHelp?: boolean = false; - @property({ type: Boolean }) public showActions?: boolean; - @property({ type: Boolean }) public isJoinPage?: boolean; + @property({ type: Boolean }) public showActions?: boolean = true; + @property({ type: Boolean }) public showName?: boolean = true; + @property({ type: Boolean }) public showEntityDetail?: boolean = true; + @property({ type: Boolean }) public showModelInfo?: boolean = true; + @property({ type: Boolean }) public showEditableInfo?: boolean = true; @property() private _serviceData?: NodeServiceData; @property() private _areas: AreaRegistryEntry[] = []; @property() private _selectedAreaIndex: number = -1; @@ -106,9 +110,6 @@ class ZHADeviceCard extends LitElement { this.addEventListener("hass-service-called", (ev) => this.serviceCalled(ev) ); - this._serviceData = { - ieee_address: this.device!.ieee, - }; } protected updated(changedProperties: PropertyValues): void { @@ -122,6 +123,9 @@ class ZHADeviceCard extends LitElement { ) + 1; } this._userGivenName = this.device!.user_given_name; + this._serviceData = { + ieee_address: this.device!.ieee, + }; } super.update(changedProperties); } @@ -135,11 +139,11 @@ class ZHADeviceCard extends LitElement { } } - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html` - + ${ - this.isJoinPage + this.showModelInfo ? html`
${this.device!.model}
@@ -160,6 +164,8 @@ class ZHADeviceCard extends LitElement {
${this.device!.ieee}
Nwk:
${formatAsPaddedHex(this.device!.nwk)}
+
Device Type:
+
${this.device!.device_type}
LQI:
${this.device!.lqi || this.hass!.localize("ui.dialogs.zha_device_info.unknown")}
@@ -202,7 +208,7 @@ class ZHADeviceCard extends LitElement { .stateObj="${this.hass!.states[entity.entity_id]}" slot="item-icon" > - ${!this.isJoinPage + ${this.showEntityDetail ? html`
@@ -218,40 +224,61 @@ class ZHADeviceCard extends LitElement { ` )}
-
- -
-
- - - - ${this.hass!.localize("ui.dialogs.zha_device_info.no_area")} - + ${ + this.device!.entities && this.device!.entities.length > 0 + ? html` +
+ + ${this.hass.localize( + "ui.panel.config.devices.entities.add_entities_lovelace" + )} + +
+ ` + : "" + } + ${ + this.showEditableInfo + ? html` +
+ +
+
+ + + + ${this.hass!.localize( + "ui.dialogs.zha_device_info.no_area" + )} + - ${this._areas.map( - (entry) => html` - ${entry.name} - ` - )} - - -
+ ${this._areas.map( + (entry) => html` + ${entry.name} + ` + )} +
+
+
+ ` + : "" + } ${ this.showActions ? html` @@ -275,6 +302,9 @@ class ZHADeviceCard extends LitElement { .hass="${this.hass}" domain="zha" service="remove" + .confirmation=${this.hass!.localize( + "ui.dialogs.zha_device_info.confirmations.remove" + )} .serviceData="${this._serviceData}" > ${this.hass!.localize( @@ -290,7 +320,8 @@ class ZHADeviceCard extends LitElement {
` : ""} - ${this.device!.power_source === "Mains" + ${this.device!.power_source === "Mains" && + this.device!.device_type === "Router" ? html` ${this.hass!.localize( @@ -379,6 +410,14 @@ class ZHADeviceCard extends LitElement { navigate(this, "/config/zha/add/" + this.device!.ieee); } + private _addToLovelaceView(): void { + addEntitiesToLovelaceView( + this, + this.hass, + this.device!.entities.map((entity) => entity.entity_id) + ); + } + static get styles(): CSSResult[] { return [ haStyle, diff --git a/src/panels/config/zha/zha-device-page.ts b/src/panels/config/zha/zha-device-page.ts index d8605b8a84..ebda46f890 100755 --- a/src/panels/config/zha/zha-device-page.ts +++ b/src/panels/config/zha/zha-device-page.ts @@ -1,6 +1,7 @@ import "../../../layouts/hass-subpage"; import "../../../components/ha-paper-icon-button-arrow-prev"; -import "./zha-binding"; +import "./zha-device-binding"; +import "./zha-group-binding"; import "./zha-cluster-attributes"; import "./zha-cluster-commands"; import "./zha-clusters"; @@ -24,10 +25,12 @@ import { fetchBindableDevices, ZHADevice, fetchZHADevice, + ZHAGroup, + fetchGroups, } from "../../../data/zha"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; -import { sortZHADevices } from "./functions"; +import { sortZHADevices, sortZHAGroups } from "./functions"; import { ZHAClusterSelectedParams } from "./types"; @customElement("zha-device-page") @@ -36,8 +39,27 @@ export class ZHADevicePage extends LitElement { @property() public isWide?: boolean; @property() public ieee?: string; @property() public device?: ZHADevice; + @property() public narrow?: boolean; @property() private _selectedCluster?: Cluster; @property() private _bindableDevices: ZHADevice[] = []; + @property() private _groups: ZHAGroup[] = []; + + private _firstUpdatedCalled: boolean = false; + + public connectedCallback(): void { + super.connectedCallback(); + if (this.hass && this._firstUpdatedCalled) { + this._fetchGroups(); + } + } + + protected firstUpdated(changedProperties: PropertyValues): void { + super.firstUpdated(changedProperties); + if (this.hass) { + this._fetchGroups(); + } + this._firstUpdatedCalled = true; + } protected updated(changedProperties: PropertyValues): void { if (changedProperties.has("ieee")) { @@ -46,47 +68,64 @@ export class ZHADevicePage extends LitElement { super.update(changedProperties); } - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html` - - ${this._selectedCluster - ? html` - - - ` - : ""} - ${this._bindableDevices.length > 0 + ${this.device && this.device.device_type !== "Coordinator" ? html` - + @zha-cluster-selected="${this._onClusterSelected}" + > + ${this._selectedCluster + ? html` + + + + ` + : ""} + ${this._bindableDevices.length > 0 + ? html` + + ` + : ""} + ${this.device && this._groups.length > 0 + ? html` + + ` + : ""} ` : ""}
@@ -103,12 +142,22 @@ export class ZHADevicePage extends LitElement { private async _fetchData(): Promise { if (this.ieee && this.hass) { this.device = await fetchZHADevice(this.hass, this.ieee); - this._bindableDevices = ( - await fetchBindableDevices(this.hass, this.ieee) - ).sort(sortZHADevices); + this._bindableDevices = + this.device && this.device.device_type !== "Coordinator" + ? (await fetchBindableDevices(this.hass, this.ieee)).sort( + sortZHADevices + ) + : []; } } + private async _fetchGroups() { + this._groups = + this.device && this.device.device_type !== "Coordinator" + ? (await fetchGroups(this.hass!)).sort(sortZHAGroups) + : []; + } + static get styles(): CSSResult[] { return [ haStyle, diff --git a/src/panels/config/zha/zha-group-binding.ts b/src/panels/config/zha/zha-group-binding.ts new file mode 100644 index 0000000000..29023d8177 --- /dev/null +++ b/src/panels/config/zha/zha-group-binding.ts @@ -0,0 +1,321 @@ +import "../../../components/buttons/ha-call-service-button"; +import "../../../components/ha-service-description"; +import "../../../components/ha-card"; +import "../ha-config-section"; +import "@material/mwc-button/mwc-button"; +import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; +import "@polymer/paper-icon-button/paper-icon-button"; +import "@polymer/paper-listbox/paper-listbox"; + +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + PropertyValues, + TemplateResult, +} from "lit-element"; + +import { + bindDeviceToGroup, + unbindDeviceFromGroup, + ZHADevice, + ZHAGroup, + Cluster, + fetchClustersForZhaNode, +} from "../../../data/zha"; +import "./zha-clusters-data-table"; +import { haStyle } from "../../../resources/styles"; +import { HomeAssistant } from "../../../types"; +import { ItemSelectedEvent } from "./types"; +import "@polymer/paper-item/paper-item"; +import { SelectionChangedEvent } from "../../../components/data-table/ha-data-table"; + +@customElement("zha-group-binding-control") +export class ZHAGroupBindingControl extends LitElement { + @property() public hass?: HomeAssistant; + @property() public isWide?: boolean; + @property() public narrow?: boolean; + @property() public selectedDevice?: ZHADevice; + @property() private _showHelp: boolean = false; + @property() private _bindTargetIndex: number = -1; + @property() private groups: ZHAGroup[] = []; + @property() private _selectedClusters: string[] = []; + @property() private _clusters: Cluster[] = []; + private _groupToBind?: ZHAGroup; + private _clustersToBind?: Cluster[]; + + protected updated(changedProperties: PropertyValues): void { + if (changedProperties.has("selectedDevice")) { + this._bindTargetIndex = -1; + this._selectedClusters = []; + this._clustersToBind = []; + this._fetchClustersForZhaNode(); + } + super.update(changedProperties); + } + + protected render(): TemplateResult { + return html` + +
+ ${this.hass!.localize( + "ui.panel.config.zha.group_binding.header" + )} + + +
+ ${this.hass!.localize( + "ui.panel.config.zha.group_binding.introduction" + )} + + +
+ + + ${this.groups.map( + (group) => html` + ${group.name} + ` + )} + + +
+ ${this._showHelp + ? html` +
+ ${this.hass!.localize( + "ui.panel.config.zha.group_binding.group_picker_help" + )} +
+ ` + : ""} +
+ +
+ ${this._showHelp + ? html` +
+ ${this.hass!.localize( + "ui.panel.config.zha.group_binding.cluster_selection_help" + )} +
+ ` + : ""} +
+ ${this.hass!.localize( + "ui.panel.config.zha.group_binding.bind_button_label" + )} + ${this._showHelp + ? html` +
+ ${this.hass!.localize( + "ui.panel.config.zha.group_binding.bind_button_help" + )} +
+ ` + : ""} + ${this.hass!.localize( + "ui.panel.config.zha.group_binding.unbind_button_label" + )} + ${this._showHelp + ? html` +
+ ${this.hass!.localize( + "ui.panel.config.zha.group_binding.unbind_button_help" + )} +
+ ` + : ""} +
+
+
+ `; + } + + private _bindTargetIndexChanged(event: ItemSelectedEvent): void { + this._bindTargetIndex = event.target!.selected; + this._groupToBind = + this._bindTargetIndex === -1 + ? undefined + : this.groups[this._bindTargetIndex]; + } + + private _onHelpTap(): void { + this._showHelp = !this._showHelp; + } + + private async _onBindGroupClick(): Promise { + if (this.hass && this._canBind) { + await bindDeviceToGroup( + this.hass, + this.selectedDevice!.ieee, + this._groupToBind!.group_id, + this._clustersToBind! + ); + } + } + + private async _onUnbindGroupClick(): Promise { + if (this.hass && this._canBind) { + await unbindDeviceFromGroup( + this.hass, + this.selectedDevice!.ieee, + this._groupToBind!.group_id, + this._clustersToBind! + ); + } + } + + private _handleClusterSelectionChanged(event: CustomEvent): void { + const changedSelection = event.detail as SelectionChangedEvent; + const clusterId = changedSelection.id; + if ( + changedSelection.selected && + !this._selectedClusters.includes(clusterId) + ) { + this._selectedClusters.push(clusterId); + } else { + const index = this._selectedClusters.indexOf(clusterId); + if (index !== -1) { + this._selectedClusters.splice(index, 1); + } + } + this._selectedClusters = [...this._selectedClusters]; + this._clustersToBind = []; + for (const clusterIndex of this._selectedClusters) { + const selectedCluster = this._clusters.find((cluster) => { + return clusterIndex === cluster.endpoint_id + "-" + cluster.id; + }); + this._clustersToBind.push(selectedCluster!); + } + } + + private async _fetchClustersForZhaNode(): Promise { + if (this.hass) { + this._clusters = await fetchClustersForZhaNode( + this.hass, + this.selectedDevice!.ieee + ); + this._clusters = this._clusters + .filter((cluster) => { + return cluster.type.toLowerCase() === "out"; + }) + .sort((a, b) => { + return a.name.localeCompare(b.name); + }); + } + } + + private get _canBind(): boolean { + return Boolean( + this._groupToBind && + this._clustersToBind && + this._clustersToBind?.length > 0 && + this.selectedDevice + ); + } + + static get styles(): CSSResult[] { + return [ + haStyle, + css` + .menu { + width: 100%; + } + + .content { + margin-top: 24px; + } + + ha-card { + max-width: 680px; + } + + .card-actions.warning ha-call-service-button { + color: var(--google-red-500); + } + + .command-picker { + align-items: center; + padding-left: 28px; + padding-right: 28px; + padding-bottom: 10px; + } + + .input-text { + padding-left: 28px; + padding-right: 28px; + padding-bottom: 10px; + } + + .sectionHeader { + flex-grow: 1; + } + + .helpText { + color: grey; + padding-left: 28px; + padding-right: 28px; + padding-bottom: 10px; + } + + .toggle-help-icon { + float: right; + top: -6px; + right: 0; + padding-right: 0px; + color: var(--primary-color); + } + + ha-service-description { + display: block; + color: grey; + } + + [hidden] { + display: none; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "zha-group-binding-control": ZHAGroupBindingControl; + } +} diff --git a/src/panels/config/zha/zha-group-page.ts b/src/panels/config/zha/zha-group-page.ts index 8aec748c87..afd265c2a4 100644 --- a/src/panels/config/zha/zha-group-page.ts +++ b/src/panels/config/zha/zha-group-page.ts @@ -121,6 +121,8 @@ export class ZHAGroupPage extends LitElement { .hass=${this.hass} .device=${member} .narrow=${this.narrow} + .showActions=${false} + .showEditableInfo=${false} > ` ) @@ -224,7 +226,10 @@ export class ZHAGroupPage extends LitElement { private _handleAddSelectionChanged(ev: CustomEvent): void { const changedSelection = ev.detail as SelectionChangedEvent; const entity = changedSelection.id; - if (changedSelection.selected) { + if ( + changedSelection.selected && + !this._selectedDevicesToAdd.includes(entity) + ) { this._selectedDevicesToAdd.push(entity); } else { const index = this._selectedDevicesToAdd.indexOf(entity); @@ -238,7 +243,10 @@ export class ZHAGroupPage extends LitElement { private _handleRemoveSelectionChanged(ev: CustomEvent): void { const changedSelection = ev.detail as SelectionChangedEvent; const entity = changedSelection.id; - if (changedSelection.selected) { + if ( + changedSelection.selected && + !this._selectedDevicesToRemove.includes(entity) + ) { this._selectedDevicesToRemove.push(entity); } else { const index = this._selectedDevicesToRemove.indexOf(entity); diff --git a/src/panels/config/zha/zha-groups-dashboard.ts b/src/panels/config/zha/zha-groups-dashboard.ts index 874918fd84..4e21dfa5a9 100644 --- a/src/panels/config/zha/zha-groups-dashboard.ts +++ b/src/panels/config/zha/zha-groups-dashboard.ts @@ -47,9 +47,9 @@ export class ZHAGroupsDashboard extends LitElement { protected render(): TemplateResult { return html` { + let outputGroups: GroupRowData[] = groups; + + outputGroups = outputGroups.map((group) => { + return { + ...group, + id: String(group.group_id), + }; + }); + + return outputGroups; + }); + private _columns = memoizeOne( (narrow: boolean): DataTableColumnContainer => narrow @@ -83,8 +96,7 @@ export class ZHAGroupsDataTable extends LitElement { return html` `; diff --git a/src/panels/config/zha/zha-node.ts b/src/panels/config/zha/zha-node.ts index 8281823bd3..b0f1468369 100644 --- a/src/panels/config/zha/zha-node.ts +++ b/src/panels/config/zha/zha-node.ts @@ -27,7 +27,7 @@ export class ZHANode extends LitElement { @property() public device?: ZHADevice; @property() private _showHelp: boolean = false; - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html`
@@ -55,16 +55,29 @@ export class ZHANode extends LitElement { "ui.panel.config.zha.node_management.hint_wakeup" )} - +
+ ${this.device + ? html` + + ` + : html` + + `} +
`; } @@ -93,17 +106,13 @@ export class ZHANode extends LitElement { padding-bottom: 16px; } - ha-card { - margin: 0 auto; - max-width: 600px; + .content { + max-width: 680px; } .card { + padding: 28px 20px 0; margin-top: 24px; - box-sizing: border-box; - display: flex; - flex: 1; - word-wrap: break-word; } ha-service-description { @@ -123,6 +132,7 @@ export class ZHANode extends LitElement { float: right; top: 6px; right: 0; + padding-right: 0px; color: var(--primary-color); } `, diff --git a/src/panels/config/zone/dialog-zone-detail.ts b/src/panels/config/zone/dialog-zone-detail.ts new file mode 100644 index 0000000000..a4afc11ec2 --- /dev/null +++ b/src/panels/config/zone/dialog-zone-detail.ts @@ -0,0 +1,323 @@ +import { + LitElement, + html, + css, + CSSResult, + TemplateResult, + property, +} from "lit-element"; + +import "@polymer/paper-input/paper-input"; +import "@material/mwc-button"; + +import "../../../components/map/ha-location-editor"; +import "../../../components/ha-switch"; +import "../../../components/ha-dialog"; + +import { ZoneDetailDialogParams } from "./show-dialog-zone-detail"; +import { HomeAssistant } from "../../../types"; +import { ZoneMutableParams } from "../../../data/zone"; +import { addDistanceToCoord } from "../../../common/location/add_distance_to_coord"; + +class DialogZoneDetail extends LitElement { + @property() public hass!: HomeAssistant; + @property() private _name!: string; + @property() private _icon!: string; + @property() private _latitude!: number; + @property() private _longitude!: number; + @property() private _passive!: boolean; + @property() private _radius!: number; + @property() private _error?: string; + @property() private _params?: ZoneDetailDialogParams; + @property() private _submitting: boolean = false; + + public async showDialog(params: ZoneDetailDialogParams): Promise { + this._params = params; + this._error = undefined; + if (this._params.entry) { + this._name = this._params.entry.name || ""; + this._icon = this._params.entry.icon || ""; + this._latitude = this._params.entry.latitude || this.hass.config.latitude; + this._longitude = + this._params.entry.longitude || this.hass.config.longitude; + this._passive = this._params.entry.passive || false; + this._radius = this._params.entry.radius || 100; + } else { + const movedHomeLocation = addDistanceToCoord( + [this.hass.config.latitude, this.hass.config.longitude], + 500, + 500 + ); + this._name = ""; + this._icon = ""; + this._latitude = movedHomeLocation[0]; + this._longitude = movedHomeLocation[1]; + this._passive = false; + this._radius = 100; + } + await this.updateComplete; + } + + protected render(): TemplateResult { + if (!this._params) { + return html``; + } + const title = html` + ${this._params.entry + ? this._params.entry.name + : this.hass!.localize("ui.panel.config.zone.detail.new_zone")} + + `; + return html` + +
+ ${this._error + ? html` +
${this._error}
+ ` + : ""} +
+ + + +
+ + +
+ +

+ ${this.hass!.localize("ui.panel.config.zone.detail.passive_note")} +

+ ${this.hass!.localize( + "ui.panel.config.zone.detail.passive" + )} +
+
+ ${this._params.entry + ? html` + + ${this.hass!.localize("ui.panel.config.zone.detail.delete")} + + ` + : html``} + + ${this._params.entry + ? this.hass!.localize("ui.panel.config.zone.detail.update") + : this.hass!.localize("ui.panel.config.zone.detail.create")} + +
+ `; + } + + private get _locationValue() { + return [Number(this._latitude), Number(this._longitude)]; + } + + private _locationChanged(ev) { + [this._latitude, this._longitude] = ev.currentTarget.location; + this._radius = ev.currentTarget.radius; + } + + private _passiveChanged(ev) { + this._passive = ev.target.checked; + } + + private _valueChanged(ev: CustomEvent) { + const configValue = (ev.target as any).configValue; + + this._error = undefined; + this[`_${configValue}`] = ev.detail.value; + } + + private async _updateEntry() { + this._submitting = true; + try { + const values: ZoneMutableParams = { + name: this._name.trim(), + icon: this._icon.trim(), + latitude: this._latitude, + longitude: this._longitude, + passive: this._passive, + radius: this._radius, + }; + if (this._params!.entry) { + await this._params!.updateEntry!(values); + } else { + await this._params!.createEntry(values); + } + this._params = undefined; + } catch (err) { + this._error = err ? err.message : "Unknown error"; + } finally { + this._submitting = false; + } + } + + private async _deleteEntry() { + this._submitting = true; + try { + if (await this._params!.removeEntry!()) { + this._params = undefined; + } + } finally { + this._submitting = false; + } + } + + private _close(): void { + this._params = undefined; + } + + static get styles(): CSSResult[] { + return [ + css` + ha-dialog { + --mdc-dialog-title-ink-color: var(--primary-text-color); + --justify-action-buttons: space-between; + } + @media only screen and (min-width: 600px) { + ha-dialog { + --mdc-dialog-min-width: 600px; + } + } + + /* make dialog fullscreen on small screens */ + @media all and (max-width: 450px), all and (max-height: 500px) { + ha-dialog { + --mdc-dialog-min-width: 100vw; + --mdc-dialog-max-height: 100vh; + --mdc-dialog-shape-radius: 0px; + --vertial-align-dialog: flex-end; + } + } + .form { + padding-bottom: 24px; + } + .location { + display: flex; + } + .location > * { + flex-grow: 1; + min-width: 0; + } + .location > *:first-child { + margin-right: 4px; + } + .location > *:last-child { + margin-left: 4px; + } + ha-location-editor { + margin-top: 16px; + } + ha-user-picker { + margin-top: 16px; + } + mwc-button.warning { + --mdc-theme-primary: var(--google-red-500); + } + .error { + color: var(--google-red-500); + } + a { + color: var(--primary-color); + } + p { + color: var(--primary-text-color); + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-zone-detail": DialogZoneDetail; + } +} + +customElements.define("dialog-zone-detail", DialogZoneDetail); diff --git a/src/panels/config/zone/ha-config-zone.ts b/src/panels/config/zone/ha-config-zone.ts new file mode 100644 index 0000000000..96ff793607 --- /dev/null +++ b/src/panels/config/zone/ha-config-zone.ts @@ -0,0 +1,470 @@ +import { + LitElement, + TemplateResult, + html, + css, + CSSResult, + property, + customElement, + query, + PropertyValues, +} from "lit-element"; +import "@polymer/paper-listbox/paper-listbox"; +import "@polymer/paper-item/paper-icon-item"; +import "@polymer/paper-item/paper-item-body"; +import "@polymer/paper-tooltip/paper-tooltip"; + +import "../../../components/map/ha-locations-editor"; + +import { HomeAssistant, Route } from "../../../types"; +import "../../../components/ha-card"; +import "../../../components/ha-fab"; +import "../../../layouts/hass-tabs-subpage"; +import "../../../layouts/hass-loading-screen"; +import { compare } from "../../../common/string/compare"; +import "../ha-config-section"; +import { showZoneDetailDialog } from "./show-dialog-zone-detail"; +import { + Zone, + fetchZones, + createZone, + updateZone, + deleteZone, + ZoneMutableParams, +} from "../../../data/zone"; +// tslint:disable-next-line +import { + HaLocationsEditor, + MarkerLocation, +} from "../../../components/map/ha-locations-editor"; +import { computeStateDomain } from "../../../common/entity/compute_state_domain"; +import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; +import memoizeOne from "memoize-one"; +import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; +import { subscribeEntityRegistry } from "../../../data/entity_registry"; +import { configSections } from "../ha-panel-config"; + +@customElement("ha-config-zone") +export class HaConfigZone extends SubscribeMixin(LitElement) { + @property() public hass!: HomeAssistant; + @property() public isWide?: boolean; + @property() public narrow?: boolean; + @property() public route!: Route; + @property() private _storageItems?: Zone[]; + @property() private _stateItems?: HassEntity[]; + @property() private _activeEntry: string = ""; + @query("ha-locations-editor") private _map?: HaLocationsEditor; + private _regEntities: string[] = []; + + private _getZones = memoizeOne( + (storageItems: Zone[], stateItems: HassEntity[]): MarkerLocation[] => { + const stateLocations: MarkerLocation[] = stateItems.map((state) => { + return { + id: state.entity_id, + icon: state.attributes.icon, + name: state.attributes.friendly_name || state.entity_id, + latitude: state.attributes.latitude, + longitude: state.attributes.longitude, + radius: state.attributes.radius, + radius_color: + state.entity_id === "zone.home" + ? "#03a9f4" + : state.attributes.passive + ? "#9b9b9b" + : "#FF9800", + editable: false, + }; + }); + const storageLocations: MarkerLocation[] = storageItems.map((zone) => { + return { + ...zone, + radius_color: zone.passive ? "#9b9b9b" : "#FF9800", + editable: true, + }; + }); + return storageLocations.concat(stateLocations); + } + ); + + public hassSubscribe(): UnsubscribeFunc[] { + return [ + subscribeEntityRegistry(this.hass.connection!, (entities) => { + this._regEntities = entities.map( + (registryEntry) => registryEntry.entity_id + ); + this._filterStates(); + }), + ]; + } + + protected render(): TemplateResult { + if ( + !this.hass || + this._storageItems === undefined || + this._stateItems === undefined + ) { + return html` + + `; + } + const hass = this.hass; + const listBox = + this._storageItems.length === 0 && this._stateItems.length === 0 + ? html` +
+ ${hass.localize("ui.panel.config.zone.no_zones_created_yet")} +
+ + ${hass.localize("ui.panel.config.zone.create_zone")} +
+ ` + : html` + + ${this._storageItems.map((entry) => { + return html` + + + + ${entry.name} + + ${!this.narrow + ? html` + + ` + : ""} + + `; + })} + ${this._stateItems.map((state) => { + return html` + + + + + ${state.attributes.friendly_name || state.entity_id} + +
+ + + ${state.entity_id === "zone.home" + ? this.hass.localize( + "ui.panel.config.zone.edit_home_zone" + ) + : this.hass.localize( + "ui.panel.config.zone.configured_in_yaml" + )} + +
+
+ `; + })} +
+ `; + + return html` + + ${this.narrow + ? html` + + + ${hass.localize("ui.panel.config.zone.introduction")} + + ${listBox} + + ` + : ""} + ${!this.narrow + ? html` +
+ + ${listBox} +
+ ` + : ""} +
+ + + `; + } + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + this._fetchData(); + } + + protected updated(changedProps: PropertyValues) { + super.updated(changedProps); + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + if (oldHass && this._stateItems) { + this._getStates(oldHass); + } + } + + private async _fetchData() { + this._storageItems = (await fetchZones(this.hass!)).sort((ent1, ent2) => + compare(ent1.name, ent2.name) + ); + this._getStates(); + } + + private _getStates(oldHass?: HomeAssistant) { + let changed = false; + const tempStates = Object.values(this.hass!.states).filter((entity) => { + if (computeStateDomain(entity) !== "zone") { + return false; + } + if (oldHass?.states[entity.entity_id] !== entity) { + changed = true; + } + if (this._regEntities.includes(entity.entity_id)) { + return false; + } + return true; + }); + + if (changed) { + this._stateItems = tempStates; + } + } + + private _filterStates() { + if (!this._stateItems) { + return; + } + const tempStates = this._stateItems.filter( + (entity) => !this._regEntities.includes(entity.entity_id) + ); + if (tempStates.length !== this._stateItems.length) { + this._stateItems = tempStates; + } + } + + private _locationUpdated(ev: CustomEvent) { + this._activeEntry = ev.detail.id; + const entry = this._storageItems!.find((item) => item.id === ev.detail.id); + if (!entry) { + return; + } + this._updateEntry(entry, { + latitude: ev.detail.location[0], + longitude: ev.detail.location[1], + }); + } + + private _radiusUpdated(ev: CustomEvent) { + this._activeEntry = ev.detail.id; + const entry = this._storageItems!.find((item) => item.id === ev.detail.id); + if (!entry) { + return; + } + this._updateEntry(entry, { + radius: ev.detail.radius, + }); + } + + private _markerClicked(ev: CustomEvent) { + this._activeEntry = ev.detail.id; + } + + private _createZone() { + this._openDialog(); + } + + private _itemClicked(ev: MouseEvent) { + if (this.narrow) { + this._openEditEntry(ev); + return; + } + const entry: Zone = (ev.currentTarget! as any).entry; + this._zoomZone(entry.id); + } + + private _stateItemClicked(ev: MouseEvent) { + const entityId = (ev.currentTarget! as HTMLElement).getAttribute( + "data-id" + )!; + this._zoomZone(entityId); + } + + private _zoomZone(id: string) { + this._map?.fitMarker(id); + } + + private _openEditEntry(ev: MouseEvent) { + const entry: Zone = (ev.currentTarget! as any).entry; + this._openDialog(entry); + } + + private async _createEntry(values: ZoneMutableParams) { + const created = await createZone(this.hass!, values); + this._storageItems = this._storageItems!.concat( + created + ).sort((ent1, ent2) => compare(ent1.name, ent2.name)); + if (this.narrow) { + return; + } + await this.updateComplete; + this._activeEntry = created.id; + this._map?.fitMarker(created.id); + } + + private async _updateEntry( + entry: Zone, + values: Partial, + fitMap: boolean = false + ) { + const updated = await updateZone(this.hass!, entry!.id, values); + this._storageItems = this._storageItems!.map((ent) => + ent === entry ? updated : ent + ); + if (this.narrow || !fitMap) { + return; + } + await this.updateComplete; + this._activeEntry = entry.id; + this._map?.fitMarker(entry.id); + } + + private async _removeEntry(entry: Zone) { + if ( + !confirm(`${this.hass!.localize("ui.panel.config.zone.confirm_delete")} + +${this.hass!.localize("ui.panel.config.zone.confirm_delete2")}`) + ) { + return false; + } + + try { + await deleteZone(this.hass!, entry!.id); + this._storageItems = this._storageItems!.filter((ent) => ent !== entry); + if (!this.narrow) { + this._map?.fitMap(); + } + return true; + } catch (err) { + return false; + } + } + + private async _openDialog(entry?: Zone) { + showZoneDetailDialog(this, { + entry, + createEntry: (values) => this._createEntry(values), + updateEntry: entry + ? (values) => this._updateEntry(entry, values, true) + : undefined, + removeEntry: entry ? () => this._removeEntry(entry) : undefined, + }); + } + + static get styles(): CSSResult { + return css` + hass-loading-screen { + --app-header-background-color: var(--sidebar-background-color); + --app-header-text-color: var(--sidebar-text-color); + } + a { + color: var(--primary-color); + } + ha-card { + max-width: 600px; + margin: 16px auto; + overflow: hidden; + } + ha-icon, + paper-icon-button:not([disabled]) { + color: var(--secondary-text-color); + } + .empty { + text-align: center; + padding: 8px; + } + .flex { + display: flex; + height: 100%; + } + ha-locations-editor { + flex-grow: 1; + height: 100%; + } + .flex paper-listbox, + .flex .empty { + border-left: 1px solid var(--divider-color); + width: 250px; + } + paper-icon-item { + padding-top: 4px; + padding-bottom: 4px; + } + paper-icon-item.iron-selected:before { + border-radius: 4px; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + pointer-events: none; + content: ""; + background-color: var(--sidebar-selected-icon-color); + opacity: 0.12; + transition: opacity 15ms linear; + will-change: opacity; + } + ha-card paper-item { + cursor: pointer; + } + ha-fab { + position: fixed; + bottom: 16px; + right: 16px; + z-index: 1; + } + ha-fab[is-wide] { + bottom: 24px; + right: 24px; + } + ha-fab[narrow] { + bottom: 84px; + } + `; + } +} diff --git a/src/panels/config/zone/show-dialog-zone-detail.ts b/src/panels/config/zone/show-dialog-zone-detail.ts new file mode 100644 index 0000000000..58a9c9261a --- /dev/null +++ b/src/panels/config/zone/show-dialog-zone-detail.ts @@ -0,0 +1,23 @@ +import { fireEvent } from "../../../common/dom/fire_event"; +import { Zone, ZoneMutableParams } from "../../../data/zone"; + +export interface ZoneDetailDialogParams { + entry?: Zone; + createEntry: (values: ZoneMutableParams) => Promise; + updateEntry?: (updates: Partial) => Promise; + removeEntry?: () => Promise; +} + +export const loadZoneDetailDialog = () => + import(/* webpackChunkName: "zone-detail-dialog" */ "./dialog-zone-detail"); + +export const showZoneDetailDialog = ( + element: HTMLElement, + systemLogDetailParams: ZoneDetailDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-zone-detail", + dialogImport: loadZoneDetailDialog, + dialogParams: systemLogDetailParams, + }); +}; diff --git a/src/panels/config/zwave/ha-config-zwave.js b/src/panels/config/zwave/ha-config-zwave.js index ccfaaa09ef..c723834ad5 100644 --- a/src/panels/config/zwave/ha-config-zwave.js +++ b/src/panels/config/zwave/ha-config-zwave.js @@ -76,10 +76,14 @@ class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) { color: grey; } - [hidden] { + ha-service-description[hidden] { display: none; } + ha-paper-icon-button-arrow-prev[hide] { + visibility: hidden; + } + .toggle-help-icon { position: absolute; top: -6px; @@ -91,6 +95,7 @@ class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) {
diff --git a/src/panels/config/zwave/zwave-network.ts b/src/panels/config/zwave/zwave-network.ts index 03d0423f61..4f178eb6d5 100644 --- a/src/panels/config/zwave/zwave-network.ts +++ b/src/panels/config/zwave/zwave-network.ts @@ -48,7 +48,7 @@ export class ZwaveNetwork extends LitElement { this._subscribe(); } - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html`
diff --git a/src/panels/config/zwave/zwave-node-config.ts b/src/panels/config/zwave/zwave-node-config.ts index ed0dab5edd..9b38099a2c 100644 --- a/src/panels/config/zwave/zwave-node-config.ts +++ b/src/panels/config/zwave/zwave-node-config.ts @@ -37,7 +37,7 @@ export class ZwaveNodeConfig extends LitElement { @property() private _selectedConfigParameter: number = -1; @property() private _selectedConfigValue: number | string = -1; - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html`
diff --git a/src/panels/developer-tools/info/developer-tools-info.ts b/src/panels/developer-tools/info/developer-tools-info.ts index 9e86c9df61..7141d4b132 100644 --- a/src/panels/developer-tools/info/developer-tools-info.ts +++ b/src/panels/developer-tools/info/developer-tools-info.ts @@ -19,7 +19,7 @@ const OPT_IN_PANEL = "states"; class HaPanelDevInfo extends LitElement { @property() public hass!: HomeAssistant; - protected render(): TemplateResult | void { + protected render(): TemplateResult { const hass = this.hass; const customUiList: Array<{ name: string; url: string; version: string }> = (window as any).CUSTOM_UI_LIST || []; @@ -32,7 +32,9 @@ class HaPanelDevInfo extends LitElement { const nonDefaultLinkText = localStorage.defaultPage === OPT_IN_PANEL && OPT_IN_PANEL === "states" ? this.hass.localize("ui.panel.developer-tools.tabs.info.lovelace_ui") - : this.hass.localize("ui.panel.developer-tools.tabs.info.states_ui"); + : `${this.hass.localize( + "ui.panel.developer-tools.tabs.info.states_ui" + )} (DEPRECATED)`; const defaultPageText = `${this.hass.localize( "ui.panel.developer-tools.tabs.info.default_ui", @@ -41,7 +43,7 @@ class HaPanelDevInfo extends LitElement { ? this.hass.localize("ui.panel.developer-tools.tabs.info.remove") : this.hass.localize("ui.panel.developer-tools.tabs.info.set"), "name", - OPT_IN_PANEL + `${OPT_IN_PANEL} (DEPRECATED)` )}`; return html` diff --git a/src/panels/developer-tools/info/system-health-card.ts b/src/panels/developer-tools/info/system-health-card.ts index b0acaa3b1a..35e1976d9e 100644 --- a/src/panels/developer-tools/info/system-health-card.ts +++ b/src/panels/developer-tools/info/system-health-card.ts @@ -35,9 +35,9 @@ class SystemHealthCard extends LitElement { @property() public hass!: HomeAssistant; @property() private _info?: SystemHealthInfo; - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this.hass) { - return; + return html``; } const sections: TemplateResult[] = []; diff --git a/src/panels/developer-tools/logs/developer-tools-logs.ts b/src/panels/developer-tools/logs/developer-tools-logs.ts index 5f6a0a5c91..fdb8fbb272 100644 --- a/src/panels/developer-tools/logs/developer-tools-logs.ts +++ b/src/panels/developer-tools/logs/developer-tools-logs.ts @@ -30,7 +30,7 @@ export class HaPanelDevLogs extends LitElement { } } - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html`
diff --git a/src/panels/developer-tools/logs/dialog-system-log-detail.ts b/src/panels/developer-tools/logs/dialog-system-log-detail.ts index ed9db2e1bb..e7a89162a9 100644 --- a/src/panels/developer-tools/logs/dialog-system-log-detail.ts +++ b/src/panels/developer-tools/logs/dialog-system-log-detail.ts @@ -24,7 +24,7 @@ class DialogSystemLogDetail extends LitElement { await this.updateComplete; } - protected render(): TemplateResult | void { + protected render(): TemplateResult { if (!this._params) { return html``; } diff --git a/src/panels/developer-tools/logs/error-log-card.ts b/src/panels/developer-tools/logs/error-log-card.ts index 99269b80e9..da74ce1a77 100644 --- a/src/panels/developer-tools/logs/error-log-card.ts +++ b/src/panels/developer-tools/logs/error-log-card.ts @@ -16,7 +16,7 @@ class ErrorLogCard extends LitElement { @property() public hass!: HomeAssistant; @property() private _errorLog?: string; - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html`

${this._errorLog diff --git a/src/panels/developer-tools/logs/system-log-card.ts b/src/panels/developer-tools/logs/system-log-card.ts index ec11cc0775..ab0f303ca5 100644 --- a/src/panels/developer-tools/logs/system-log-card.ts +++ b/src/panels/developer-tools/logs/system-log-card.ts @@ -41,7 +41,7 @@ export class SystemLogCard extends LitElement { this._items = await fetchSystemLog(this.hass!); } - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html`

diff --git a/src/panels/developer-tools/service/developer-tools-service.js b/src/panels/developer-tools/service/developer-tools-service.js index b9bb843422..f4a057f910 100644 --- a/src/panels/developer-tools/service/developer-tools-service.js +++ b/src/panels/developer-tools/service/developer-tools-service.js @@ -11,6 +11,7 @@ import "../../../components/ha-service-picker"; import "../../../resources/ha-style"; import "../../../util/app-localstorage-document"; import LocalizeMixin from "../../../mixins/localize-mixin"; +import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; const ERROR_SENTINEL = {}; /* @@ -290,14 +291,13 @@ class HaPanelDevService extends LocalizeMixin(PolymerElement) { _callService() { if (this.parsedJSON === ERROR_SENTINEL) { - // eslint-disable-next-line - alert( - this.hass.localize( + showAlertDialog(this, { + text: this.hass.localize( "ui.panel.developer-tools.tabs.services.alert_parsing_yaml", "data", this.serviceData - ) - ); + ), + }); return; } diff --git a/src/panels/developer-tools/state/developer-tools-state.js b/src/panels/developer-tools/state/developer-tools-state.js index 8312f15e34..9cc5fb6bc8 100644 --- a/src/panels/developer-tools/state/developer-tools-state.js +++ b/src/panels/developer-tools/state/developer-tools-state.js @@ -11,6 +11,7 @@ import "../../../components/ha-code-editor"; import "../../../resources/ha-style"; import { EventsMixin } from "../../../mixins/events-mixin"; import LocalizeMixin from "../../../mixins/localize-mixin"; +import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; const ERROR_SENTINEL = {}; /* @@ -257,11 +258,11 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) { handleSetState() { if (!this._entityId) { - alert( - this.hass.localize( + showAlertDialog(this, { + text: this.hass.localize( "ui.panel.developer-tools.tabs.states.alert_entity_field" - ) - ); + ), + }); return; } this.hass.callApi("POST", "states/" + this._entityId, { diff --git a/src/panels/logbook/ha-logbook.js b/src/panels/logbook/ha-logbook.js deleted file mode 100644 index 3faa6be64f..0000000000 --- a/src/panels/logbook/ha-logbook.js +++ /dev/null @@ -1,137 +0,0 @@ -import "@polymer/iron-flex-layout/iron-flex-layout-classes"; -import "@polymer/iron-icon/iron-icon"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -import { PolymerElement } from "@polymer/polymer/polymer-element"; - -import formatTime from "../../common/datetime/format_time"; -import formatDate from "../../common/datetime/format_date"; -import { EventsMixin } from "../../mixins/events-mixin"; -import LocalizeMixin from "../../mixins/localize-mixin"; -import { domainIcon } from "../../common/entity/domain_icon"; -import { computeRTL } from "../../common/util/compute_rtl"; - -/* - * @appliesMixin EventsMixin - */ -class HaLogbook extends LocalizeMixin(EventsMixin(PolymerElement)) { - static get template() { - return html` - - - - - - - `; - } - - static get properties() { - return { - hass: { - type: Object, - }, - - entries: { - type: Array, - value: [], - }, - rtl: { - type: Boolean, - reflectToAttribute: true, - computed: "_computeRTL(hass)", - }, - }; - } - - _formatTime(date) { - return formatTime(new Date(date), this.hass.language); - } - - _formatDate(date) { - return formatDate(new Date(date), this.hass.language); - } - - _needHeader(change, index) { - if (!index) return true; - const current = this.get("when", change.base[index]); - const previous = this.get("when", change.base[index - 1]); - return ( - current && - previous && - new Date(current).toDateString() !== new Date(previous).toDateString() - ); - } - - _computeIcon(domain) { - return domainIcon(domain); - } - - _computeRTL(hass) { - return computeRTL(hass); - } - - entityClicked(ev) { - ev.preventDefault(); - this.fire("hass-more-info", { entityId: ev.model.item.entity_id }); - } -} - -customElements.define("ha-logbook", HaLogbook); diff --git a/src/panels/logbook/ha-logbook.ts b/src/panels/logbook/ha-logbook.ts new file mode 100644 index 0000000000..86a0f8455a --- /dev/null +++ b/src/panels/logbook/ha-logbook.ts @@ -0,0 +1,159 @@ +import "../../components/ha-icon"; +import formatTime from "../../common/datetime/format_time"; +import formatDate from "../../common/datetime/format_date"; +import { domainIcon } from "../../common/entity/domain_icon"; +import { stateIcon } from "../../common/entity/state_icon"; +import { computeRTL } from "../../common/util/compute_rtl"; +import { + LitElement, + html, + property, + TemplateResult, + CSSResult, + css, + PropertyValues, +} from "lit-element"; +import { HomeAssistant } from "../../types"; +import { fireEvent } from "../../common/dom/fire_event"; +import "lit-virtualizer"; +import { LogbookEntry } from "../../data/logbook"; + +class HaLogbook extends LitElement { + @property() public hass!: HomeAssistant; + @property() public entries: LogbookEntry[] = []; + @property({ attribute: "rtl", type: Boolean, reflect: true }) + // @ts-ignore + private _rtl = false; + + protected updated(changedProps: PropertyValues) { + super.updated(changedProps); + if (!changedProps.has("hass")) { + return; + } + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + if (oldHass && oldHass.language !== this.hass.language) { + this._rtl = computeRTL(this.hass); + } + } + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + this._rtl = computeRTL(this.hass); + } + + protected render(): TemplateResult { + if (!this.entries?.length) { + return html` + ${this.hass.localize("ui.panel.logbook.entries_not_found")} + `; + } + + return html` + + this._renderLogbookItem(item, index)} + style="height: 100%;" + > + `; + } + + private _renderLogbookItem( + item: LogbookEntry, + index: number + ): TemplateResult { + const previous = this.entries[index - 1]; + const state = item.entity_id ? this.hass.states[item.entity_id] : undefined; + return html` +
+ ${index === 0 || + (item?.when && + previous?.when && + new Date(item.when).toDateString() !== + new Date(previous.when).toDateString()) + ? html` +

+ ${formatDate(new Date(item.when), this.hass.language)} +

+ ` + : html``} + +
+
+ ${formatTime(new Date(item.when), this.hass.language)} +
+ +
+ ${!item.entity_id + ? html` + ${item.name} + ` + : html` + + ${item.name} + + `} + ${item.message} +
+
+
+ `; + } + + private _entityClicked(ev: Event) { + ev.preventDefault(); + fireEvent(this, "hass-more-info", { + entityId: (ev.target as any).entityId, + }); + } + + static get styles(): CSSResult { + return css` + :host { + display: block; + height: 100%; + } + + :host([rtl]) { + direction: ltr; + } + + .entry { + display: flex; + line-height: 2em; + } + + .time { + width: 55px; + font-size: 0.8em; + color: var(--secondary-text-color); + } + + :host([rtl]) .date { + direction: rtl; + } + + ha-icon { + margin: 0 8px 0 16px; + color: var(--primary-text-color); + } + + .message { + color: var(--primary-text-color); + } + + a { + color: var(--primary-color); + } + `; + } +} + +customElements.define("ha-logbook", HaLogbook); diff --git a/src/panels/logbook/ha-panel-logbook.js b/src/panels/logbook/ha-panel-logbook.js index 7e46b66979..1f72e8ccba 100644 --- a/src/panels/logbook/ha-panel-logbook.js +++ b/src/panels/logbook/ha-panel-logbook.js @@ -28,7 +28,15 @@ class HaPanelLogbook extends LocalizeMixin(PolymerElement) { return html` + DEPRECATED
diff --git a/src/state-summary/state-card-input_select.ts b/src/state-summary/state-card-input_select.ts index c447d0e4e8..87d0c36b73 100644 --- a/src/state-summary/state-card-input_select.ts +++ b/src/state-summary/state-card-input_select.ts @@ -27,7 +27,7 @@ class StateCardInputSelect extends LitElement { @property() public hass!: HomeAssistant; @property() public stateObj!: InputSelectEntity; - protected render(): TemplateResult | void { + protected render(): TemplateResult { return html`