Add hassio ingress support (#3062)

* Add hassio ingress support

* Remove logging

* Better integrate

* Add badge

* FIx type
This commit is contained in:
Paulus Schoutsen 2019-04-06 00:28:08 -07:00 committed by Pascal Vizeli
parent 7f99f1d9be
commit b07f95f956
7 changed files with 291 additions and 9 deletions

View File

@ -10,6 +10,7 @@ import "../../../src/components/ha-markdown";
import "../../../src/components/buttons/ha-call-api-button";
import "../../../src/resources/ha-style";
import EventsMixin from "../../../src/mixins/events-mixin";
import { navigate } from "../../../src/common/navigate";
import "../components/hassio-card-content";
@ -59,6 +60,11 @@ const PERMIS_DESC = {
description:
"An addon 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) {
@ -310,6 +316,15 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
description=""
></ha-label-badge>
</template>
<template is="dom-if" if="[[addon.ingress]]">
<ha-label-badge
on-click="showMoreInfo"
id="ingress"
icon="hassio:cursor-default-click-outline"
label="ingress"
description=""
></ha-label-badge>
</template>
</div>
<template is="dom-if" if="[[addon.version]]">
<div class="state">
@ -371,7 +386,7 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
</template>
<template
is="dom-if"
if="[[computeShowWebUI(addon.webui, isRunning)]]"
if="[[computeShowWebUI(addon.ingress, addon.webui, isRunning)]]"
>
<a
href="[[pathWebui(addon.webui)]]"
@ -381,6 +396,16 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
><mwc-button>Open web UI</mwc-button></a
>
</template>
<template
is="dom-if"
if="[[computeShowIngressUI(addon.ingress, isRunning)]]"
>
<mwc-button
tabindex="-1"
class="right"
on-click="openIngress"
>Open web UI</mwc-button>
</template>
</template>
<template is="dom-if" if="[[!addon.version]]">
<template is="dom-if" if="[[!addon.available]]">
@ -448,8 +473,16 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
return webui && webui.replace("[HOST]", document.location.hostname);
}
computeShowWebUI(webui, isRunning) {
return webui && isRunning;
computeShowWebUI(ingress, webui, isRunning) {
return !ingress && webui && isRunning;
}
openIngress() {
navigate(this, `/hassio/ingress/${this.addon.slug}`);
}
computeShowIngressUI(ingress, isRunning) {
return ingress && isRunning;
}
computeStartOnBoot(state) {

View File

@ -2,4 +2,16 @@ window.loadES5Adapter().then(() => {
import(/* webpackChunkName: "hassio-icons" */ "./resources/hassio-icons.js");
import(/* webpackChunkName: "hassio-main" */ "./hassio-main.js");
});
document.body.style.height = "100%";
const styleEl = document.createElement("style");
styleEl.innerHTML = `
body {
font-family: Roboto, sans-serif;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-weight: 400;
margin: 0;
padding: 0;
height: 100vh;
}
`;
document.head.appendChild(styleEl);

View File

@ -4,9 +4,11 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../src/layouts/hass-loading-screen";
import "./addon-view/hassio-addon-view";
import "./ingress-view/hassio-ingress-view";
import "./hassio-data";
import "./hassio-pages-with-tabs";
import "../../src/resources/ha-style";
import applyThemesOnElement from "../../src/common/dom/apply_themes_on_element";
import EventsMixin from "../../src/mixins/events-mixin";
import NavigateMixin from "../../src/mixins/navigate-mixin";
@ -33,7 +35,7 @@ class HassioMain extends EventsMixin(NavigateMixin(PolymerElement)) {
</template>
<template is="dom-if" if="[[loaded]]">
<template is="dom-if" if="[[!equalsAddon(routeData.page)]]">
<template is="dom-if" if="[[equalsDashboard(routeData.page)]]">
<hassio-pages-with-tabs
hass="[[hass]]"
page="[[routeData.page]]"
@ -48,6 +50,12 @@ class HassioMain extends EventsMixin(NavigateMixin(PolymerElement)) {
route="[[route]]"
></hassio-addon-view>
</template>
<template is="dom-if" if="[[equalsIngress(routeData.page)]]">
<hassio-ingress-view
hass="[[hass]]"
route="[[route]]"
></hassio-ingress-view>
</template>
</template>
`;
}
@ -137,6 +145,14 @@ class HassioMain extends EventsMixin(NavigateMixin(PolymerElement)) {
equalsAddon(page) {
return page && page === "addon";
}
equalsDashboard(page) {
return !page || !["addon", "ingress"].includes(page);
}
equalsIngress(page) {
return page && page === "ingress";
}
}
customElements.define("hassio-main", HassioMain);

View File

@ -0,0 +1,113 @@
import {
LitElement,
customElement,
property,
TemplateResult,
html,
PropertyValues,
CSSResult,
css,
} from "lit-element";
import { HomeAssistant, Route } from "../../../src/types";
import {
createHassioSession,
HassioAddon,
fetchHassioAddonInfo,
} from "../../../src/data/hassio";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage";
const extractAddon = (path: string) => {
path = path.substr("/ingress".length);
const subpathStart = path.indexOf("/", 1);
return subpathStart === -1
? path.substr(1)
: path.substr(1, subpathStart - 1);
};
@customElement("hassio-ingress-view")
class HassioIngressView extends LitElement {
@property() public hass!: HomeAssistant;
@property() public route!: Route & { slug: string };
@property() private _hasSession = false;
@property() private _addon?: HassioAddon;
protected render(): TemplateResult | void {
if (!this._hasSession || !this._addon) {
return html`
<hass-loading-screen></hass-loading-screen>
`;
}
return html`
<hass-subpage .header=${this._addon.name} hassio root>
<a .href=${this._addon.ingress_url} slot="toolbar-icon" target="_blank">
<paper-icon-button icon="hassio:open-in-new"></paper-icon-button>
</a>
<iframe src=${this._addon.ingress_url}></iframe>
</hass-subpage>
`;
}
protected updated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
if (!changedProps.has("route")) {
return;
}
const addon = extractAddon(this.route.path);
const oldRoute = changedProps.get("route") as this["route"] | undefined;
const oldAddon = oldRoute ? extractAddon(oldRoute.path) : undefined;
if (addon !== oldAddon) {
this._createSession();
this._fetchAddonInfo(addon);
}
}
private async _fetchAddonInfo(addonSlug: string) {
try {
const addon = await fetchHassioAddonInfo(this.hass, addonSlug);
if (addon.ingress) {
this._addon = addon;
} else {
alert("This add-on does not support ingress.");
history.back();
}
} catch (err) {
alert("Failed to fetch add-on info");
history.back();
}
}
private async _createSession() {
try {
await createHassioSession(this.hass);
this._hasSession = true;
} catch (err) {
alert("Failed to generate a session");
history.back();
}
}
static get styles(): CSSResult {
return css`
iframe {
display: block;
width: 100%;
height: 100%;
border: 0;
}
paper-icon-button {
color: var(--text-primary-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-ingress-view": HassioIngressView;
}
}

View File

@ -9,10 +9,16 @@ const paperIconButtonClass = customElements.get(
) as Constructor<PaperIconButtonElement>;
export class HaPaperIconButtonArrowPrev extends paperIconButtonClass {
public hassio?: boolean;
public connectedCallback() {
this.icon =
window.getComputedStyle(this).direction === "ltr"
? "hass:arrow-left"
? this.hassio
? "hassio:arrow-left"
: "hass:arrow-left"
: this.hassio
? "hassio:arrow-right"
: "hass:arrow-right";
// calling super after setting icon to have it consistently show the icon (otherwise not always shown)

86
src/data/hassio.ts Normal file
View File

@ -0,0 +1,86 @@
import { HomeAssistant } from "../types";
interface HassioResponse<T> {
data: T;
result: "ok";
}
interface CreateSessionResponse {
session: string;
}
export interface HassioAddon {
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;
}
const hassioApiResultExtractor = <T>(response: HassioResponse<T>) =>
response.data;
export const createHassioSession = async (hass: HomeAssistant) => {
const response = await hass.callApi<HassioResponse<CreateSessionResponse>>(
"POST",
"hassio/ingress/session"
);
document.cookie = `ingress_session=${
response.data.session
};path=/api/hassio_ingress/`;
};
export const fetchHassioAddonInfo = async (
hass: HomeAssistant,
addon: string
) =>
hass
.callApi<HassioResponse<HassioAddon>>("GET", `hassio/addons/${addon}/info`)
.then(hassioApiResultExtractor);

View File

@ -7,6 +7,7 @@ import {
css,
CSSResult,
} from "lit-element";
import "../components/ha-menu-button";
import "../components/ha-paper-icon-button-arrow-prev";
@customElement("hass-subpage")
@ -14,12 +15,26 @@ class HassSubpage extends LitElement {
@property()
public header?: string;
@property({ type: Boolean })
public root = false;
@property({ type: Boolean })
public hassio = false;
protected render(): TemplateResult | void {
return html`
<div class="toolbar">
<ha-paper-icon-button-arrow-prev
@click=${this._backTapped}
></ha-paper-icon-button-arrow-prev>
${this.root
? html`
<ha-menu-button .hassio=${this.hassio}></ha-menu-button>
`
: html`
<ha-paper-icon-button-arrow-prev
.hassio=${this.hassio}
@click=${this._backTapped}
></ha-paper-icon-button-arrow-prev>
`}
<div main-title>${this.header}</div>
<slot name="toolbar-icon"></slot>
</div>
@ -51,6 +66,7 @@ class HassSubpage extends LitElement {
color: var(--text-primary-color, white);
}
ha-menu-button,
ha-paper-icon-button-arrow-prev,
::slotted([slot="toolbar-icon"]) {
pointer-events: auto;