mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +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/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) {
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
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>;
|
||||
|
||||
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
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,
|
||||
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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user