From c17ebfd2797d79f0de2e6948a0abe7f7f1c9ba17 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 6 Aug 2020 23:42:10 +0200 Subject: [PATCH] Show small header on ingress panels when the sidebar is hidden (#6488) --- .../markdown/dialog-hassio-markdown.ts | 10 +- hassio/src/hassio-main.ts | 256 ++---------------- hassio/src/hassio-panel.ts | 15 + hassio/src/hassio-router.ts | 150 ++++++++++ .../src/ingress-view/hassio-ingress-view.ts | 137 ++++++++-- src/state/url-sync-mixin.ts | 19 +- 6 files changed, 323 insertions(+), 264 deletions(-) create mode 100644 hassio/src/hassio-router.ts diff --git a/hassio/src/dialogs/markdown/dialog-hassio-markdown.ts b/hassio/src/dialogs/markdown/dialog-hassio-markdown.ts index 907904c8a3..6b36d31e5c 100644 --- a/hassio/src/dialogs/markdown/dialog-hassio-markdown.ts +++ b/hassio/src/dialogs/markdown/dialog-hassio-markdown.ts @@ -31,6 +31,10 @@ class HassioMarkdownDialog extends LitElement { this._opened = true; } + public closeDialog() { + this._opened = false; + } + protected render(): TemplateResult { if (!this._opened) { return html``; @@ -38,7 +42,7 @@ class HassioMarkdownDialog extends LitElement { return html` @@ -46,10 +50,6 @@ class HassioMarkdownDialog extends LitElement { `; } - private _closeDialog(): void { - this._opened = false; - } - static get styles(): CSSResult[] { return [ haStyleDialog, diff --git a/hassio/src/hassio-main.ts b/hassio/src/hassio-main.ts index 1fd42fa091..d20c690e30 100644 --- a/hassio/src/hassio-main.ts +++ b/hassio/src/hassio-main.ts @@ -1,93 +1,29 @@ -import { PolymerElement } from "@polymer/polymer"; import { - customElement, - property, - internalProperty, + html, PropertyValues, + customElement, + LitElement, + property, } from "lit-element"; +import "./hassio-router"; +import { urlSyncMixin } from "../../src/state/url-sync-mixin"; +import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin"; +import { HomeAssistant, Route } from "../../src/types"; +import { HassioPanelInfo } from "../../src/data/hassio/supervisor"; import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element"; import { fireEvent } from "../../src/common/dom/fire_event"; -import { navigate } from "../../src/common/navigate"; -import { atLeastVersion } from "../../src/common/config/version"; -import { fetchHassioAddonInfo } from "../../src/data/hassio/addon"; -import { - fetchHassioHassOsInfo, - fetchHassioHostInfo, - HassioHassOSInfo, - HassioHostInfo, -} from "../../src/data/hassio/host"; -import { - createHassioSession, - fetchHassioHomeAssistantInfo, - fetchHassioSupervisorInfo, - fetchHassioInfo, - HassioHomeAssistantInfo, - HassioInfo, - HassioPanelInfo, - HassioSupervisorInfo, -} from "../../src/data/hassio/supervisor"; -import { - AlertDialogParams, - showAlertDialog, -} from "../../src/dialogs/generic/show-dialog-box"; import { makeDialogManager } from "../../src/dialogs/make-dialog-manager"; -import { - HassRouterPage, - RouterOptions, -} from "../../src/layouts/hass-router-page"; -import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin"; -import "../../src/resources/ha-style"; -import { HomeAssistant } from "../../src/types"; -// Don't codesplit it, that way the dashboard always loads fast. -import "./hassio-panel"; +import { atLeastVersion } from "../../src/common/config/version"; @customElement("hassio-main") -class HassioMain extends ProvideHassLitMixin(HassRouterPage) { +export class HassioMain extends urlSyncMixin(ProvideHassLitMixin(LitElement)) { @property({ attribute: false }) public hass!: HomeAssistant; @property() public panel!: HassioPanelInfo; @property() public narrow!: boolean; - protected routerOptions: RouterOptions = { - // Hass.io has a page with tabs, so we route all non-matching routes to it. - defaultPage: "dashboard", - initialLoad: () => this._fetchData(), - showLoading: true, - routes: { - dashboard: { - tag: "hassio-panel", - cache: true, - }, - snapshots: "dashboard", - store: "dashboard", - system: "dashboard", - addon: { - tag: "hassio-addon-dashboard", - load: () => - import( - /* webpackChunkName: "hassio-addon-dashboard" */ "./addon-view/hassio-addon-dashboard" - ), - }, - ingress: { - tag: "hassio-ingress-view", - load: () => - import( - /* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view" - ), - }, - }, - }; - - @internalProperty() private _supervisorInfo: HassioSupervisorInfo; - - @internalProperty() private _hostInfo: HassioHostInfo; - - @internalProperty() private _hassioInfo?: HassioInfo; - - @internalProperty() private _hassOsInfo?: HassioHassOSInfo; - - @internalProperty() private _hassInfo: HassioHomeAssistantInfo; + @property() public route?: Route; protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); @@ -102,20 +38,6 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) { this.hass.selectedTheme ); - 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)" - ); - - this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev)); // Paulus - March 17, 2019 // We went to a single hass-toggle-menu event in HA 0.90. However, the // supervisor UI can also run under older versions of Home Assistant. @@ -148,152 +70,18 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) { }); }); - makeDialogManager(this, document.body); + makeDialogManager(this, this.shadowRoot!); } - protected updatePageEl(el) { - // the tabs page does its own routing so needs full route. - const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail; - - if ("setProperties" in el) { - // As long as we have Polymer pages - (el as PolymerElement).setProperties({ - hass: this.hass, - narrow: this.narrow, - supervisorInfo: this._supervisorInfo, - hassioInfo: this._hassioInfo, - hostInfo: this._hostInfo, - hassInfo: this._hassInfo, - hassOsInfo: this._hassOsInfo, - route, - }); - } else { - el.hass = this.hass; - el.narrow = this.narrow; - el.supervisorInfo = this._supervisorInfo; - el.hassioInfo = this._hassioInfo; - el.hostInfo = this._hostInfo; - el.hassInfo = this._hassInfo; - el.hassOsInfo = this._hassOsInfo; - el.route = route; - } - } - - private async _fetchData() { - if (this.panel.config && this.panel.config.ingress) { - await this._redirectIngress(this.panel.config.ingress); - return; - } - - const [supervisorInfo, hostInfo, hassInfo, hassioInfo] = await Promise.all([ - fetchHassioSupervisorInfo(this.hass), - fetchHassioHostInfo(this.hass), - fetchHassioHomeAssistantInfo(this.hass), - fetchHassioInfo(this.hass), - ]); - this._supervisorInfo = supervisorInfo; - this._hassioInfo = hassioInfo; - this._hostInfo = hostInfo; - this._hassInfo = hassInfo; - - if (this._hostInfo.features && this._hostInfo.features.includes("hassos")) { - this._hassOsInfo = await fetchHassioHassOsInfo(this.hass); - } - } - - private async _redirectIngress(addonSlug: string) { - // When we trigger a navigation, we sleep to make sure we don't - // show the hassio dashboard before navigating away. - const awaitAlert = async ( - alertParams: AlertDialogParams, - action: () => void - ) => { - await new Promise((resolve) => { - alertParams.confirm = resolve; - showAlertDialog(this, alertParams); - }); - action(); - await new Promise((resolve) => setTimeout(resolve, 1000)); - }; - - const createSessionPromise = createHassioSession(this.hass).then( - () => true, - () => false - ); - - let addon; - - try { - addon = await fetchHassioAddonInfo(this.hass, addonSlug); - } catch (err) { - await awaitAlert( - { - text: "Unable to fetch add-on info to start Ingress", - title: "Supervisor", - }, - () => history.back() - ); - - return; - } - - if (!addon.ingress_url) { - await awaitAlert( - { - text: "Add-on does not support Ingress", - title: addon.name, - }, - () => history.back() - ); - - return; - } - - if (addon.state !== "started") { - await awaitAlert( - { - text: "Add-on is not running. Please start it first", - title: addon.name, - }, - () => navigate(this, `/hassio/addon/${addon.slug}/info`, true) - ); - - return; - } - - if (!(await createSessionPromise)) { - await awaitAlert( - { - text: "Unable to create an Ingress session", - title: addon.name, - }, - () => history.back() - ); - - 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); - } - - private _apiCalled(ev) { - if (!ev.detail.success) { - return; - } - - let tries = 1; - - const tryUpdate = () => { - this._fetchData().catch(() => { - tries += 1; - setTimeout(tryUpdate, Math.min(tries, 5) * 1000); - }); - }; - - tryUpdate(); + protected render() { + return html` + + `; } } diff --git a/hassio/src/hassio-panel.ts b/hassio/src/hassio-panel.ts index 471c717970..245f8ba1ee 100644 --- a/hassio/src/hassio-panel.ts +++ b/hassio/src/hassio-panel.ts @@ -4,6 +4,8 @@ import { LitElement, property, TemplateResult, + css, + CSSResult, } from "lit-element"; import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host"; import { @@ -33,6 +35,9 @@ class HassioPanel extends LitElement { @property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo; protected render(): TemplateResult { + if (!this.supervisorInfo) { + return html``; + } return html` `; } + + static get styles(): CSSResult { + return css` + :host { + --app-header-background-color: var(--sidebar-background-color); + --app-header-text-color: var(--sidebar-text-color); + --app-header-border-bottom: 1px solid var(--divider-color); + } + `; + } } declare global { diff --git a/hassio/src/hassio-router.ts b/hassio/src/hassio-router.ts new file mode 100644 index 0000000000..a3ecdaf784 --- /dev/null +++ b/hassio/src/hassio-router.ts @@ -0,0 +1,150 @@ +import { + customElement, + property, + internalProperty, + PropertyValues, +} from "lit-element"; +import { + fetchHassioHassOsInfo, + fetchHassioHostInfo, + HassioHassOSInfo, + HassioHostInfo, +} from "../../src/data/hassio/host"; +import { + fetchHassioHomeAssistantInfo, + fetchHassioSupervisorInfo, + fetchHassioInfo, + HassioHomeAssistantInfo, + HassioInfo, + HassioPanelInfo, + HassioSupervisorInfo, +} from "../../src/data/hassio/supervisor"; +import { + HassRouterPage, + RouterOptions, +} from "../../src/layouts/hass-router-page"; +import "../../src/resources/ha-style"; +import { HomeAssistant } from "../../src/types"; +// Don't codesplit it, that way the dashboard always loads fast. +import "./hassio-panel"; + +@customElement("hassio-router") +class HassioRouter extends HassRouterPage { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public panel!: HassioPanelInfo; + + @property() public narrow!: boolean; + + protected routerOptions: RouterOptions = { + // Hass.io has a page with tabs, so we route all non-matching routes to it. + defaultPage: "dashboard", + initialLoad: () => this._fetchData(), + showLoading: true, + routes: { + dashboard: { + tag: "hassio-panel", + cache: true, + }, + snapshots: "dashboard", + store: "dashboard", + system: "dashboard", + addon: { + tag: "hassio-addon-dashboard", + load: () => + import( + /* webpackChunkName: "hassio-addon-dashboard" */ "./addon-view/hassio-addon-dashboard" + ), + }, + ingress: { + tag: "hassio-ingress-view", + load: () => + import( + /* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view" + ), + }, + }, + }; + + @internalProperty() private _supervisorInfo: HassioSupervisorInfo; + + @internalProperty() private _hostInfo: HassioHostInfo; + + @internalProperty() private _hassioInfo?: HassioInfo; + + @internalProperty() private _hassOsInfo?: HassioHassOSInfo; + + @internalProperty() private _hassInfo: HassioHomeAssistantInfo; + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev)); + } + + protected updatePageEl(el) { + // the tabs page does its own routing so needs full route. + const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail; + + el.hass = this.hass; + el.narrow = this.narrow; + el.supervisorInfo = this._supervisorInfo; + el.hassioInfo = this._hassioInfo; + el.hostInfo = this._hostInfo; + el.hassInfo = this._hassInfo; + el.hassOsInfo = this._hassOsInfo; + el.route = route; + + if (el.localName === "hassio-ingress-view") { + el.ingressPanel = this.panel.config && this.panel.config.ingress; + } + } + + private async _fetchData() { + if (this.panel.config && this.panel.config.ingress) { + this._redirectIngress(this.panel.config.ingress); + return; + } + + const [supervisorInfo, hostInfo, hassInfo, hassioInfo] = await Promise.all([ + fetchHassioSupervisorInfo(this.hass), + fetchHassioHostInfo(this.hass), + fetchHassioHomeAssistantInfo(this.hass), + fetchHassioInfo(this.hass), + ]); + this._supervisorInfo = supervisorInfo; + this._hassioInfo = hassioInfo; + this._hostInfo = hostInfo; + this._hassInfo = hassInfo; + + if (this._hostInfo.features && this._hostInfo.features.includes("hassos")) { + this._hassOsInfo = await fetchHassioHassOsInfo(this.hass); + } + } + + private _redirectIngress(addonSlug: string) { + this.route = { prefix: "/hassio", path: `/ingress/${addonSlug}` }; + } + + private _apiCalled(ev) { + if (!ev.detail.success) { + return; + } + + let tries = 1; + + const tryUpdate = () => { + this._fetchData().catch(() => { + tries += 1; + setTimeout(tryUpdate, Math.min(tries, 5) * 1000); + }); + }; + + tryUpdate(); + } +} + +declare global { + interface HTMLElementTagNameMap { + "hassio-router": HassioRouter; + } +} diff --git a/hassio/src/ingress-view/hassio-ingress-view.ts b/hassio/src/ingress-view/hassio-ingress-view.ts index 42cf1b373a..8780d9617d 100644 --- a/hassio/src/ingress-view/hassio-ingress-view.ts +++ b/hassio/src/ingress-view/hassio-ingress-view.ts @@ -17,6 +17,10 @@ import { createHassioSession } from "../../../src/data/hassio/supervisor"; import "../../../src/layouts/hass-loading-screen"; import "../../../src/layouts/hass-subpage"; import { HomeAssistant, Route } from "../../../src/types"; +import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; +import { navigate } from "../../../src/common/navigate"; +import { mdiMenu } from "@mdi/js"; +import { fireEvent } from "../../../src/common/dom/fire_event"; @customElement("hassio-ingress-view") class HassioIngressView extends LitElement { @@ -24,22 +28,45 @@ class HassioIngressView extends LitElement { @property() public route!: Route; + @property() public ingressPanel = false; + @internalProperty() private _addon?: HassioAddonDetails; + @property({ type: Boolean }) + public narrow = false; + protected render(): TemplateResult { if (!this._addon) { return html` `; } - return html` - - - - `; + const iframe = html``; + + if (!this.ingressPanel) { + return html` + ${iframe} + `; + } + + return html`${this.narrow || this.hass.dockedSidebar === "always_hidden" + ? html`
+ + + +
${this._addon.name}
+
+ ${iframe}` + : iframe}`; } protected updated(changedProps: PropertyValues) { - super.firstUpdated(changedProps); + super.updated(changedProps); if (!changedProps.has("route")) { return; @@ -56,27 +83,56 @@ class HassioIngressView extends LitElement { } private async _fetchData(addonSlug: string) { + const createSessionPromise = createHassioSession(this.hass).then( + () => true, + () => false + ); + + let addon; + try { - const [addon] = await Promise.all([ - fetchHassioAddonInfo(this.hass, addonSlug).catch(() => { - throw new Error("Failed to fetch add-on info"); - }), - createHassioSession(this.hass).catch(() => { - throw new Error("Failed to create an ingress session"); - }), - ]); - - if (!addon.ingress) { - throw new Error("This add-on does not support ingress"); - } - - this._addon = addon; + addon = await fetchHassioAddonInfo(this.hass, addonSlug); } catch (err) { - // eslint-disable-next-line - console.error(err); - alert(err.message || "Unknown error starting ingress."); + await showAlertDialog(this, { + text: "Unable to fetch add-on info to start Ingress", + title: "Supervisor", + }); history.back(); + return; } + + if (!addon.ingress_url) { + await showAlertDialog(this, { + text: "Add-on does not support Ingress", + title: addon.name, + }); + history.back(); + return; + } + + if (addon.state !== "started") { + await showAlertDialog(this, { + text: "Add-on is not running. Please start it first", + title: addon.name, + }); + navigate(this, `/hassio/addon/${addon.slug}/info`, true); + return; + } + + if (!(await createSessionPromise)) { + await showAlertDialog(this, { + text: "Unable to create an Ingress session", + title: addon.name, + }); + history.back(); + return; + } + + this._addon = addon; + } + + private _toggleMenu(): void { + fireEvent(this, "hass-toggle-menu"); } static get styles(): CSSResult { @@ -87,6 +143,41 @@ class HassioIngressView extends LitElement { height: 100%; border: 0; } + + .header + iframe { + height: calc(100% - 40px); + } + + .header { + display: flex; + align-items: center; + font-size: 16px; + height: 40px; + padding: 0 16px; + pointer-events: none; + background-color: var(--app-header-background-color); + font-weight: 400; + color: var(--app-header-text-color, white); + border-bottom: var(--app-header-border-bottom, none); + box-sizing: border-box; + --mdc-icon-size: 20px; + } + + .main-title { + margin: 0 0 0 24px; + line-height: 20px; + flex-grow: 1; + } + + mwc-icon-button { + pointer-events: auto; + } + + hass-subpage { + --app-header-background-color: var(--sidebar-background-color); + --app-header-text-color: var(--sidebar-text-color); + --app-header-border-bottom: 1px solid var(--divider-color); + } `; } } diff --git a/src/state/url-sync-mixin.ts b/src/state/url-sync-mixin.ts index e564963cc1..12919204e5 100644 --- a/src/state/url-sync-mixin.ts +++ b/src/state/url-sync-mixin.ts @@ -6,12 +6,15 @@ import { DialogClosedParams, } from "../dialogs/make-dialog-manager"; import { Constructor } from "../types"; -import { HassBaseEl } from "./hass-base-mixin"; import { HASSDomEvent } from "../common/dom/fire_event"; +import { UpdatingElement } from "lit-element"; +import { ProvideHassElement } from "../mixins/provide-hass-lit-mixin"; const DEBUG = false; -export const urlSyncMixin = >( +export const urlSyncMixin = < + T extends Constructor +>( superClass: T ) => // Disable this functionality in the demo. @@ -35,11 +38,17 @@ export const urlSyncMixin = >( private _dialogClosedListener = ( ev: HASSDomEvent ) => { + if (DEBUG) { + console.log("dialog closed", ev.detail.dialog); + } // If not closed by navigating back, and not a new dialog is open, remove the open state from history if ( history.state?.open && history.state?.dialog === ev.detail.dialog ) { + if (DEBUG) { + console.log("remove state", ev.detail.dialog); + } this._ignoreNextPopState = true; history.back(); } @@ -65,6 +74,9 @@ export const urlSyncMixin = >( if (!state.open) { const closed = await closeDialog(state.dialog); if (!closed) { + if (DEBUG) { + console.log("dialog could not be closed"); + } // dialog could not be closed, push state again history.pushState( { @@ -78,6 +90,9 @@ export const urlSyncMixin = >( return; } if (state.oldState) { + if (DEBUG) { + console.log("handle old state"); + } this._handleDialogStateChange(state.oldState); } return;