mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +00:00
Add hassio ingress support (#3062)
* Add hassio ingress support * Remove logging * Better integrate * Add badge * FIx type
This commit is contained in:
parent
7f99f1d9be
commit
b07f95f956
@ -10,6 +10,7 @@ import "../../../src/components/ha-markdown";
|
|||||||
import "../../../src/components/buttons/ha-call-api-button";
|
import "../../../src/components/buttons/ha-call-api-button";
|
||||||
import "../../../src/resources/ha-style";
|
import "../../../src/resources/ha-style";
|
||||||
import EventsMixin from "../../../src/mixins/events-mixin";
|
import EventsMixin from "../../../src/mixins/events-mixin";
|
||||||
|
import { navigate } from "../../../src/common/navigate";
|
||||||
|
|
||||||
import "../components/hassio-card-content";
|
import "../components/hassio-card-content";
|
||||||
|
|
||||||
@ -59,6 +60,11 @@ const PERMIS_DESC = {
|
|||||||
description:
|
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.",
|
"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) {
|
class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||||
@ -310,6 +316,15 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
|||||||
description=""
|
description=""
|
||||||
></ha-label-badge>
|
></ha-label-badge>
|
||||||
</template>
|
</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>
|
</div>
|
||||||
<template is="dom-if" if="[[addon.version]]">
|
<template is="dom-if" if="[[addon.version]]">
|
||||||
<div class="state">
|
<div class="state">
|
||||||
@ -371,7 +386,7 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
|||||||
</template>
|
</template>
|
||||||
<template
|
<template
|
||||||
is="dom-if"
|
is="dom-if"
|
||||||
if="[[computeShowWebUI(addon.webui, isRunning)]]"
|
if="[[computeShowWebUI(addon.ingress, addon.webui, isRunning)]]"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="[[pathWebui(addon.webui)]]"
|
href="[[pathWebui(addon.webui)]]"
|
||||||
@ -381,6 +396,16 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
|||||||
><mwc-button>Open web UI</mwc-button></a
|
><mwc-button>Open web UI</mwc-button></a
|
||||||
>
|
>
|
||||||
</template>
|
</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>
|
||||||
<template is="dom-if" if="[[!addon.version]]">
|
<template is="dom-if" if="[[!addon.version]]">
|
||||||
<template is="dom-if" if="[[!addon.available]]">
|
<template is="dom-if" if="[[!addon.available]]">
|
||||||
@ -448,8 +473,16 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
|||||||
return webui && webui.replace("[HOST]", document.location.hostname);
|
return webui && webui.replace("[HOST]", document.location.hostname);
|
||||||
}
|
}
|
||||||
|
|
||||||
computeShowWebUI(webui, isRunning) {
|
computeShowWebUI(ingress, webui, isRunning) {
|
||||||
return webui && isRunning;
|
return !ingress && webui && isRunning;
|
||||||
|
}
|
||||||
|
|
||||||
|
openIngress() {
|
||||||
|
navigate(this, `/hassio/ingress/${this.addon.slug}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
computeShowIngressUI(ingress, isRunning) {
|
||||||
|
return ingress && isRunning;
|
||||||
}
|
}
|
||||||
|
|
||||||
computeStartOnBoot(state) {
|
computeStartOnBoot(state) {
|
||||||
|
@ -2,4 +2,16 @@ window.loadES5Adapter().then(() => {
|
|||||||
import(/* webpackChunkName: "hassio-icons" */ "./resources/hassio-icons.js");
|
import(/* webpackChunkName: "hassio-icons" */ "./resources/hassio-icons.js");
|
||||||
import(/* webpackChunkName: "hassio-main" */ "./hassio-main.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);
|
||||||
|
@ -4,9 +4,11 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|||||||
|
|
||||||
import "../../src/layouts/hass-loading-screen";
|
import "../../src/layouts/hass-loading-screen";
|
||||||
import "./addon-view/hassio-addon-view";
|
import "./addon-view/hassio-addon-view";
|
||||||
|
import "./ingress-view/hassio-ingress-view";
|
||||||
import "./hassio-data";
|
import "./hassio-data";
|
||||||
import "./hassio-pages-with-tabs";
|
import "./hassio-pages-with-tabs";
|
||||||
|
|
||||||
|
import "../../src/resources/ha-style";
|
||||||
import applyThemesOnElement from "../../src/common/dom/apply_themes_on_element";
|
import applyThemesOnElement from "../../src/common/dom/apply_themes_on_element";
|
||||||
import EventsMixin from "../../src/mixins/events-mixin";
|
import EventsMixin from "../../src/mixins/events-mixin";
|
||||||
import NavigateMixin from "../../src/mixins/navigate-mixin";
|
import NavigateMixin from "../../src/mixins/navigate-mixin";
|
||||||
@ -33,7 +35,7 @@ class HassioMain extends EventsMixin(NavigateMixin(PolymerElement)) {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template is="dom-if" if="[[loaded]]">
|
<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
|
<hassio-pages-with-tabs
|
||||||
hass="[[hass]]"
|
hass="[[hass]]"
|
||||||
page="[[routeData.page]]"
|
page="[[routeData.page]]"
|
||||||
@ -48,6 +50,12 @@ class HassioMain extends EventsMixin(NavigateMixin(PolymerElement)) {
|
|||||||
route="[[route]]"
|
route="[[route]]"
|
||||||
></hassio-addon-view>
|
></hassio-addon-view>
|
||||||
</template>
|
</template>
|
||||||
|
<template is="dom-if" if="[[equalsIngress(routeData.page)]]">
|
||||||
|
<hassio-ingress-view
|
||||||
|
hass="[[hass]]"
|
||||||
|
route="[[route]]"
|
||||||
|
></hassio-ingress-view>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -137,6 +145,14 @@ class HassioMain extends EventsMixin(NavigateMixin(PolymerElement)) {
|
|||||||
equalsAddon(page) {
|
equalsAddon(page) {
|
||||||
return page && page === "addon";
|
return page && page === "addon";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
equalsDashboard(page) {
|
||||||
|
return !page || !["addon", "ingress"].includes(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
equalsIngress(page) {
|
||||||
|
return page && page === "ingress";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("hassio-main", HassioMain);
|
customElements.define("hassio-main", HassioMain);
|
||||||
|
113
hassio/src/ingress-view/hassio-ingress-view.ts
Normal file
113
hassio/src/ingress-view/hassio-ingress-view.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -9,10 +9,16 @@ const paperIconButtonClass = customElements.get(
|
|||||||
) as Constructor<PaperIconButtonElement>;
|
) as Constructor<PaperIconButtonElement>;
|
||||||
|
|
||||||
export class HaPaperIconButtonArrowPrev extends paperIconButtonClass {
|
export class HaPaperIconButtonArrowPrev extends paperIconButtonClass {
|
||||||
|
public hassio?: boolean;
|
||||||
|
|
||||||
public connectedCallback() {
|
public connectedCallback() {
|
||||||
this.icon =
|
this.icon =
|
||||||
window.getComputedStyle(this).direction === "ltr"
|
window.getComputedStyle(this).direction === "ltr"
|
||||||
? "hass:arrow-left"
|
? this.hassio
|
||||||
|
? "hassio:arrow-left"
|
||||||
|
: "hass:arrow-left"
|
||||||
|
: this.hassio
|
||||||
|
? "hassio:arrow-right"
|
||||||
: "hass:arrow-right";
|
: "hass:arrow-right";
|
||||||
|
|
||||||
// calling super after setting icon to have it consistently show the icon (otherwise not always shown)
|
// calling super after setting icon to have it consistently show the icon (otherwise not always shown)
|
||||||
|
86
src/data/hassio.ts
Normal file
86
src/data/hassio.ts
Normal 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);
|
@ -7,6 +7,7 @@ import {
|
|||||||
css,
|
css,
|
||||||
CSSResult,
|
CSSResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
|
import "../components/ha-menu-button";
|
||||||
import "../components/ha-paper-icon-button-arrow-prev";
|
import "../components/ha-paper-icon-button-arrow-prev";
|
||||||
|
|
||||||
@customElement("hass-subpage")
|
@customElement("hass-subpage")
|
||||||
@ -14,12 +15,26 @@ class HassSubpage extends LitElement {
|
|||||||
@property()
|
@property()
|
||||||
public header?: string;
|
public header?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public root = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public hassio = false;
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
return html`
|
return html`
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<ha-paper-icon-button-arrow-prev
|
${this.root
|
||||||
@click=${this._backTapped}
|
? html`
|
||||||
></ha-paper-icon-button-arrow-prev>
|
<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>
|
<div main-title>${this.header}</div>
|
||||||
<slot name="toolbar-icon"></slot>
|
<slot name="toolbar-icon"></slot>
|
||||||
</div>
|
</div>
|
||||||
@ -51,6 +66,7 @@ class HassSubpage extends LitElement {
|
|||||||
color: var(--text-primary-color, white);
|
color: var(--text-primary-color, white);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ha-menu-button,
|
||||||
ha-paper-icon-button-arrow-prev,
|
ha-paper-icon-button-arrow-prev,
|
||||||
::slotted([slot="toolbar-icon"]) {
|
::slotted([slot="toolbar-icon"]) {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user