mirror of
https://github.com/home-assistant/frontend.git
synced 2026-09-07 11:58:45 +00:00
Compare commits
3
Commits
map-engine
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6cbe49d81 | ||
|
|
1e9592a0b8 | ||
|
|
b2b0491c99 |
@@ -0,0 +1,5 @@
|
||||
export const isExternalHassUrl = (): boolean =>
|
||||
Boolean(__HASS_URL__) && __HASS_URL__ !== location.origin;
|
||||
|
||||
export const resolveHassUrl = (path: string): string =>
|
||||
isExternalHassUrl() ? new URL(path, __HASS_URL__).toString() : path;
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { fetchHassioAddonsInfo } from "../data/hassio/addon";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import "./ha-alert";
|
||||
import "./ha-app-icon";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-generic-picker";
|
||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
||||
@@ -18,13 +19,22 @@ const SEARCH_KEYS = [
|
||||
{ name: "search_labels.repository", weight: 5 },
|
||||
];
|
||||
|
||||
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
||||
interface AddonPickerItem extends PickerComboBoxItem {
|
||||
slug: string;
|
||||
hasIcon: boolean;
|
||||
}
|
||||
|
||||
const rowRenderer: RenderItemFunction<AddonPickerItem> = (item) => html`
|
||||
<ha-combo-box-item type="button">
|
||||
<span slot="headline">${item.primary}</span>
|
||||
<span slot="supporting-text">${item.secondary}</span>
|
||||
${
|
||||
item.icon
|
||||
? html` <img alt="" slot="start" .src=${item.icon} /> `
|
||||
item.hasIcon
|
||||
? html`<ha-app-icon
|
||||
slot="start"
|
||||
.slug=${item.slug}
|
||||
.hasIcon=${item.hasIcon}
|
||||
></ha-app-icon>`
|
||||
: nothing
|
||||
}
|
||||
</ha-combo-box-item>
|
||||
@@ -40,7 +50,7 @@ class HaAddonPicker extends LitElement {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@state() private _addons?: PickerComboBoxItem[];
|
||||
@state() private _addons?: AddonPickerItem[];
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@@ -102,11 +112,10 @@ class HaAddonPicker extends LitElement {
|
||||
.filter((addon) => addon.version)
|
||||
.map((addon) => ({
|
||||
id: addon.slug,
|
||||
slug: addon.slug,
|
||||
hasIcon: addon.icon,
|
||||
primary: addon.name,
|
||||
secondary: addon.slug,
|
||||
icon: addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined,
|
||||
search_labels: {
|
||||
description: addon.description || null,
|
||||
repository: addon.repository || null,
|
||||
@@ -151,15 +160,22 @@ class HaAddonPicker extends LitElement {
|
||||
private _valueRenderer = (itemId: string) => {
|
||||
const item = this._addons!.find((addon) => addon.id === itemId);
|
||||
return html`${
|
||||
item?.icon
|
||||
? html`<img
|
||||
item?.hasIcon
|
||||
? html`<ha-app-icon
|
||||
slot="start"
|
||||
alt=${item.primary ?? "Unknown"}
|
||||
.src=${item.icon}
|
||||
/>`
|
||||
.slug=${item.slug}
|
||||
.hasIcon=${item.hasIcon}
|
||||
.alt=${item.primary ?? "Unknown"}
|
||||
></ha-app-icon>`
|
||||
: nothing
|
||||
}<span slot="headline">${item?.primary || "Unknown"}</span>`;
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
ha-app-icon {
|
||||
--ha-app-icon-size: var(--ha-space-8);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import type { HASSDomCurrentTargetEvent } from "../common/dom/fire_event";
|
||||
import { isExternalHassUrl } from "../common/url/hass-url";
|
||||
import { supervisorUrl } from "../data/hassio/common";
|
||||
|
||||
@customElement("ha-app-icon")
|
||||
export class HaAppIcon extends LitElement {
|
||||
@property() public slug = "";
|
||||
|
||||
@property({ attribute: "has-icon", type: Boolean })
|
||||
public hasIcon?: boolean;
|
||||
|
||||
@property() public alt = "";
|
||||
|
||||
@property() public loading: "eager" | "lazy" = "eager";
|
||||
|
||||
@state() private _failedSrc?: string;
|
||||
|
||||
@query("img") private _image?: HTMLImageElement;
|
||||
|
||||
protected render() {
|
||||
const src = supervisorUrl(`addons/${this.slug}/icon`);
|
||||
|
||||
if (!this.slug || this.hasIcon === false || this._failedSrc === src) {
|
||||
return html`<slot></slot>`;
|
||||
}
|
||||
|
||||
return keyed(
|
||||
src,
|
||||
html`
|
||||
<img
|
||||
src=${src}
|
||||
alt=${this.alt}
|
||||
loading=${this.loading}
|
||||
crossorigin=${ifDefined(
|
||||
isExternalHassUrl() ? undefined : "anonymous"
|
||||
)}
|
||||
referrerpolicy="no-referrer"
|
||||
@error=${this._handleError}
|
||||
/>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
private _handleError(ev: HASSDomCurrentTargetEvent<HTMLImageElement>) {
|
||||
if (ev.currentTarget !== this._image) {
|
||||
return;
|
||||
}
|
||||
this._failedSrc = ev.currentTarget.getAttribute("src") ?? undefined;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--ha-app-icon-size, var(--mdc-icon-size));
|
||||
height: var(--ha-app-icon-size, var(--mdc-icon-size));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-app-icon": HaAppIcon;
|
||||
}
|
||||
}
|
||||
@@ -51,10 +51,19 @@ export class HaComboBoxItem extends HaMdListItem {
|
||||
white-space: normal;
|
||||
}
|
||||
::slotted(state-badge),
|
||||
::slotted(img) {
|
||||
::slotted(img),
|
||||
::slotted(ha-app-icon) {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
::slotted(ha-app-icon.colored) {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: var(--ha-space-1);
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
background-color: var(--app-icon-background-color);
|
||||
color: var(--white-color);
|
||||
}
|
||||
::slotted(.code) {
|
||||
font-family: var(--ha-font-family-code);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { atLeastVersion } from "../common/config/version";
|
||||
import type { HomeAssistant, LogFileDisabledReason } from "../types";
|
||||
import type { HassioAddonInfo } from "./hassio/addon";
|
||||
import { supervisorUrl } from "./hassio/common";
|
||||
|
||||
export interface LogProvider {
|
||||
key: string;
|
||||
@@ -18,7 +19,7 @@ export const fetchErrorLog = (hass: HomeAssistant) =>
|
||||
|
||||
export const getErrorLogDownloadUrl = (hass: HomeAssistant) =>
|
||||
hasSupervisorCoreLogDownload(hass)
|
||||
? "/api/hassio/core/logs/latest"
|
||||
? supervisorUrl("core/logs/latest")
|
||||
: "/api/error_log";
|
||||
|
||||
export const getCoreLogFileDownloadUnavailableReason = (
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { resolveHassUrl } from "../../common/url/hass-url";
|
||||
import type { CallWS } from "../../types";
|
||||
|
||||
export const supervisorUrl = (path: string): string =>
|
||||
resolveHassUrl(`/api/hassio/${path}`);
|
||||
|
||||
export interface HassioResponse<T> {
|
||||
data: T;
|
||||
message?: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { HomeAssistant, PanelInfo } from "../../types";
|
||||
import type { SupervisorArch } from "../supervisor/supervisor";
|
||||
import type { HassioResponse } from "./common";
|
||||
import { supervisorUrl, type HassioResponse } from "./common";
|
||||
|
||||
export interface HassioHomeAssistantInfo {
|
||||
arch: SupervisorArch;
|
||||
@@ -198,18 +198,20 @@ export const fetchHassioLogsFollowSkip = async (
|
||||
);
|
||||
|
||||
export const getHassioLogDownloadUrl = (provider: string) =>
|
||||
`/api/hassio/${
|
||||
provider.includes("_") ? `addons/${provider}` : provider
|
||||
}/logs`;
|
||||
supervisorUrl(
|
||||
`${provider.includes("_") ? `addons/${provider}` : provider}/logs`
|
||||
);
|
||||
|
||||
export const getHassioLogDownloadLinesUrl = (
|
||||
provider: string,
|
||||
lines: number,
|
||||
boot = 0
|
||||
) =>
|
||||
`/api/hassio/${
|
||||
provider.includes("_") ? `addons/${provider}` : provider
|
||||
}/logs${boot !== 0 ? `/boots/${boot}` : ""}?lines=${lines}`;
|
||||
supervisorUrl(
|
||||
`${
|
||||
provider.includes("_") ? `addons/${provider}` : provider
|
||||
}/logs${boot !== 0 ? `/boots/${boot}` : ""}?lines=${lines}`
|
||||
);
|
||||
|
||||
export const setSupervisorOption = async (
|
||||
hass: HomeAssistant,
|
||||
@@ -223,4 +225,4 @@ export const setSupervisorOption = async (
|
||||
});
|
||||
};
|
||||
|
||||
export const coreLatestLogsUrl = "/api/hassio/core/logs/latest";
|
||||
export const coreLatestLogsUrl = supervisorUrl("core/logs/latest");
|
||||
|
||||
+7
-10
@@ -28,6 +28,7 @@ export interface NavigationComboBoxItem extends PickerComboBoxItem {
|
||||
path: string;
|
||||
image?: string;
|
||||
iconColor?: string;
|
||||
app?: Pick<HassioAddonInfo, "slug" | "icon">;
|
||||
}
|
||||
|
||||
export interface BaseNavigationCommand {
|
||||
@@ -38,6 +39,7 @@ export interface BaseNavigationCommand {
|
||||
iconPath?: string;
|
||||
iconColor?: string;
|
||||
image?: string;
|
||||
app?: Pick<HassioAddonInfo, "slug" | "icon">;
|
||||
}
|
||||
|
||||
export interface ActionCommandComboBoxItem extends PickerComboBoxItem {
|
||||
@@ -62,19 +64,14 @@ const generateNavigationPanelCommands = (
|
||||
|
||||
const primary = localize(translationKey) || panel.title || panel.url_path;
|
||||
|
||||
let image: string | undefined;
|
||||
|
||||
if (apps) {
|
||||
const app = apps.find(({ slug }) => slug === panel.url_path);
|
||||
if (app) {
|
||||
image = app.icon ? `/api/hassio/addons/${app.slug}/icon` : undefined;
|
||||
}
|
||||
}
|
||||
const panelApp = apps?.find(({ slug }) => slug === panel.url_path);
|
||||
|
||||
return {
|
||||
primary,
|
||||
icon,
|
||||
image,
|
||||
app: panelApp
|
||||
? { slug: panelApp.slug, icon: panelApp.icon }
|
||||
: undefined,
|
||||
path: `/${panel.url_path}`,
|
||||
};
|
||||
});
|
||||
@@ -171,7 +168,7 @@ export const generateNavigationCommands = (
|
||||
for (const app of apps.filter((a) => a.version)) {
|
||||
appItems.push({
|
||||
path: `/config/app/${app.slug}`,
|
||||
image: app.icon ? `/api/hassio/addons/${app.slug}/icon` : undefined,
|
||||
app: { slug: app.slug, icon: app.icon },
|
||||
primary: hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.navigation.app_info",
|
||||
{ app: app.name }
|
||||
|
||||
@@ -12,6 +12,7 @@ import { navigate } from "../../common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import "../../components/entity/state-badge";
|
||||
import "../../components/ha-adaptive-dialog";
|
||||
import "../../components/ha-app-icon";
|
||||
import "../../components/ha-combo-box-item";
|
||||
import "../../components/ha-domain-icon";
|
||||
import "../../components/ha-icon";
|
||||
@@ -310,6 +311,7 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
|
||||
const iconPath = item.icon_path || mdiDevices;
|
||||
const iconColor = "iconColor" in item ? item.iconColor : undefined;
|
||||
|
||||
return html`
|
||||
<ha-combo-box-item
|
||||
@@ -334,44 +336,65 @@ export class QuickBar extends LitElement {
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
`
|
||||
: "image" in item && item.image
|
||||
: "app" in item && item.app
|
||||
? html`
|
||||
<img
|
||||
<ha-app-icon
|
||||
slot="start"
|
||||
alt=${item.primary ?? "Unknown"}
|
||||
.src=${item.image}
|
||||
.slug=${item.app.slug}
|
||||
.hasIcon=${item.app.icon}
|
||||
.alt=${item.primary ?? "Unknown"}
|
||||
class=${iconColor ? "colored" : nothing}
|
||||
style=${
|
||||
"iconColor" in item && item.iconColor
|
||||
? `background-color: ${item.iconColor}; padding: 4px; border-radius: var(--ha-border-radius-circle); width: 24px; height: 24px`
|
||||
: ""
|
||||
iconColor
|
||||
? `--app-icon-background-color: ${iconColor}`
|
||||
: nothing
|
||||
}
|
||||
/>
|
||||
>
|
||||
${
|
||||
item.icon
|
||||
? html`<ha-icon .icon=${item.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon .path=${iconPath}></ha-svg-icon>`
|
||||
}
|
||||
</ha-app-icon>
|
||||
`
|
||||
: item.icon
|
||||
? html`<ha-icon
|
||||
style="margin: var(--ha-space-1);"
|
||||
slot="start"
|
||||
.icon=${item.icon}
|
||||
></ha-icon>`
|
||||
: "iconColor" in item && item.iconColor
|
||||
? html`
|
||||
<div
|
||||
slot="start"
|
||||
style=${`padding: 4px; border-radius: var(--ha-border-radius-circle); background-color: ${item.iconColor};`}
|
||||
>
|
||||
: "image" in item && item.image
|
||||
? html`
|
||||
<img
|
||||
slot="start"
|
||||
alt=${item.primary ?? "Unknown"}
|
||||
.src=${item.image}
|
||||
style=${
|
||||
"iconColor" in item && item.iconColor
|
||||
? `background-color: ${item.iconColor}; padding: 4px; border-radius: var(--ha-border-radius-circle); width: 24px; height: 24px`
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
`
|
||||
: item.icon
|
||||
? html`<ha-icon
|
||||
style="margin: var(--ha-space-1);"
|
||||
slot="start"
|
||||
.icon=${item.icon}
|
||||
></ha-icon>`
|
||||
: "iconColor" in item && item.iconColor
|
||||
? html`
|
||||
<div
|
||||
slot="start"
|
||||
style=${`padding: 4px; border-radius: var(--ha-border-radius-circle); background-color: ${item.iconColor};`}
|
||||
>
|
||||
<ha-svg-icon
|
||||
style="color: var(--white-color); --mdc-icon-size: 24px;"
|
||||
.path=${iconPath}
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
style="color: var(--white-color); --mdc-icon-size: 24px;"
|
||||
style="margin: var(--ha-space-1);"
|
||||
slot="start"
|
||||
.path=${iconPath}
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
style="margin: var(--ha-space-1);"
|
||||
slot="start"
|
||||
.path=${iconPath}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
`
|
||||
}
|
||||
<span slot="headline">${item.primary}</span>
|
||||
${
|
||||
|
||||
@@ -86,6 +86,7 @@ import type { HassioStats } from "../../../../../data/hassio/common";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
fetchHassioStats,
|
||||
supervisorUrl,
|
||||
} from "../../../../../data/hassio/common";
|
||||
import type { StoreAddonDetails } from "../../../../../data/supervisor/store";
|
||||
import {
|
||||
@@ -205,7 +206,9 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
<img
|
||||
class="logo"
|
||||
alt=""
|
||||
src="/api/hassio/addons/${this._currentAddon.slug}/logo"
|
||||
src=${supervisorUrl(
|
||||
`addons/${this._currentAddon.slug}/logo`
|
||||
)}
|
||||
/>
|
||||
`
|
||||
: nothing
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mdiCheckCircle, mdiHelpCircleOutline } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../components/ha-app-icon";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { AddonStage, AddonState } from "../../../../data/hassio/addon";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -41,21 +42,28 @@ class SupervisorAppsCardContent extends LitElement {
|
||||
|
||||
@property() public icon = mdiHelpCircleOutline;
|
||||
|
||||
@property({ attribute: false }) public iconImage?: string;
|
||||
@property({ attribute: false }) public appSlug?: string;
|
||||
|
||||
@property({ attribute: false }) public hasAppIcon?: boolean;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="app">
|
||||
<div class="icon-wrapper">
|
||||
${
|
||||
this.iconImage
|
||||
this.appSlug
|
||||
? html`
|
||||
<img
|
||||
class="icon-image"
|
||||
src=${this.iconImage}
|
||||
.title=${this.iconTitle}
|
||||
alt=${this.iconTitle ?? ""}
|
||||
/>
|
||||
<ha-app-icon
|
||||
.slug=${this.appSlug}
|
||||
.hasIcon=${this.hasAppIcon}
|
||||
.alt=${this.iconTitle ?? ""}
|
||||
.title=${this.iconTitle ?? ""}
|
||||
>
|
||||
<ha-svg-icon
|
||||
class="app-icon"
|
||||
.path=${this.icon}
|
||||
></ha-svg-icon>
|
||||
</ha-app-icon>
|
||||
`
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
@@ -134,15 +142,15 @@ class SupervisorAppsCardContent extends LitElement {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.app-icon {
|
||||
margin-left: var(--ha-space-2);
|
||||
margin-top: var(--ha-space-2);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.icon-image {
|
||||
max-height: 40px;
|
||||
max-width: 40px;
|
||||
ha-app-icon {
|
||||
--ha-app-icon-size: 40px;
|
||||
}
|
||||
.title {
|
||||
flex: 1;
|
||||
|
||||
@@ -151,11 +151,8 @@ export class HaConfigAppsInstalled extends LitElement {
|
||||
"ui.panel.config.apps.installed.app_running"
|
||||
)
|
||||
}
|
||||
.iconImage=${
|
||||
addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined
|
||||
}
|
||||
.appSlug=${addon.slug}
|
||||
.hasAppIcon=${addon.icon}
|
||||
></supervisor-apps-card-content>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
@@ -116,11 +116,8 @@ export class SupervisorAppsRepositoryEl extends LitElement {
|
||||
? "not_available"
|
||||
: ""
|
||||
}
|
||||
.iconImage=${
|
||||
addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined
|
||||
}
|
||||
.appSlug=${addon.slug}
|
||||
.hasAppIcon=${addon.icon}
|
||||
.showTopbar=${addon.installed || !addon.available}
|
||||
.topbarClass=${
|
||||
addon.installed
|
||||
|
||||
@@ -4,8 +4,10 @@ import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
import "../../../../components/ha-app-icon";
|
||||
import "../../../../components/ha-checkbox";
|
||||
import type { HaCheckbox } from "../../../../components/ha-checkbox";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "./ha-backup-formfield-label";
|
||||
|
||||
@@ -50,13 +52,17 @@ export class HaBackupAddonsPicker extends LitElement {
|
||||
<ha-backup-formfield-label
|
||||
.label=${item.name}
|
||||
.version=${this.hideVersion ? undefined : item.version}
|
||||
.iconPath=${item.iconPath || mdiPuzzle}
|
||||
.imageUrl=${
|
||||
this.addons?.find((a) => a.slug === item.slug)?.icon
|
||||
? `/api/hassio/addons/${item.slug}/icon`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<ha-app-icon
|
||||
slot="icon"
|
||||
.slug=${item.slug}
|
||||
.hasIcon=${item.icon}
|
||||
loading="lazy"
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${item.iconPath || mdiPuzzle}
|
||||
></ha-svg-icon>
|
||||
</ha-app-icon>
|
||||
</ha-backup-formfield-label>
|
||||
</ha-checkbox>
|
||||
`
|
||||
@@ -86,6 +92,10 @@ export class HaBackupAddonsPicker extends LitElement {
|
||||
padding-inline-start: var(--ha-space-2);
|
||||
padding-bottom: var(--ha-space-3);
|
||||
}
|
||||
|
||||
ha-app-icon {
|
||||
--ha-app-icon-size: var(--ha-space-6);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,20 +15,25 @@ class SupervisorFormfieldLabel extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${
|
||||
this.imageUrl
|
||||
? html`<img
|
||||
loading="lazy"
|
||||
alt=""
|
||||
src=${this.imageUrl}
|
||||
class="icon"
|
||||
/>`
|
||||
: this.iconPath
|
||||
? html`
|
||||
<ha-svg-icon .path=${this.iconPath} class="icon"></ha-svg-icon>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<slot name="icon">
|
||||
${
|
||||
this.imageUrl
|
||||
? html`<img
|
||||
loading="lazy"
|
||||
alt=""
|
||||
src=${this.imageUrl}
|
||||
class="icon"
|
||||
/>`
|
||||
: this.iconPath
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${this.iconPath}
|
||||
class="icon"
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</slot>
|
||||
<span class="label">
|
||||
${this.label}
|
||||
${
|
||||
|
||||
+18
-21
@@ -17,6 +17,7 @@ import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
|
||||
import { caseInsensitiveStringCompare } from "../../../../../common/string/compare";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-app-icon";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-icon-next";
|
||||
@@ -204,23 +205,12 @@ export class SerialConfigDashboard extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _renderConsumerIcon(src: string, alt: string): TemplateResult {
|
||||
return html`<img
|
||||
slot="start"
|
||||
.src=${src}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
alt=${alt}
|
||||
/>`;
|
||||
}
|
||||
|
||||
// The panel the integration behind this consumer is configured in, if it has
|
||||
// one. A stopped consumer has no panel loaded to send the user to.
|
||||
private _consumerPanel(consumer: SerialPortConsumer): string | undefined {
|
||||
if (!consumer.active) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const domain =
|
||||
consumer.kind === "config_entry"
|
||||
? consumer.domain
|
||||
@@ -253,22 +243,28 @@ export class SerialConfigDashboard extends LitElement {
|
||||
<ha-md-list-item type="link" href=${href} class="consumer">
|
||||
${
|
||||
consumer.kind === "config_entry"
|
||||
? this._renderConsumerIcon(
|
||||
brandsUrl(
|
||||
? html`<img
|
||||
slot="start"
|
||||
.src=${brandsUrl(
|
||||
{
|
||||
domain: consumer.domain!,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
),
|
||||
consumer.domain!
|
||||
)
|
||||
)}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
alt=${consumer.domain!}
|
||||
/>`
|
||||
: consumer.kind === "app"
|
||||
? this._renderConsumerIcon(
|
||||
`/api/hassio/addons/${consumer.slug}/icon`,
|
||||
consumer.slug!
|
||||
)
|
||||
? html`<ha-app-icon
|
||||
slot="start"
|
||||
.slug=${consumer.slug!}
|
||||
.alt=${consumer.title || consumer.slug!}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPuzzle}></ha-svg-icon>
|
||||
</ha-app-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPuzzle}
|
||||
@@ -673,7 +669,8 @@ export class SerialConfigDashboard extends LitElement {
|
||||
--md-list-item-leading-space: var(--ha-space-14);
|
||||
}
|
||||
|
||||
ha-md-list-item.consumer img[slot="start"] {
|
||||
ha-md-list-item.consumer img[slot="start"],
|
||||
ha-md-list-item.consumer ha-app-icon[slot="start"] {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { HASSDomTargetEvent } from "../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { stringCompare } from "../../../common/string/compare";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import "../../../components/ha-app-icon";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-generic-picker";
|
||||
import type { HaGenericPicker } from "../../../components/ha-generic-picker";
|
||||
@@ -24,7 +25,10 @@ import type { PickerComboBoxItem } from "../../../components/ha-picker-combo-box
|
||||
import "../../../components/input/ha-input-search";
|
||||
import type { HaInputSearch } from "../../../components/input/ha-input-search";
|
||||
import type { LogProvider } from "../../../data/error_log";
|
||||
import { fetchHassioAddonsInfo } from "../../../data/hassio/addon";
|
||||
import {
|
||||
fetchHassioAddonsInfo,
|
||||
type HassioAddonInfo,
|
||||
} from "../../../data/hassio/addon";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { mdiHomeAssistant } from "../../../resources/home-assistant-logo-svg";
|
||||
@@ -61,6 +65,11 @@ const logProviders: LogProvider[] = [
|
||||
},
|
||||
];
|
||||
|
||||
interface LogProviderPickerItem extends PickerComboBoxItem {
|
||||
addon?: HassioAddonInfo;
|
||||
hasAppIcon?: boolean;
|
||||
}
|
||||
|
||||
@customElement("ha-config-logs")
|
||||
export class HaConfigLogs extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -138,18 +147,9 @@ export class HaConfigLogs extends LitElement {
|
||||
@click=${this._openPicker}
|
||||
>
|
||||
${
|
||||
selectedProvider?.icon
|
||||
? html`<img
|
||||
src=${selectedProvider.icon}
|
||||
alt=${selectedProvider.primary}
|
||||
slot="start"
|
||||
/>`
|
||||
: selectedProvider?.icon_path
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${selectedProvider.icon_path}
|
||||
></ha-svg-icon>`
|
||||
: nothing
|
||||
selectedProvider
|
||||
? this._renderProviderIcon(selectedProvider)
|
||||
: nothing
|
||||
}
|
||||
${selectedProvider?.primary}
|
||||
<ha-svg-icon
|
||||
@@ -264,33 +264,40 @@ export class HaConfigLogs extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _getLogProviderItems = (): PickerComboBoxItem[] =>
|
||||
private _getLogProviderItems = (): LogProviderPickerItem[] =>
|
||||
this._logProviders.map((provider) => ({
|
||||
id: provider.key,
|
||||
primary: provider.name,
|
||||
icon: provider.addon
|
||||
addon: provider.addon,
|
||||
hasAppIcon: provider.addon
|
||||
? atLeastVersion(this.hass.config.version, 0, 105) &&
|
||||
provider.addon.icon
|
||||
? `/api/hassio/addons/${provider.addon.slug}/icon`
|
||||
: undefined
|
||||
: undefined,
|
||||
icon_path: provider.addon
|
||||
? mdiPuzzle
|
||||
: this._getProviderIconPath(provider.key),
|
||||
}));
|
||||
|
||||
private _providerRenderer = (item: PickerComboBoxItem) => html`
|
||||
private _renderProviderIcon(item: LogProviderPickerItem) {
|
||||
if (item.addon) {
|
||||
return html`<ha-app-icon
|
||||
slot="start"
|
||||
.alt=${item.primary}
|
||||
.hasIcon=${item.hasAppIcon}
|
||||
.slug=${item.addon.slug}
|
||||
>
|
||||
<ha-svg-icon .path=${item.icon_path}></ha-svg-icon>
|
||||
</ha-app-icon>`;
|
||||
}
|
||||
|
||||
return item.icon_path
|
||||
? html`<ha-svg-icon slot="start" .path=${item.icon_path}></ha-svg-icon>`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
private _providerRenderer = (item: LogProviderPickerItem) => html`
|
||||
<ha-combo-box-item type="button" compact>
|
||||
${
|
||||
item.icon
|
||||
? html`<img src=${item.icon} alt=${item.primary} slot="start" />`
|
||||
: item.icon_path
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${item.icon_path}
|
||||
></ha-svg-icon>`
|
||||
: nothing
|
||||
}
|
||||
${this._renderProviderIcon(item)}
|
||||
<span slot="headline">${item.primary}</span>
|
||||
${
|
||||
item.secondary
|
||||
@@ -308,11 +315,10 @@ export class HaConfigLogs extends LitElement {
|
||||
return {
|
||||
id: provider.key,
|
||||
primary: provider.name,
|
||||
icon: provider.addon
|
||||
addon: provider.addon,
|
||||
hasAppIcon: provider.addon
|
||||
? atLeastVersion(this.hass.config.version, 0, 105) &&
|
||||
provider.addon.icon
|
||||
? `/api/hassio/addons/${provider.addon.slug}/icon`
|
||||
: undefined
|
||||
: undefined,
|
||||
icon_path: provider.addon
|
||||
? mdiPuzzle
|
||||
@@ -365,8 +371,8 @@ export class HaConfigLogs extends LitElement {
|
||||
--mdc-icon-size: var(--ha-space-6);
|
||||
}
|
||||
|
||||
img {
|
||||
height: 32px;
|
||||
ha-app-icon {
|
||||
--ha-app-icon-size: 32px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveHassUrl } from "../common/url/hass-url";
|
||||
import { isExternalAndroid } from "../data/external";
|
||||
|
||||
// 10 seconds gives the Android WebView download listener enough time
|
||||
@@ -9,7 +10,7 @@ const BLOB_REVOKE_DELAY_MS = 10_000;
|
||||
export const fileDownload = (href: string, filename = ""): void => {
|
||||
const element = document.createElement("a");
|
||||
element.target = "_blank";
|
||||
element.href = href;
|
||||
element.href = resolveHassUrl(href);
|
||||
element.download = filename;
|
||||
element.style.display = "none";
|
||||
document.body.appendChild(element);
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { HaAppIcon } from "../../src/components/ha-app-icon";
|
||||
import "../../src/components/ha-app-icon";
|
||||
|
||||
let appIcon: HaAppIcon | undefined;
|
||||
|
||||
const mountAppIcon = async (properties: Partial<HaAppIcon> = {}) => {
|
||||
appIcon = document.createElement("ha-app-icon");
|
||||
Object.assign(appIcon, properties);
|
||||
appIcon.append(document.createElement("ha-svg-icon"));
|
||||
document.body.append(appIcon);
|
||||
await appIcon.updateComplete;
|
||||
return appIcon;
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
appIcon?.remove();
|
||||
appIcon = undefined;
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("ha-app-icon", () => {
|
||||
it("does not request an unavailable icon", async () => {
|
||||
const element = await mountAppIcon({ slug: "example", hasIcon: false });
|
||||
|
||||
expect(element.shadowRoot!.querySelector("img")).toBeNull();
|
||||
expect(element.shadowRoot!.querySelector("slot")).not.toBeNull();
|
||||
});
|
||||
|
||||
it.each([true, undefined])(
|
||||
"requests the icon when availability is %s",
|
||||
async (hasIcon) => {
|
||||
const element = await mountAppIcon({ slug: "example", hasIcon });
|
||||
|
||||
expect(
|
||||
element.shadowRoot!.querySelector("img")!.getAttribute("src")
|
||||
).toBe("/api/hassio/addons/example/icon");
|
||||
}
|
||||
);
|
||||
|
||||
it("requests the icon from the configured Home Assistant URL", async () => {
|
||||
vi.stubGlobal("__HASS_URL__", "http://homeassistant.local:8123");
|
||||
const element = await mountAppIcon({ slug: "example", hasIcon: true });
|
||||
const image = element.shadowRoot!.querySelector("img")!;
|
||||
|
||||
expect(image.getAttribute("src")).toBe(
|
||||
"http://homeassistant.local:8123/api/hassio/addons/example/icon"
|
||||
);
|
||||
expect(image.hasAttribute("crossorigin")).toBe(false);
|
||||
});
|
||||
|
||||
it("renders the fallback after an image error", async () => {
|
||||
const element = await mountAppIcon({ slug: "example", hasIcon: true });
|
||||
|
||||
element.shadowRoot!.querySelector("img")!.dispatchEvent(new Event("error"));
|
||||
await element.updateComplete;
|
||||
|
||||
expect(element.shadowRoot!.querySelector("img")).toBeNull();
|
||||
expect(element.shadowRoot!.querySelector("slot")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("tries a new source after the slug changes", async () => {
|
||||
const element = await mountAppIcon({ slug: "first", hasIcon: true });
|
||||
element.shadowRoot!.querySelector("img")!.dispatchEvent(new Event("error"));
|
||||
await element.updateComplete;
|
||||
|
||||
element.slug = "second";
|
||||
await element.updateComplete;
|
||||
|
||||
expect(element.shadowRoot!.querySelector("img")!.getAttribute("src")).toBe(
|
||||
"/api/hassio/addons/second/icon"
|
||||
);
|
||||
});
|
||||
|
||||
it("ignores an error from a previous source", async () => {
|
||||
const element = await mountAppIcon({ slug: "first", hasIcon: true });
|
||||
const firstImage = element.shadowRoot!.querySelector("img")!;
|
||||
|
||||
element.slug = "second";
|
||||
await element.updateComplete;
|
||||
firstImage.dispatchEvent(new Event("error"));
|
||||
await element.updateComplete;
|
||||
|
||||
expect(element.shadowRoot!.querySelector("img")!.getAttribute("src")).toBe(
|
||||
"/api/hassio/addons/second/icon"
|
||||
);
|
||||
});
|
||||
|
||||
it("ignores an old error after returning to the same source", async () => {
|
||||
const element = await mountAppIcon({ slug: "first", hasIcon: true });
|
||||
const firstImage = element.shadowRoot!.querySelector("img")!;
|
||||
|
||||
element.slug = "second";
|
||||
await element.updateComplete;
|
||||
element.slug = "first";
|
||||
await element.updateComplete;
|
||||
firstImage.dispatchEvent(new Event("error"));
|
||||
await element.updateComplete;
|
||||
|
||||
expect(element.shadowRoot!.querySelector("img")!.getAttribute("src")).toBe(
|
||||
"/api/hassio/addons/first/icon"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -36,6 +36,7 @@ describe("fileDownload", () => {
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
delete (window as any).externalApp;
|
||||
delete (window as any).externalAppV2;
|
||||
});
|
||||
@@ -58,6 +59,16 @@ describe("fileDownload", () => {
|
||||
expect(URL.revokeObjectURL).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resolves relative URLs against the configured Home Assistant URL", async () => {
|
||||
vi.stubGlobal("__HASS_URL__", "http://homeassistant.local:8123");
|
||||
await loadFileDownload();
|
||||
fileDownload("/api/hassio/core/logs/follow");
|
||||
|
||||
expect(createdElement.href).toBe(
|
||||
"http://homeassistant.local:8123/api/hassio/core/logs/follow"
|
||||
);
|
||||
});
|
||||
|
||||
it("revokes blob URLs immediately outside Android", async () => {
|
||||
await loadFileDownload();
|
||||
fileDownload("blob:http://localhost/abc-123", "file.json");
|
||||
|
||||
Reference in New Issue
Block a user