diff --git a/hassio/src/addon-view/hassio-addon-dashboard.ts b/hassio/src/addon-view/hassio-addon-dashboard.ts index 4260ea4f92..528b46f50a 100644 --- a/hassio/src/addon-view/hassio-addon-dashboard.ts +++ b/hassio/src/addon-view/hassio-addon-dashboard.ts @@ -9,16 +9,21 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, TemplateResult, } from "lit-element"; import memoizeOne from "memoize-one"; +import { navigate } from "../../../src/common/navigate"; +import { extractSearchParam } from "../../../src/common/url/search-params"; import "../../../src/components/ha-circular-progress"; import { fetchHassioAddonInfo, HassioAddonDetails, } from "../../../src/data/hassio/addon"; +import "../../../src/layouts/hass-loading-screen"; +import "../../../src/layouts/hass-error-screen"; import { Supervisor } from "../../../src/data/supervisor/supervisor"; import "../../../src/layouts/hass-tabs-subpage"; import type { PageNavigation } from "../../../src/layouts/hass-tabs-subpage"; @@ -31,6 +36,7 @@ import "./config/hassio-addon-network"; import "./hassio-addon-router"; import "./info/hassio-addon-info"; import "./log/hassio-addon-logs"; +import { extractApiErrorMessage } from "../../../src/data/hassio/common"; @customElement("hassio-addon-dashboard") class HassioAddonDashboard extends LitElement { @@ -44,6 +50,8 @@ class HassioAddonDashboard extends LitElement { @property({ type: Boolean }) public narrow!: boolean; + @internalProperty() _error?: string; + private _computeTail = memoizeOne((route: Route) => { const dividerPos = route.path.indexOf("/", 1); return dividerPos === -1 @@ -58,8 +66,14 @@ class HassioAddonDashboard extends LitElement { }); protected render(): TemplateResult { + if (this._error) { + return html``; + } + if (!this.addon) { - return html``; + return html``; } const addonTabs: PageNavigation[] = [ @@ -156,7 +170,12 @@ class HassioAddonDashboard extends LitElement { } protected async firstUpdated(): Promise { - await this._routeDataChanged(this.route); + if (this.route.path === "") { + const addon = extractSearchParam("addon"); + if (addon) { + navigate(this, `/hassio/addon/${addon}`, true); + } + } this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev)); } @@ -170,16 +189,26 @@ class HassioAddonDashboard extends LitElement { if (path === "uninstall") { history.back(); } else { - await this._routeDataChanged(this.route); + await this._routeDataChanged(); } } - private async _routeDataChanged(routeData: Route): Promise { - const addon = routeData.path.split("/")[1]; + protected updated(changedProperties) { + if (changedProperties.has("route") && !this.addon) { + this._routeDataChanged(); + } + } + + private async _routeDataChanged(): Promise { + const addon = this.route.path.split("/")[1]; + if (!addon) { + return; + } try { const addoninfo = await fetchHassioAddonInfo(this.hass, addon); this.addon = addoninfo; - } catch { + } catch (err) { + this._error = `Error fetching addon info: ${extractApiErrorMessage(err)}`; this.addon = undefined; } } diff --git a/hassio/src/hassio-my-redirect.ts b/hassio/src/hassio-my-redirect.ts new file mode 100644 index 0000000000..9391da6914 --- /dev/null +++ b/hassio/src/hassio-my-redirect.ts @@ -0,0 +1,125 @@ +import { + customElement, + html, + internalProperty, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import { sanitizeUrl } from "@braintree/sanitize-url"; +import { + createSearchParam, + extractSearchParamsObject, +} from "../../src/common/url/search-params"; +import "../../src/layouts/hass-error-screen"; +import { + ParamType, + Redirect, + Redirects, +} from "../../src/panels/my/ha-panel-my"; +import { navigate } from "../../src/common/navigate"; +import { HomeAssistant, Route } from "../../src/types"; + +const REDIRECTS: Redirects = { + supervisor_system: { + redirect: "/hassio/system", + }, + supervisor_snapshots: { + redirect: "/hassio/snapshots", + }, + supervisor_store: { + redirect: "/hassio/store", + }, + supervisor: { + redirect: "/hassio/dashboard", + }, + supervisor_addon: { + redirect: "/hassio/addon", + params: { + addon: "string", + }, + }, +}; + +@customElement("hassio-my-redirect") +class HassioMyRedirect extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public route!: Route; + + @internalProperty() public _error?: TemplateResult | string; + + connectedCallback() { + super.connectedCallback(); + const path = this.route.path.substr(1); + const redirect = REDIRECTS[path]; + + if (!redirect) { + this._error = html`This redirect is not supported by your Home Assistant + instance. Check the + My Home Assistant FAQ + for the supported redirects and the version they where introduced.`; + return; + } + + let url: string; + try { + url = this._createRedirectUrl(redirect); + } catch (err) { + this._error = "An unknown error occured"; + return; + } + + navigate(this, url, true); + } + + protected render(): TemplateResult { + if (this._error) { + return html``; + } + return html``; + } + + private _createRedirectUrl(redirect: Redirect): string { + const params = this._createRedirectParams(redirect); + return `${redirect.redirect}${params}`; + } + + private _createRedirectParams(redirect: Redirect): string { + const params = extractSearchParamsObject(); + if (!redirect.params && !Object.keys(params).length) { + return ""; + } + const resultParams = {}; + Object.entries(redirect.params || {}).forEach(([key, type]) => { + if (!params[key] || !this._checkParamType(type, params[key])) { + throw Error(); + } + resultParams[key] = params[key]; + }); + return `?${createSearchParam(resultParams)}`; + } + + private _checkParamType(type: ParamType, value: string) { + if (type === "string") { + return true; + } + if (type === "url") { + return value && value === sanitizeUrl(value); + } + return false; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hassio-my-redirect": HassioMyRedirect; + } +} diff --git a/hassio/src/hassio-router.ts b/hassio/src/hassio-router.ts index 50bd69cd3a..dbcbfd08ed 100644 --- a/hassio/src/hassio-router.ts +++ b/hassio/src/hassio-router.ts @@ -41,6 +41,10 @@ class HassioRouter extends HassRouterPage { tag: "hassio-ingress-view", load: () => import("./ingress-view/hassio-ingress-view"), }, + _my_redirect: { + tag: "hassio-my-redirect", + load: () => import("./hassio-my-redirect"), + }, }, }; diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts index 4db3b69260..9789157691 100644 --- a/src/dialogs/quick-bar/ha-quick-bar.ts +++ b/src/dialogs/quick-bar/ha-quick-bar.ts @@ -380,22 +380,24 @@ export class QuickBar extends LitElement { QuickBarNavigationItem, "action" >[] { - return Object.keys(this.hass.panels).map((panelKey) => { - const panel = this.hass.panels[panelKey]; - const translationKey = getPanelNameTranslationKey(panel); + return Object.keys(this.hass.panels) + .filter((panelKey) => panelKey !== "_my_redirect") + .map((panelKey) => { + const panel = this.hass.panels[panelKey]; + const translationKey = getPanelNameTranslationKey(panel); - const text = this.hass.localize( - "ui.dialogs.quick-bar.commands.navigation.navigate_to", - "panel", - this.hass.localize(translationKey) || panel.title || panel.url_path - ); + const text = this.hass.localize( + "ui.dialogs.quick-bar.commands.navigation.navigate_to", + "panel", + this.hass.localize(translationKey) || panel.title || panel.url_path + ); - return { - text, - icon: getPanelIcon(panel) || DEFAULT_NAVIGATION_ICON, - path: `/${panel.url_path}`, - }; - }); + return { + text, + icon: getPanelIcon(panel) || DEFAULT_NAVIGATION_ICON, + path: `/${panel.url_path}`, + }; + }); } private _generateNavigationConfigSectionCommands(): Partial< diff --git a/src/panels/my/ha-panel-my.ts b/src/panels/my/ha-panel-my.ts index b9eaea9916..97f12ef6aa 100644 --- a/src/panels/my/ha-panel-my.ts +++ b/src/panels/my/ha-panel-my.ts @@ -13,8 +13,10 @@ import { extractSearchParamsObject, } from "../../common/url/search-params"; import "../../layouts/hass-error-screen"; +import { isComponentLoaded } from "../../common/config/is_component_loaded"; +import { domainToName } from "../../data/integration"; -const REDIRECTS = { +const REDIRECTS: Redirects = { info: { redirect: "/config/info", }, @@ -38,10 +40,12 @@ const REDIRECTS = { }, }; -type ParamType = "url" | "string"; +export type ParamType = "url" | "string"; -interface Redirect { +export type Redirects = { [key: string]: Redirect }; +export interface Redirect { redirect: string; + component?: string; params?: { [key: string]: ParamType; }; @@ -58,7 +62,25 @@ class HaPanelMy extends LitElement { connectedCallback() { super.connectedCallback(); const path = this.route.path.substr(1); - const redirect: Redirect | undefined = REDIRECTS[path]; + + if (path.startsWith("supervisor")) { + if (!isComponentLoaded(this.hass, "hassio")) { + this._error = this.hass.localize( + "ui.panel.my.component_not_loaded", + "integration", + domainToName(this.hass.localize, "hassio") + ); + return; + } + navigate( + this, + `/hassio/_my_redirect/${path}${window.location.search}`, + true + ); + return; + } + + const redirect = REDIRECTS[path]; if (!redirect) { this._error = this.hass.localize( @@ -74,6 +96,18 @@ class HaPanelMy extends LitElement { return; } + if ( + redirect.component && + !isComponentLoaded(this.hass, redirect.component) + ) { + this._error = this.hass.localize( + "ui.panel.my.component_not_loaded", + "integration", + domainToName(this.hass.localize, redirect.component) + ); + return; + } + let url: string; try { url = this._createRedirectUrl(redirect); diff --git a/src/translations/en.json b/src/translations/en.json index 9a7cff23c4..ef30609c90 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -806,6 +806,7 @@ "panel": { "my": { "not_supported": "This redirect is not supported by your Home Assistant instance. Check the {link} for the supported redirects and the version they where introduced.", + "component_not_loaded": "This redirect is not supported by your Home Assistant instance. You need the integration {integration} to use this redirect.", "faq_link": "My Home Assistant FAQ", "error": "An unknown error occured" },