mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-18 14:56:37 +00:00
Overhaul Integrations page, add integration page (#16640)
Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
This commit is contained in:
parent
2b4f199337
commit
70fbf68603
@ -9,10 +9,13 @@ export const restoreScroll =
|
|||||||
key: element.key,
|
key: element.key,
|
||||||
descriptor: {
|
descriptor: {
|
||||||
set(this: LitElement, value: number) {
|
set(this: LitElement, value: number) {
|
||||||
|
history.replaceState({ scrollPosition: value }, "");
|
||||||
this[`__${String(element.key)}`] = value;
|
this[`__${String(element.key)}`] = value;
|
||||||
},
|
},
|
||||||
get(this: LitElement) {
|
get(this: LitElement) {
|
||||||
return this[`__${String(element.key)}`];
|
return (
|
||||||
|
this[`__${String(element.key)}`] || history.state?.scrollPosition
|
||||||
|
);
|
||||||
},
|
},
|
||||||
enumerable: true,
|
enumerable: true,
|
||||||
configurable: true,
|
configurable: true,
|
||||||
@ -21,12 +24,17 @@ export const restoreScroll =
|
|||||||
const connectedCallback = cls.prototype.connectedCallback;
|
const connectedCallback = cls.prototype.connectedCallback;
|
||||||
cls.prototype.connectedCallback = function () {
|
cls.prototype.connectedCallback = function () {
|
||||||
connectedCallback.call(this);
|
connectedCallback.call(this);
|
||||||
if (this[element.key]) {
|
const scrollPos = this[element.key];
|
||||||
|
if (scrollPos) {
|
||||||
|
this.updateComplete.then(() => {
|
||||||
const target = this.renderRoot.querySelector(selector);
|
const target = this.renderRoot.querySelector(selector);
|
||||||
if (!target) {
|
if (!target) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
target.scrollTop = this[element.key];
|
setTimeout(() => {
|
||||||
|
target.scrollTop = scrollPos;
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -5,6 +5,13 @@ import { customElement } from "lit/decorators";
|
|||||||
|
|
||||||
@customElement("ha-list-item")
|
@customElement("ha-list-item")
|
||||||
export class HaListItem extends ListItemBase {
|
export class HaListItem extends ListItemBase {
|
||||||
|
protected renderRipple() {
|
||||||
|
if (this.noninteractive) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return super.renderRipple();
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
styles,
|
styles,
|
||||||
@ -32,6 +39,7 @@ export class HaListItem extends ListItemBase {
|
|||||||
}
|
}
|
||||||
.mdc-deprecated-list-item__meta {
|
.mdc-deprecated-list-item__meta {
|
||||||
display: var(--mdc-list-item-meta-display);
|
display: var(--mdc-list-item-meta-display);
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
:host([multiline-secondary]) {
|
:host([multiline-secondary]) {
|
||||||
height: auto;
|
height: auto;
|
||||||
@ -60,6 +68,9 @@ export class HaListItem extends ListItemBase {
|
|||||||
:host([disabled]) {
|
:host([disabled]) {
|
||||||
color: var(--disabled-text-color);
|
color: var(--disabled-text-color);
|
||||||
}
|
}
|
||||||
|
:host([noninteractive]) {
|
||||||
|
pointer-events: unset;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -96,7 +96,7 @@ export class HaRelatedItems extends LitElement {
|
|||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<a
|
<a
|
||||||
href=${`/config/integrations#config_entry=${relatedConfigEntryId}`}
|
href=${`/config/integrations/integration/${entry.domain}#config_entry=${relatedConfigEntryId}`}
|
||||||
@click=${this._navigateAwayClose}
|
@click=${this._navigateAwayClose}
|
||||||
>
|
>
|
||||||
<ha-list-item hasMeta graphic="icon">
|
<ha-list-item hasMeta graphic="icon">
|
||||||
|
@ -3,6 +3,14 @@ import { LocalizeFunc } from "../common/translations/localize";
|
|||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import { debounce } from "../common/util/debounce";
|
import { debounce } from "../common/util/debounce";
|
||||||
|
|
||||||
|
export const integrationsWithPanel = {
|
||||||
|
matter: "/config/matter",
|
||||||
|
mqtt: "/config/mqtt",
|
||||||
|
thread: "/config/thread",
|
||||||
|
zha: "/config/zha/dashboard",
|
||||||
|
zwave_js: "/config/zwave_js/dashboard",
|
||||||
|
};
|
||||||
|
|
||||||
export type IntegrationType =
|
export type IntegrationType =
|
||||||
| "device"
|
| "device"
|
||||||
| "helper"
|
| "helper"
|
||||||
|
@ -9,7 +9,14 @@ import {
|
|||||||
mdiPlusCircle,
|
mdiPlusCircle,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import {
|
||||||
|
css,
|
||||||
|
CSSResultGroup,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
nothing,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
@ -261,6 +268,9 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
|
if (!this.devices || !this.deviceId) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
const device = this._device(this.deviceId, this.devices);
|
const device = this._device(this.deviceId, this.devices);
|
||||||
|
|
||||||
if (!device) {
|
if (!device) {
|
||||||
@ -290,7 +300,29 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
: undefined;
|
: undefined;
|
||||||
const area = this._computeArea(this.areas, device);
|
const area = this._computeArea(this.areas, device);
|
||||||
|
|
||||||
const deviceInfo: TemplateResult[] = [];
|
const deviceInfo: TemplateResult[] = integrations.map(
|
||||||
|
(integration) =>
|
||||||
|
html`<a
|
||||||
|
slot="actions"
|
||||||
|
href=${`/config/integrations/integration/${integration.domain}#config_entry=${integration.entry_id}`}
|
||||||
|
>
|
||||||
|
<ha-list-item graphic="icon" hasMeta>
|
||||||
|
<img
|
||||||
|
slot="graphic"
|
||||||
|
alt=${domainToName(this.hass.localize, integration.domain)}
|
||||||
|
src=${brandsUrl({
|
||||||
|
domain: integration.domain,
|
||||||
|
type: "icon",
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
})}
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
|
||||||
|
${domainToName(this.hass.localize, integration.domain)}
|
||||||
|
<ha-icon-next slot="meta"></ha-icon-next>
|
||||||
|
</ha-list-item>
|
||||||
|
</a>`
|
||||||
|
);
|
||||||
|
|
||||||
const actions = [...(this._deviceActions || [])];
|
const actions = [...(this._deviceActions || [])];
|
||||||
if (Array.isArray(this._diagnosticDownloadLinks)) {
|
if (Array.isArray(this._diagnosticDownloadLinks)) {
|
||||||
|
@ -128,6 +128,13 @@ export class HaConfigDeviceDashboard extends LitElement {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "domain": {
|
||||||
|
filterTexts.push(
|
||||||
|
`${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.integration"
|
||||||
|
)} "${domainToName(localize, value)}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return filterTexts.length ? filterTexts : undefined;
|
return filterTexts.length ? filterTexts : undefined;
|
||||||
@ -187,6 +194,15 @@ export class HaConfigDeviceDashboard extends LitElement {
|
|||||||
startLength = outputDevices.length;
|
startLength = outputDevices.length;
|
||||||
filterConfigEntry = entries.find((entry) => entry.entry_id === value);
|
filterConfigEntry = entries.find((entry) => entry.entry_id === value);
|
||||||
}
|
}
|
||||||
|
if (key === "domain") {
|
||||||
|
const entryIds = entries
|
||||||
|
.filter((entry) => entry.domain === value)
|
||||||
|
.map((entry) => entry.entry_id);
|
||||||
|
outputDevices = outputDevices.filter((device) =>
|
||||||
|
device.config_entries.some((entryId) => entryIds.includes(entryId))
|
||||||
|
);
|
||||||
|
startLength = outputDevices.length;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!showDisabled) {
|
if (!showDisabled) {
|
||||||
@ -383,8 +399,11 @@ export class HaConfigDeviceDashboard extends LitElement {
|
|||||||
|
|
||||||
public willUpdate(changedProps) {
|
public willUpdate(changedProps) {
|
||||||
if (changedProps.has("_searchParms")) {
|
if (changedProps.has("_searchParms")) {
|
||||||
if (this._searchParms.get("config_entry")) {
|
if (
|
||||||
// If we are requested to show the devices for a given config entry,
|
this._searchParms.get("config_entry") ||
|
||||||
|
this._searchParms.get("domain")
|
||||||
|
) {
|
||||||
|
// If we are requested to show the devices for a given config entry / domain,
|
||||||
// also show the disabled ones by default.
|
// also show the disabled ones by default.
|
||||||
this._showDisabled = true;
|
this._showDisabled = true;
|
||||||
}
|
}
|
||||||
@ -548,7 +567,9 @@ export class HaConfigDeviceDashboard extends LitElement {
|
|||||||
showMatterAddDeviceDialog(this);
|
showMatterAddDeviceDialog(this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showAddIntegrationDialog(this);
|
showAddIntegrationDialog(this, {
|
||||||
|
domain: this._searchParms.get("domain") || undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _showZJSAddDeviceDialog(filteredConfigEntry: ConfigEntry) {
|
private _showZJSAddDeviceDialog(filteredConfigEntry: ConfigEntry) {
|
||||||
|
@ -230,7 +230,7 @@ export class EnergyGridSettings extends LitElement {
|
|||||||
/>
|
/>
|
||||||
<span class="content">${this._co2ConfigEntry.title}</span>
|
<span class="content">${this._co2ConfigEntry.title}</span>
|
||||||
<a
|
<a
|
||||||
href=${`/config/integrations#config_entry=${this._co2ConfigEntry.entry_id}`}
|
href=${`/config/integrations/integration/${this._co2ConfigEntry?.domain}`}
|
||||||
>
|
>
|
||||||
<ha-icon-button .path=${mdiPencil}></ha-icon-button>
|
<ha-icon-button .path=${mdiPencil}></ha-icon-button>
|
||||||
</a>
|
</a>
|
||||||
|
@ -159,6 +159,14 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "domain": {
|
||||||
|
this._showDisabled = true;
|
||||||
|
filterTexts.push(
|
||||||
|
`${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.integration"
|
||||||
|
)} "${domainToName(localize, value)}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return filterTexts.length ? filterTexts : undefined;
|
return filterTexts.length ? filterTexts : undefined;
|
||||||
@ -368,6 +376,22 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
filteredDomains.push(configEntry.domain);
|
filteredDomains.push(configEntry.domain);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (key === "domain") {
|
||||||
|
if (!entries) {
|
||||||
|
this._loadConfigEntries();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const entryIds = entries
|
||||||
|
.filter((entry) => entry.domain === value)
|
||||||
|
.map((entry) => entry.entry_id);
|
||||||
|
filteredEntities = filteredEntities.filter(
|
||||||
|
(entity) =>
|
||||||
|
entity.config_entry_id &&
|
||||||
|
entryIds.includes(entity.config_entry_id)
|
||||||
|
);
|
||||||
|
filteredDomains.push(value);
|
||||||
|
startLength = filteredEntities.length;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!showDisabled) {
|
if (!showDisabled) {
|
||||||
|
@ -60,7 +60,7 @@ export class HaConfigFlowCard extends LitElement {
|
|||||||
}`
|
}`
|
||||||
)}
|
)}
|
||||||
></mwc-button>
|
></mwc-button>
|
||||||
<ha-button-menu>
|
<ha-button-menu slot="header-button">
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
slot="trigger"
|
slot="trigger"
|
||||||
.label=${this.hass.localize("ui.common.menu")}
|
.label=${this.hass.localize("ui.common.menu")}
|
||||||
@ -186,6 +186,9 @@ export class HaConfigFlowCard extends LitElement {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
ha-button-menu {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
ha-svg-icon[slot="meta"] {
|
ha-svg-icon[slot="meta"] {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
|
1269
src/panels/config/integrations/ha-config-integration-page.ts
Normal file
1269
src/panels/config/integrations/ha-config-integration-page.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,862 @@
|
|||||||
|
import { ActionDetail } from "@material/mwc-list";
|
||||||
|
import { mdiFilterVariant, mdiPlus } from "@mdi/js";
|
||||||
|
import Fuse from "fuse.js";
|
||||||
|
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResultGroup,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
nothing,
|
||||||
|
PropertyValues,
|
||||||
|
} from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
|
import {
|
||||||
|
protocolIntegrationPicked,
|
||||||
|
PROTOCOL_INTEGRATIONS,
|
||||||
|
} from "../../../common/integrations/protocolIntegrationPicked";
|
||||||
|
import { navigate } from "../../../common/navigate";
|
||||||
|
import { extractSearchParam } from "../../../common/url/search-params";
|
||||||
|
import { nextRender } from "../../../common/util/render-status";
|
||||||
|
import "../../../components/ha-button-menu";
|
||||||
|
import "../../../components/ha-check-list-item";
|
||||||
|
import "../../../components/ha-checkbox";
|
||||||
|
import "../../../components/ha-fab";
|
||||||
|
import "../../../components/ha-icon-button";
|
||||||
|
import "../../../components/ha-svg-icon";
|
||||||
|
import "../../../components/search-input";
|
||||||
|
import { ConfigEntry } from "../../../data/config_entries";
|
||||||
|
import { getConfigFlowInProgressCollection } from "../../../data/config_flow";
|
||||||
|
import { fetchDiagnosticHandlers } from "../../../data/diagnostics";
|
||||||
|
import {
|
||||||
|
EntityRegistryEntry,
|
||||||
|
subscribeEntityRegistry,
|
||||||
|
} from "../../../data/entity_registry";
|
||||||
|
import {
|
||||||
|
domainToName,
|
||||||
|
fetchIntegrationManifest,
|
||||||
|
fetchIntegrationManifests,
|
||||||
|
IntegrationLogInfo,
|
||||||
|
IntegrationManifest,
|
||||||
|
subscribeLogInfo,
|
||||||
|
} from "../../../data/integration";
|
||||||
|
import {
|
||||||
|
findIntegration,
|
||||||
|
getIntegrationDescriptions,
|
||||||
|
} from "../../../data/integrations";
|
||||||
|
import { scanUSBDevices } from "../../../data/usb";
|
||||||
|
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
||||||
|
import {
|
||||||
|
showAlertDialog,
|
||||||
|
showConfirmationDialog,
|
||||||
|
} from "../../../dialogs/generic/show-dialog-box";
|
||||||
|
import "../../../layouts/hass-loading-screen";
|
||||||
|
import "../../../layouts/hass-tabs-subpage";
|
||||||
|
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||||
|
import { haStyle } from "../../../resources/styles";
|
||||||
|
import type { HomeAssistant, Route } from "../../../types";
|
||||||
|
import { configSections } from "../ha-panel-config";
|
||||||
|
import { isHelperDomain } from "../helpers/const";
|
||||||
|
import "./ha-config-flow-card";
|
||||||
|
import { DataEntryFlowProgressExtended } from "./ha-config-integrations";
|
||||||
|
import "./ha-ignored-config-entry-card";
|
||||||
|
import "./ha-integration-card";
|
||||||
|
import type { HaIntegrationCard } from "./ha-integration-card";
|
||||||
|
import "./ha-integration-overflow-menu";
|
||||||
|
import { showAddIntegrationDialog } from "./show-add-integration-dialog";
|
||||||
|
import "./ha-disabled-config-entry-card";
|
||||||
|
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||||
|
|
||||||
|
export interface ConfigEntryExtended extends ConfigEntry {
|
||||||
|
localized_domain_name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupByIntegration = (
|
||||||
|
entries: ConfigEntryExtended[]
|
||||||
|
): Map<string, ConfigEntryExtended[]> => {
|
||||||
|
const result = new Map();
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (result.has(entry.domain)) {
|
||||||
|
result.get(entry.domain).push(entry);
|
||||||
|
} else {
|
||||||
|
result.set(entry.domain, [entry]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
@customElement("ha-config-integrations-dashboard")
|
||||||
|
class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
|
||||||
|
|
||||||
|
@property() public isWide!: boolean;
|
||||||
|
|
||||||
|
@property() public showAdvanced!: boolean;
|
||||||
|
|
||||||
|
@property() public route!: Route;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public configEntries?: ConfigEntryExtended[];
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public configEntriesInProgress?: DataEntryFlowProgressExtended[];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private _entityRegistryEntries: EntityRegistryEntry[] = [];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private _manifests: Record<string, IntegrationManifest> = {};
|
||||||
|
|
||||||
|
private _extraFetchedManifests?: Set<string>;
|
||||||
|
|
||||||
|
@state() private _showIgnored = false;
|
||||||
|
|
||||||
|
@state() private _showDisabled = false;
|
||||||
|
|
||||||
|
@state() private _searchParms = new URLSearchParams(
|
||||||
|
window.location.hash.substring(1)
|
||||||
|
);
|
||||||
|
|
||||||
|
@state() private _filter: string = history.state?.filter || "";
|
||||||
|
|
||||||
|
@state() private _diagnosticHandlers?: Record<string, boolean>;
|
||||||
|
|
||||||
|
@state() private _logInfos?: {
|
||||||
|
[integration: string]: IntegrationLogInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
public hassSubscribe(): Array<UnsubscribeFunc | Promise<UnsubscribeFunc>> {
|
||||||
|
return [
|
||||||
|
subscribeEntityRegistry(this.hass.connection, (entries) => {
|
||||||
|
this._entityRegistryEntries = entries;
|
||||||
|
}),
|
||||||
|
subscribeLogInfo(this.hass.connection, (log_infos) => {
|
||||||
|
const logInfoLookup: { [integration: string]: IntegrationLogInfo } = {};
|
||||||
|
for (const log_info of log_infos) {
|
||||||
|
logInfoLookup[log_info.domain] = log_info;
|
||||||
|
}
|
||||||
|
this._logInfos = logInfoLookup;
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _filterConfigEntries = memoizeOne(
|
||||||
|
(
|
||||||
|
configEntries: ConfigEntryExtended[],
|
||||||
|
filter?: string
|
||||||
|
): [
|
||||||
|
[string, ConfigEntryExtended[]][],
|
||||||
|
ConfigEntryExtended[],
|
||||||
|
ConfigEntryExtended[]
|
||||||
|
] => {
|
||||||
|
let filteredConfigEntries: ConfigEntryExtended[];
|
||||||
|
const ignored: ConfigEntryExtended[] = [];
|
||||||
|
const disabled: ConfigEntryExtended[] = [];
|
||||||
|
const integrations: ConfigEntryExtended[] = [];
|
||||||
|
if (filter) {
|
||||||
|
const options: Fuse.IFuseOptions<ConfigEntryExtended> = {
|
||||||
|
keys: ["domain", "localized_domain_name", "title"],
|
||||||
|
isCaseSensitive: false,
|
||||||
|
minMatchCharLength: 2,
|
||||||
|
threshold: 0.2,
|
||||||
|
};
|
||||||
|
const fuse = new Fuse(configEntries, options);
|
||||||
|
filteredConfigEntries = fuse
|
||||||
|
.search(filter)
|
||||||
|
.map((result) => result.item);
|
||||||
|
} else {
|
||||||
|
filteredConfigEntries = configEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of filteredConfigEntries) {
|
||||||
|
if (entry.source === "ignore") {
|
||||||
|
ignored.push(entry);
|
||||||
|
} else if (entry.disabled_by !== null) {
|
||||||
|
disabled.push(entry);
|
||||||
|
} else {
|
||||||
|
integrations.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
Array.from(groupByIntegration(integrations)).sort((groupA, groupB) =>
|
||||||
|
caseInsensitiveStringCompare(
|
||||||
|
groupA[1][0].localized_domain_name || groupA[0],
|
||||||
|
groupB[1][0].localized_domain_name || groupB[0],
|
||||||
|
this.hass.locale.language
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ignored,
|
||||||
|
disabled,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private _filterConfigEntriesInProgress = memoizeOne(
|
||||||
|
(
|
||||||
|
configEntriesInProgress: DataEntryFlowProgressExtended[],
|
||||||
|
filter?: string
|
||||||
|
): DataEntryFlowProgressExtended[] => {
|
||||||
|
let filteredEntries: DataEntryFlowProgressExtended[];
|
||||||
|
if (filter) {
|
||||||
|
const options: Fuse.IFuseOptions<DataEntryFlowProgressExtended> = {
|
||||||
|
keys: ["handler", "localized_title"],
|
||||||
|
isCaseSensitive: false,
|
||||||
|
minMatchCharLength: 2,
|
||||||
|
threshold: 0.2,
|
||||||
|
};
|
||||||
|
const fuse = new Fuse(configEntriesInProgress, options);
|
||||||
|
filteredEntries = fuse.search(filter).map((result) => result.item);
|
||||||
|
} else {
|
||||||
|
filteredEntries = configEntriesInProgress;
|
||||||
|
}
|
||||||
|
return filteredEntries.sort((a, b) =>
|
||||||
|
caseInsensitiveStringCompare(
|
||||||
|
a.localized_title || a.handler,
|
||||||
|
b.localized_title || b.handler,
|
||||||
|
this.hass.locale.language
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
protected firstUpdated(changed: PropertyValues) {
|
||||||
|
super.firstUpdated(changed);
|
||||||
|
this._fetchManifests();
|
||||||
|
if (this.route.path === "/add") {
|
||||||
|
this._handleAdd();
|
||||||
|
}
|
||||||
|
this._scanUSBDevices();
|
||||||
|
if (isComponentLoaded(this.hass, "diagnostics")) {
|
||||||
|
fetchDiagnosticHandlers(this.hass).then((infos) => {
|
||||||
|
const handlers = {};
|
||||||
|
for (const info of infos) {
|
||||||
|
handlers[info.domain] = info.handlers.config_entry;
|
||||||
|
}
|
||||||
|
this._diagnosticHandlers = handlers;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changed: PropertyValues) {
|
||||||
|
super.updated(changed);
|
||||||
|
if (
|
||||||
|
(this._searchParms.has("config_entry") ||
|
||||||
|
this._searchParms.has("domain")) &&
|
||||||
|
changed.has("configEntries") &&
|
||||||
|
!changed.get("configEntries") &&
|
||||||
|
this.configEntries
|
||||||
|
) {
|
||||||
|
this._highlightEntry();
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
changed.has("configEntriesInProgress") &&
|
||||||
|
this.configEntriesInProgress
|
||||||
|
) {
|
||||||
|
this._fetchIntegrationManifests(
|
||||||
|
this.configEntriesInProgress.map((flow) => flow.handler)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this.configEntries || !this.configEntriesInProgress) {
|
||||||
|
return html`<hass-loading-screen
|
||||||
|
.hass=${this.hass}
|
||||||
|
.narrow=${this.narrow}
|
||||||
|
></hass-loading-screen>`;
|
||||||
|
}
|
||||||
|
const [integrations, ignoredConfigEntries, disabledConfigEntries] =
|
||||||
|
this._filterConfigEntries(this.configEntries, this._filter);
|
||||||
|
const configEntriesInProgress = this._filterConfigEntriesInProgress(
|
||||||
|
this.configEntriesInProgress,
|
||||||
|
this._filter
|
||||||
|
);
|
||||||
|
|
||||||
|
const filterMenu = html`
|
||||||
|
<div slot=${ifDefined(this.narrow ? "toolbar-icon" : undefined)}>
|
||||||
|
<div class="menu-badge-container">
|
||||||
|
${!this._showDisabled && this.narrow && disabledConfigEntries.length
|
||||||
|
? html`<span class="badge">${disabledConfigEntries.length}</span>`
|
||||||
|
: ""}
|
||||||
|
<ha-button-menu
|
||||||
|
multi
|
||||||
|
@action=${this._handleMenuAction}
|
||||||
|
@click=${this._preventDefault}
|
||||||
|
>
|
||||||
|
<ha-icon-button
|
||||||
|
slot="trigger"
|
||||||
|
.label=${this.hass.localize("ui.common.menu")}
|
||||||
|
.path=${mdiFilterVariant}
|
||||||
|
>
|
||||||
|
</ha-icon-button>
|
||||||
|
<ha-check-list-item left .selected=${this._showIgnored}>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.ignore.show_ignored"
|
||||||
|
)}
|
||||||
|
</ha-check-list-item>
|
||||||
|
<ha-check-list-item left .selected=${this._showDisabled}>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.disable.show_disabled"
|
||||||
|
)}
|
||||||
|
</ha-check-list-item>
|
||||||
|
</ha-button-menu>
|
||||||
|
</div>
|
||||||
|
${this.narrow
|
||||||
|
? html`
|
||||||
|
<ha-integration-overflow-menu
|
||||||
|
.hass=${this.hass}
|
||||||
|
slot="toolbar-icon"
|
||||||
|
></ha-integration-overflow-menu>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<hass-tabs-subpage
|
||||||
|
.hass=${this.hass}
|
||||||
|
.narrow=${this.narrow}
|
||||||
|
back-path="/config"
|
||||||
|
.route=${this.route}
|
||||||
|
.tabs=${configSections.devices}
|
||||||
|
>
|
||||||
|
${this.narrow
|
||||||
|
? html`
|
||||||
|
<div slot="header">
|
||||||
|
<search-input
|
||||||
|
.hass=${this.hass}
|
||||||
|
.filter=${this._filter}
|
||||||
|
class="header"
|
||||||
|
@value-changed=${this._handleSearchChange}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.search"
|
||||||
|
)}
|
||||||
|
></search-input>
|
||||||
|
</div>
|
||||||
|
${filterMenu}
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<ha-integration-overflow-menu
|
||||||
|
.hass=${this.hass}
|
||||||
|
slot="toolbar-icon"
|
||||||
|
></ha-integration-overflow-menu>
|
||||||
|
<div class="search">
|
||||||
|
<search-input
|
||||||
|
.hass=${this.hass}
|
||||||
|
suffix
|
||||||
|
.filter=${this._filter}
|
||||||
|
@value-changed=${this._handleSearchChange}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.search"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div class="filters" slot="suffix">
|
||||||
|
${!this._showDisabled && disabledConfigEntries.length
|
||||||
|
? html`<div
|
||||||
|
class="active-filters"
|
||||||
|
@click=${this._preventDefault}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.disable.disabled_integrations",
|
||||||
|
{ number: disabledConfigEntries.length }
|
||||||
|
)}
|
||||||
|
<mwc-button
|
||||||
|
@click=${this._toggleShowDisabled}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.disable.show"
|
||||||
|
)}
|
||||||
|
></mwc-button>
|
||||||
|
</div>`
|
||||||
|
: ""}
|
||||||
|
${filterMenu}
|
||||||
|
</div>
|
||||||
|
</search-input>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
${this._showIgnored
|
||||||
|
? ignoredConfigEntries.map(
|
||||||
|
(entry: ConfigEntryExtended) => html`
|
||||||
|
<ha-ignored-config-entry-card
|
||||||
|
.hass=${this.hass}
|
||||||
|
.manifest=${this._manifests[entry.domain]}
|
||||||
|
.entry=${entry}
|
||||||
|
@change=${this._handleFlowUpdated}
|
||||||
|
></ha-ignored-config-entry-card>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
: ""}
|
||||||
|
${configEntriesInProgress.length
|
||||||
|
? configEntriesInProgress.map(
|
||||||
|
(flow: DataEntryFlowProgressExtended) => html`
|
||||||
|
<ha-config-flow-card
|
||||||
|
.hass=${this.hass}
|
||||||
|
.manifest=${this._manifests[flow.handler]}
|
||||||
|
.flow=${flow}
|
||||||
|
@change=${this._handleFlowUpdated}
|
||||||
|
></ha-config-flow-card>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
: ""}
|
||||||
|
${this._showDisabled
|
||||||
|
? disabledConfigEntries.map(
|
||||||
|
(entry: ConfigEntryExtended) => html`
|
||||||
|
<ha-disabled-config-entry-card
|
||||||
|
.hass=${this.hass}
|
||||||
|
.entry=${entry}
|
||||||
|
.manifest=${this._manifests[entry.domain]}
|
||||||
|
.entityRegistryEntries=${this._entityRegistryEntries}
|
||||||
|
></ha-disabled-config-entry-card>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
: ""}
|
||||||
|
${integrations.length
|
||||||
|
? integrations.map(
|
||||||
|
([domain, items]) =>
|
||||||
|
html`<ha-integration-card
|
||||||
|
data-domain=${domain}
|
||||||
|
.hass=${this.hass}
|
||||||
|
.domain=${domain}
|
||||||
|
.items=${items}
|
||||||
|
.manifest=${this._manifests[domain]}
|
||||||
|
.entityRegistryEntries=${this._entityRegistryEntries}
|
||||||
|
.supportsDiagnostics=${this._diagnosticHandlers
|
||||||
|
? this._diagnosticHandlers[domain]
|
||||||
|
: false}
|
||||||
|
.logInfo=${this._logInfos
|
||||||
|
? this._logInfos[domain]
|
||||||
|
: nothing}
|
||||||
|
></ha-integration-card>`
|
||||||
|
)
|
||||||
|
: this._filter &&
|
||||||
|
!configEntriesInProgress.length &&
|
||||||
|
!integrations.length &&
|
||||||
|
this.configEntries.length
|
||||||
|
? html`
|
||||||
|
<div class="empty-message">
|
||||||
|
<h1>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.none_found"
|
||||||
|
)}
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.none_found_detail"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<mwc-button
|
||||||
|
@click=${this._createFlow}
|
||||||
|
unelevated
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.add_integration"
|
||||||
|
)}
|
||||||
|
></mwc-button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: // If we have a filter, never show a card
|
||||||
|
this._filter
|
||||||
|
? ""
|
||||||
|
: // If we're showing 0 cards, show empty state text
|
||||||
|
(!this._showIgnored || ignoredConfigEntries.length === 0) &&
|
||||||
|
(!this._showDisabled || disabledConfigEntries.length === 0) &&
|
||||||
|
integrations.length === 0
|
||||||
|
? html`
|
||||||
|
<div class="empty-message">
|
||||||
|
<h1>
|
||||||
|
${this.hass.localize("ui.panel.config.integrations.none")}
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.no_integrations"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<mwc-button
|
||||||
|
@click=${this._createFlow}
|
||||||
|
unelevated
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.add_integration"
|
||||||
|
)}
|
||||||
|
></mwc-button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
<ha-fab
|
||||||
|
slot="fab"
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.add_integration"
|
||||||
|
)}
|
||||||
|
extended
|
||||||
|
@click=${this._createFlow}
|
||||||
|
>
|
||||||
|
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||||
|
</ha-fab>
|
||||||
|
</hass-tabs-subpage>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _preventDefault(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _scanUSBDevices() {
|
||||||
|
if (!isComponentLoaded(this.hass, "usb")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await scanUSBDevices(this.hass);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _fetchManifests(integrations?: string[]) {
|
||||||
|
const fetched = await fetchIntegrationManifests(this.hass, integrations);
|
||||||
|
// Make a copy so we can keep track of previously loaded manifests
|
||||||
|
// for discovered flows (which are not part of these results)
|
||||||
|
const manifests = { ...this._manifests };
|
||||||
|
for (const manifest of fetched) {
|
||||||
|
manifests[manifest.domain] = manifest;
|
||||||
|
}
|
||||||
|
this._manifests = manifests;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _fetchIntegrationManifests(integrations: string[]) {
|
||||||
|
const manifestsToFetch: string[] = [];
|
||||||
|
for (const integration of integrations) {
|
||||||
|
if (integration in this._manifests) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (this._extraFetchedManifests) {
|
||||||
|
if (this._extraFetchedManifests.has(integration)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._extraFetchedManifests = new Set();
|
||||||
|
}
|
||||||
|
this._extraFetchedManifests.add(integration);
|
||||||
|
manifestsToFetch.push(integration);
|
||||||
|
}
|
||||||
|
if (manifestsToFetch.length) {
|
||||||
|
await this._fetchManifests(manifestsToFetch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleFlowUpdated() {
|
||||||
|
getConfigFlowInProgressCollection(this.hass.connection).refresh();
|
||||||
|
this._fetchManifests();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createFlow() {
|
||||||
|
showAddIntegrationDialog(this, {
|
||||||
|
initialFilter: this._filter,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||||
|
switch (ev.detail.index) {
|
||||||
|
case 0:
|
||||||
|
this._showIgnored = !this._showIgnored;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
this._toggleShowDisabled();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _toggleShowDisabled() {
|
||||||
|
this._showDisabled = !this._showDisabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleSearchChange(ev: CustomEvent) {
|
||||||
|
this._filter = ev.detail.value;
|
||||||
|
history.replaceState({ filter: this._filter }, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _highlightEntry() {
|
||||||
|
await nextRender();
|
||||||
|
const entryId = this._searchParms.get("config_entry");
|
||||||
|
let domain: string | null;
|
||||||
|
if (entryId) {
|
||||||
|
const configEntry = this.configEntries!.find(
|
||||||
|
(entry) => entry.entry_id === entryId
|
||||||
|
);
|
||||||
|
if (!configEntry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
domain = configEntry.domain;
|
||||||
|
} else {
|
||||||
|
domain = this._searchParms.get("domain");
|
||||||
|
}
|
||||||
|
const card: HaIntegrationCard = this.shadowRoot!.querySelector(
|
||||||
|
`[data-domain=${domain}]`
|
||||||
|
) as HaIntegrationCard;
|
||||||
|
if (card) {
|
||||||
|
card.scrollIntoView({
|
||||||
|
block: "center",
|
||||||
|
});
|
||||||
|
card.classList.add("highlight");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _handleAdd() {
|
||||||
|
const brand = extractSearchParam("brand");
|
||||||
|
const domain = extractSearchParam("domain");
|
||||||
|
navigate("/config/integrations", { replace: true });
|
||||||
|
|
||||||
|
if (brand) {
|
||||||
|
showAddIntegrationDialog(this, {
|
||||||
|
brand,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!domain) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const descriptions = await getIntegrationDescriptions(this.hass);
|
||||||
|
const integrations = {
|
||||||
|
...descriptions.core.integration,
|
||||||
|
...descriptions.custom.integration,
|
||||||
|
};
|
||||||
|
|
||||||
|
const integration = findIntegration(integrations, domain);
|
||||||
|
|
||||||
|
if (integration?.config_flow) {
|
||||||
|
// Integration exists, so we can just create a flow
|
||||||
|
const localize = await this.hass.loadBackendTranslation(
|
||||||
|
"title",
|
||||||
|
domain,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
await showConfirmationDialog(this, {
|
||||||
|
title: localize("ui.panel.config.integrations.confirm_new", {
|
||||||
|
integration: integration.name || domainToName(localize, domain),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
showAddIntegrationDialog(this, {
|
||||||
|
domain,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (integration?.supported_by) {
|
||||||
|
// Integration is a alias, so we can just create a flow
|
||||||
|
const localize = await this.hass.loadBackendTranslation(
|
||||||
|
"title",
|
||||||
|
domain,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
const supportedIntegration = findIntegration(
|
||||||
|
integrations,
|
||||||
|
integration.supported_by
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!supportedIntegration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showConfirmationDialog(this, {
|
||||||
|
text: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_flow.supported_brand_flow",
|
||||||
|
{
|
||||||
|
supported_brand: integration.name || domainToName(localize, domain),
|
||||||
|
flow_domain_name:
|
||||||
|
supportedIntegration.name ||
|
||||||
|
domainToName(localize, integration.supported_by),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
confirm: async () => {
|
||||||
|
if (
|
||||||
|
(PROTOCOL_INTEGRATIONS as ReadonlyArray<string>).includes(
|
||||||
|
integration.supported_by!
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
protocolIntegrationPicked(
|
||||||
|
this,
|
||||||
|
this.hass,
|
||||||
|
integration.supported_by!
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showConfigFlowDialog(this, {
|
||||||
|
dialogClosedCallback: () => {
|
||||||
|
this._handleFlowUpdated();
|
||||||
|
},
|
||||||
|
startFlowHandler: integration.supported_by,
|
||||||
|
manifest: await fetchIntegrationManifest(
|
||||||
|
this.hass,
|
||||||
|
integration.supported_by!
|
||||||
|
),
|
||||||
|
showAdvanced: this.hass.userData?.showAdvanced,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not an integration or supported brand, try helper else show alert
|
||||||
|
if (isHelperDomain(domain)) {
|
||||||
|
navigate(`/config/helpers/add?domain=${domain}`, {
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const helpers = {
|
||||||
|
...descriptions.core.helper,
|
||||||
|
...descriptions.custom.helper,
|
||||||
|
};
|
||||||
|
const helper = findIntegration(helpers, domain);
|
||||||
|
if (helper) {
|
||||||
|
navigate(`/config/helpers/add?domain=${domain}`, {
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_flow.error"
|
||||||
|
),
|
||||||
|
text: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_flow.no_config_flow"
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
css`
|
||||||
|
:host([narrow]) hass-tabs-subpage {
|
||||||
|
--main-title-margin: 0;
|
||||||
|
}
|
||||||
|
ha-button-menu {
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
margin-inline-end: initial;
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
grid-gap: 16px 16px;
|
||||||
|
padding: 8px 16px 16px;
|
||||||
|
margin-bottom: 64px;
|
||||||
|
}
|
||||||
|
.container > * {
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
.empty-message {
|
||||||
|
margin: auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.empty-message h1 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
search-input {
|
||||||
|
--mdc-text-field-fill-color: var(--sidebar-background-color);
|
||||||
|
--mdc-text-field-idle-line-color: var(--divider-color);
|
||||||
|
--text-field-overflow: visible;
|
||||||
|
}
|
||||||
|
search-input.header {
|
||||||
|
display: block;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
margin-inline-end: initial;
|
||||||
|
direction: var(--direction);
|
||||||
|
--mdc-ripple-color: transparant;
|
||||||
|
}
|
||||||
|
.search {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
height: 56px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.search search-input {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.filters {
|
||||||
|
--mdc-text-field-fill-color: var(--input-fill-color);
|
||||||
|
--mdc-text-field-idle-line-color: var(--input-idle-line-color);
|
||||||
|
--mdc-shape-small: 4px;
|
||||||
|
--text-field-overflow: initial;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
.active-filters {
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
padding-right: 2px;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-inline-start: 8px;
|
||||||
|
padding-inline-end: 2px;
|
||||||
|
font-size: 14px;
|
||||||
|
width: max-content;
|
||||||
|
cursor: initial;
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
.active-filters mwc-button {
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
margin-inline-end: initial;
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
.active-filters::before {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
opacity: 0.12;
|
||||||
|
border-radius: 4px;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
min-width: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: 400;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
line-height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0px 4px;
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
position: absolute;
|
||||||
|
right: 0px;
|
||||||
|
top: 4px;
|
||||||
|
font-size: 0.65em;
|
||||||
|
}
|
||||||
|
.menu-badge-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
ha-button-menu {
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-config-integrations-dashboard": HaConfigIntegrationsDashboard;
|
||||||
|
}
|
||||||
|
}
|
@ -1,81 +1,26 @@
|
|||||||
import { ActionDetail } from "@material/mwc-list";
|
import { PropertyValues } from "lit";
|
||||||
import { mdiFilterVariant, mdiPlus } from "@mdi/js";
|
|
||||||
import Fuse from "fuse.js";
|
|
||||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
|
||||||
import {
|
|
||||||
css,
|
|
||||||
CSSResultGroup,
|
|
||||||
html,
|
|
||||||
LitElement,
|
|
||||||
nothing,
|
|
||||||
PropertyValues,
|
|
||||||
} from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
|
||||||
import {
|
|
||||||
protocolIntegrationPicked,
|
|
||||||
PROTOCOL_INTEGRATIONS,
|
|
||||||
} from "../../../common/integrations/protocolIntegrationPicked";
|
|
||||||
import { navigate } from "../../../common/navigate";
|
import { navigate } from "../../../common/navigate";
|
||||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
|
||||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
|
||||||
import { extractSearchParam } from "../../../common/url/search-params";
|
|
||||||
import { nextRender } from "../../../common/util/render-status";
|
|
||||||
import "../../../components/ha-button-menu";
|
|
||||||
import "../../../components/ha-check-list-item";
|
|
||||||
import "../../../components/ha-checkbox";
|
|
||||||
import "../../../components/ha-fab";
|
|
||||||
import "../../../components/ha-icon-button";
|
|
||||||
import "../../../components/ha-svg-icon";
|
|
||||||
import "../../../components/search-input";
|
|
||||||
import {
|
import {
|
||||||
ConfigEntry,
|
ConfigEntry,
|
||||||
subscribeConfigEntries,
|
subscribeConfigEntries,
|
||||||
} from "../../../data/config_entries";
|
} from "../../../data/config_entries";
|
||||||
import {
|
import {
|
||||||
getConfigFlowInProgressCollection,
|
|
||||||
localizeConfigFlowTitle,
|
localizeConfigFlowTitle,
|
||||||
subscribeConfigFlowInProgress,
|
subscribeConfigFlowInProgress,
|
||||||
} from "../../../data/config_flow";
|
} from "../../../data/config_flow";
|
||||||
import type { DataEntryFlowProgress } from "../../../data/data_entry_flow";
|
import { DataEntryFlowProgress } from "../../../data/data_entry_flow";
|
||||||
import { fetchDiagnosticHandlers } from "../../../data/diagnostics";
|
import { domainToName } from "../../../data/integration";
|
||||||
import {
|
|
||||||
EntityRegistryEntry,
|
|
||||||
subscribeEntityRegistry,
|
|
||||||
} from "../../../data/entity_registry";
|
|
||||||
import {
|
|
||||||
domainToName,
|
|
||||||
fetchIntegrationManifest,
|
|
||||||
fetchIntegrationManifests,
|
|
||||||
IntegrationLogInfo,
|
|
||||||
IntegrationManifest,
|
|
||||||
subscribeLogInfo,
|
|
||||||
} from "../../../data/integration";
|
|
||||||
import {
|
|
||||||
findIntegration,
|
|
||||||
getIntegrationDescriptions,
|
|
||||||
} from "../../../data/integrations";
|
|
||||||
import { scanUSBDevices } from "../../../data/usb";
|
|
||||||
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
|
||||||
import {
|
|
||||||
showAlertDialog,
|
|
||||||
showConfirmationDialog,
|
|
||||||
} from "../../../dialogs/generic/show-dialog-box";
|
|
||||||
import "../../../layouts/hass-loading-screen";
|
import "../../../layouts/hass-loading-screen";
|
||||||
import "../../../layouts/hass-tabs-subpage";
|
import {
|
||||||
|
HassRouterPage,
|
||||||
|
RouterOptions,
|
||||||
|
} from "../../../layouts/hass-router-page";
|
||||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||||
import { haStyle } from "../../../resources/styles";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import type { HomeAssistant, Route } from "../../../types";
|
|
||||||
import { configSections } from "../ha-panel-config";
|
import "./ha-config-integration-page";
|
||||||
import { isHelperDomain } from "../helpers/const";
|
import "./ha-config-integrations-dashboard";
|
||||||
import "./ha-config-flow-card";
|
|
||||||
import "./ha-ignored-config-entry-card";
|
|
||||||
import "./ha-integration-card";
|
|
||||||
import type { HaIntegrationCard } from "./ha-integration-card";
|
|
||||||
import "./ha-integration-overflow-menu";
|
|
||||||
import { showAddIntegrationDialog } from "./show-add-integration-dialog";
|
|
||||||
|
|
||||||
export interface ConfigEntryUpdatedEvent {
|
export interface ConfigEntryUpdatedEvent {
|
||||||
entry: ConfigEntry;
|
entry: ConfigEntry;
|
||||||
@ -85,6 +30,10 @@ export interface ConfigEntryRemovedEvent {
|
|||||||
entryId: string;
|
entryId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DataEntryFlowProgressExtended extends DataEntryFlowProgress {
|
||||||
|
localized_title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// for fire event
|
// for fire event
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
@ -93,30 +42,12 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataEntryFlowProgressExtended extends DataEntryFlowProgress {
|
|
||||||
localized_title?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConfigEntryExtended extends ConfigEntry {
|
export interface ConfigEntryExtended extends ConfigEntry {
|
||||||
localized_domain_name?: string;
|
localized_domain_name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupByIntegration = (
|
|
||||||
entries: ConfigEntryExtended[]
|
|
||||||
): Map<string, ConfigEntryExtended[]> => {
|
|
||||||
const result = new Map();
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (result.has(entry.domain)) {
|
|
||||||
result.get(entry.domain).push(entry);
|
|
||||||
} else {
|
|
||||||
result.set(entry.domain, [entry]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
@customElement("ha-config-integrations")
|
@customElement("ha-config-integrations")
|
||||||
class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
class HaConfigIntegrations extends SubscribeMixin(HassRouterPage) {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
|
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
|
||||||
@ -125,66 +56,32 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@property() public showAdvanced!: boolean;
|
@property() public showAdvanced!: boolean;
|
||||||
|
|
||||||
@property() public route!: Route;
|
protected routerOptions: RouterOptions = {
|
||||||
|
defaultPage: "dashboard",
|
||||||
|
routes: {
|
||||||
|
dashboard: {
|
||||||
|
tag: "ha-config-integrations-dashboard",
|
||||||
|
cache: true,
|
||||||
|
},
|
||||||
|
integration: {
|
||||||
|
tag: "ha-config-integration-page",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
@state() private _configEntries?: ConfigEntryExtended[];
|
@state() private _configEntries?: ConfigEntryExtended[];
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
private _configEntriesInProgress: DataEntryFlowProgressExtended[] = [];
|
private _configEntriesInProgress?: DataEntryFlowProgressExtended[];
|
||||||
|
|
||||||
@state()
|
private _loadTranslationsPromise?: Promise<unknown>;
|
||||||
private _entityRegistryEntries: EntityRegistryEntry[] = [];
|
|
||||||
|
|
||||||
@state()
|
public hassSubscribe() {
|
||||||
private _manifests: Record<string, IntegrationManifest> = {};
|
|
||||||
|
|
||||||
private _extraFetchedManifests?: Set<string>;
|
|
||||||
|
|
||||||
@state() private _showIgnored = false;
|
|
||||||
|
|
||||||
@state() private _showDisabled = false;
|
|
||||||
|
|
||||||
@state() private _searchParms = new URLSearchParams(
|
|
||||||
window.location.hash.substring(1)
|
|
||||||
);
|
|
||||||
|
|
||||||
@state() private _filter: string = history.state?.filter || "";
|
|
||||||
|
|
||||||
@state() private _diagnosticHandlers?: Record<string, boolean>;
|
|
||||||
|
|
||||||
@state() private _logInfos?: {
|
|
||||||
[integration: string]: IntegrationLogInfo;
|
|
||||||
};
|
|
||||||
|
|
||||||
public hassSubscribe(): Array<UnsubscribeFunc | Promise<UnsubscribeFunc>> {
|
|
||||||
return [
|
return [
|
||||||
subscribeEntityRegistry(this.hass.connection, (entries) => {
|
|
||||||
this._entityRegistryEntries = entries;
|
|
||||||
}),
|
|
||||||
subscribeConfigFlowInProgress(this.hass, async (flowsInProgress) => {
|
|
||||||
const integrations: Set<string> = new Set();
|
|
||||||
const manifests: Set<string> = new Set();
|
|
||||||
flowsInProgress.forEach((flow) => {
|
|
||||||
// To render title placeholders
|
|
||||||
if (flow.context.title_placeholders) {
|
|
||||||
integrations.add(flow.handler);
|
|
||||||
}
|
|
||||||
manifests.add(flow.handler);
|
|
||||||
});
|
|
||||||
await this.hass.loadBackendTranslation(
|
|
||||||
"config",
|
|
||||||
Array.from(integrations)
|
|
||||||
);
|
|
||||||
this._fetchIntegrationManifests(manifests);
|
|
||||||
await nextRender();
|
|
||||||
this._configEntriesInProgress = flowsInProgress.map((flow) => ({
|
|
||||||
...flow,
|
|
||||||
localized_title: localizeConfigFlowTitle(this.hass.localize, flow),
|
|
||||||
}));
|
|
||||||
}),
|
|
||||||
subscribeConfigEntries(
|
subscribeConfigEntries(
|
||||||
this.hass,
|
this.hass,
|
||||||
(messages) => {
|
async (messages) => {
|
||||||
|
await this._loadTranslationsPromise;
|
||||||
let fullUpdate = false;
|
let fullUpdate = false;
|
||||||
const newEntries: ConfigEntryExtended[] = [];
|
const newEntries: ConfigEntryExtended[] = [];
|
||||||
messages.forEach((message) => {
|
messages.forEach((message) => {
|
||||||
@ -219,718 +116,60 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const existingEntries = fullUpdate ? [] : this._configEntries;
|
const existingEntries = fullUpdate ? [] : this._configEntries;
|
||||||
this._configEntries = [...existingEntries!, ...newEntries].sort(
|
this._configEntries = [...existingEntries!, ...newEntries];
|
||||||
(conf1, conf2) =>
|
|
||||||
caseInsensitiveStringCompare(
|
|
||||||
conf1.localized_domain_name + conf1.title,
|
|
||||||
conf2.localized_domain_name + conf2.title,
|
|
||||||
this.hass.locale.language
|
|
||||||
)
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
{ type: ["device", "hub", "service"] }
|
{ type: ["device", "hub", "service"] }
|
||||||
),
|
),
|
||||||
subscribeLogInfo(this.hass.connection, (log_infos) => {
|
subscribeConfigFlowInProgress(this.hass, async (flowsInProgress) => {
|
||||||
const logInfoLookup: { [integration: string]: IntegrationLogInfo } = {};
|
const integrations: Set<string> = new Set();
|
||||||
for (const log_info of log_infos) {
|
flowsInProgress.forEach((flow) => {
|
||||||
logInfoLookup[log_info.domain] = log_info;
|
// To render title placeholders
|
||||||
|
if (flow.context.title_placeholders) {
|
||||||
|
integrations.add(flow.handler);
|
||||||
}
|
}
|
||||||
this._logInfos = logInfoLookup;
|
});
|
||||||
|
await this.hass.loadBackendTranslation(
|
||||||
|
"config",
|
||||||
|
Array.from(integrations)
|
||||||
|
);
|
||||||
|
this._configEntriesInProgress = flowsInProgress.map((flow) => ({
|
||||||
|
...flow,
|
||||||
|
localized_title: localizeConfigFlowTitle(this.hass.localize, flow),
|
||||||
|
}));
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private _filterConfigEntries = memoizeOne(
|
|
||||||
(
|
|
||||||
configEntries: ConfigEntryExtended[],
|
|
||||||
filter?: string
|
|
||||||
): ConfigEntryExtended[] => {
|
|
||||||
if (!filter) {
|
|
||||||
return [...configEntries];
|
|
||||||
}
|
|
||||||
const options: Fuse.IFuseOptions<ConfigEntryExtended> = {
|
|
||||||
keys: ["domain", "localized_domain_name", "title"],
|
|
||||||
isCaseSensitive: false,
|
|
||||||
minMatchCharLength: 2,
|
|
||||||
threshold: 0.2,
|
|
||||||
};
|
|
||||||
const fuse = new Fuse(configEntries, options);
|
|
||||||
return fuse.search(filter).map((result) => result.item);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
private _filterGroupConfigEntries = memoizeOne(
|
|
||||||
(
|
|
||||||
configEntries: ConfigEntryExtended[],
|
|
||||||
filter?: string
|
|
||||||
): [
|
|
||||||
Map<string, ConfigEntryExtended[]>,
|
|
||||||
ConfigEntryExtended[],
|
|
||||||
Map<string, ConfigEntryExtended[]>,
|
|
||||||
// Counter for disabled integrations since the tuple element above will
|
|
||||||
// be grouped by the integration name and therefore not provide a valid count
|
|
||||||
number
|
|
||||||
] => {
|
|
||||||
const filteredConfigEnties = this._filterConfigEntries(
|
|
||||||
configEntries,
|
|
||||||
filter
|
|
||||||
);
|
|
||||||
const ignored: ConfigEntryExtended[] = [];
|
|
||||||
const disabled: ConfigEntryExtended[] = [];
|
|
||||||
for (let i = filteredConfigEnties.length - 1; i >= 0; i--) {
|
|
||||||
if (filteredConfigEnties[i].source === "ignore") {
|
|
||||||
ignored.push(filteredConfigEnties.splice(i, 1)[0]);
|
|
||||||
} else if (filteredConfigEnties[i].disabled_by !== null) {
|
|
||||||
disabled.push(filteredConfigEnties.splice(i, 1)[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
groupByIntegration(filteredConfigEnties),
|
|
||||||
ignored,
|
|
||||||
groupByIntegration(disabled),
|
|
||||||
disabled.length,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
private _filterConfigEntriesInProgress = memoizeOne(
|
|
||||||
(
|
|
||||||
configEntriesInProgress: DataEntryFlowProgressExtended[],
|
|
||||||
filter?: string
|
|
||||||
): DataEntryFlowProgressExtended[] => {
|
|
||||||
if (!filter) {
|
|
||||||
return configEntriesInProgress;
|
|
||||||
}
|
|
||||||
const options: Fuse.IFuseOptions<DataEntryFlowProgressExtended> = {
|
|
||||||
keys: ["handler", "localized_title"],
|
|
||||||
isCaseSensitive: false,
|
|
||||||
minMatchCharLength: 2,
|
|
||||||
threshold: 0.2,
|
|
||||||
};
|
|
||||||
const fuse = new Fuse(configEntriesInProgress, options);
|
|
||||||
return fuse.search(filter).map((result) => result.item);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
protected firstUpdated(changed: PropertyValues) {
|
protected firstUpdated(changed: PropertyValues) {
|
||||||
super.firstUpdated(changed);
|
super.firstUpdated(changed);
|
||||||
const localizePromise = this.hass.loadBackendTranslation(
|
this._loadTranslationsPromise = this.hass.loadBackendTranslation(
|
||||||
"title",
|
"title",
|
||||||
undefined,
|
undefined,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
this._fetchManifests();
|
|
||||||
if (this.route.path === "/add") {
|
|
||||||
this._handleAdd(localizePromise);
|
|
||||||
}
|
|
||||||
this._scanUSBDevices();
|
|
||||||
if (isComponentLoaded(this.hass, "diagnostics")) {
|
|
||||||
fetchDiagnosticHandlers(this.hass).then((infos) => {
|
|
||||||
const handlers = {};
|
|
||||||
for (const info of infos) {
|
|
||||||
handlers[info.domain] = info.handlers.config_entry;
|
|
||||||
}
|
|
||||||
this._diagnosticHandlers = handlers;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changed: PropertyValues) {
|
protected updatePageEl(pageEl) {
|
||||||
super.updated(changed);
|
pageEl.hass = this.hass;
|
||||||
if (
|
|
||||||
this._searchParms.has("config_entry") &&
|
|
||||||
changed.has("_configEntries") &&
|
|
||||||
!changed.get("_configEntries") &&
|
|
||||||
this._configEntries
|
|
||||||
) {
|
|
||||||
this._highlightEntry();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
if (this._currentPage === "integration") {
|
||||||
if (!this._configEntries) {
|
if (this.routeTail.path) {
|
||||||
return html`<hass-loading-screen
|
pageEl.domain = this.routeTail.path.substring(1);
|
||||||
.hass=${this.hass}
|
} else if (window.location.search) {
|
||||||
.narrow=${this.narrow}
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
></hass-loading-screen>`;
|
if (urlParams.has("domain")) {
|
||||||
}
|
const domain = urlParams.get("domain");
|
||||||
const [
|
pageEl.domain = domain;
|
||||||
groupedConfigEntries,
|
navigate(`/config/integrations/integration/${domain}`);
|
||||||
ignoredConfigEntries,
|
|
||||||
disabledConfigEntries,
|
|
||||||
disabledCount,
|
|
||||||
] = this._filterGroupConfigEntries(this._configEntries, this._filter);
|
|
||||||
const configEntriesInProgress = this._filterConfigEntriesInProgress(
|
|
||||||
this._configEntriesInProgress,
|
|
||||||
this._filter
|
|
||||||
);
|
|
||||||
|
|
||||||
const filterMenu = html`
|
|
||||||
<div slot=${ifDefined(this.narrow ? "toolbar-icon" : undefined)}>
|
|
||||||
<div class="menu-badge-container">
|
|
||||||
${!this._showDisabled && this.narrow && disabledCount
|
|
||||||
? html`<span class="badge">${disabledCount}</span>`
|
|
||||||
: ""}
|
|
||||||
<ha-button-menu
|
|
||||||
multi
|
|
||||||
@action=${this._handleMenuAction}
|
|
||||||
@click=${this._preventDefault}
|
|
||||||
>
|
|
||||||
<ha-icon-button
|
|
||||||
slot="trigger"
|
|
||||||
.label=${this.hass.localize("ui.common.menu")}
|
|
||||||
.path=${mdiFilterVariant}
|
|
||||||
>
|
|
||||||
</ha-icon-button>
|
|
||||||
<ha-check-list-item left .selected=${this._showIgnored}>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.ignore.show_ignored"
|
|
||||||
)}
|
|
||||||
</ha-check-list-item>
|
|
||||||
<ha-check-list-item left .selected=${this._showDisabled}>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.disable.show_disabled"
|
|
||||||
)}
|
|
||||||
</ha-check-list-item>
|
|
||||||
</ha-button-menu>
|
|
||||||
</div>
|
|
||||||
${this.narrow
|
|
||||||
? html`
|
|
||||||
<ha-integration-overflow-menu
|
|
||||||
.hass=${this.hass}
|
|
||||||
slot="toolbar-icon"
|
|
||||||
></ha-integration-overflow-menu>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<hass-tabs-subpage
|
|
||||||
.hass=${this.hass}
|
|
||||||
.narrow=${this.narrow}
|
|
||||||
back-path="/config"
|
|
||||||
.route=${this.route}
|
|
||||||
.tabs=${configSections.devices}
|
|
||||||
>
|
|
||||||
${this.narrow
|
|
||||||
? html`
|
|
||||||
<div slot="header">
|
|
||||||
<search-input
|
|
||||||
.hass=${this.hass}
|
|
||||||
.filter=${this._filter}
|
|
||||||
class="header"
|
|
||||||
@value-changed=${this._handleSearchChange}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.search"
|
|
||||||
)}
|
|
||||||
></search-input>
|
|
||||||
</div>
|
|
||||||
${filterMenu}
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<ha-integration-overflow-menu
|
|
||||||
.hass=${this.hass}
|
|
||||||
slot="toolbar-icon"
|
|
||||||
></ha-integration-overflow-menu>
|
|
||||||
<div class="search">
|
|
||||||
<search-input
|
|
||||||
.hass=${this.hass}
|
|
||||||
suffix
|
|
||||||
.filter=${this._filter}
|
|
||||||
@value-changed=${this._handleSearchChange}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.search"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div class="filters" slot="suffix">
|
|
||||||
${!this._showDisabled && disabledCount
|
|
||||||
? html`<div
|
|
||||||
class="active-filters"
|
|
||||||
@click=${this._preventDefault}
|
|
||||||
>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.disable.disabled_integrations",
|
|
||||||
{ number: disabledCount }
|
|
||||||
)}
|
|
||||||
<mwc-button
|
|
||||||
@click=${this._toggleShowDisabled}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.disable.show"
|
|
||||||
)}
|
|
||||||
></mwc-button>
|
|
||||||
</div>`
|
|
||||||
: ""}
|
|
||||||
${filterMenu}
|
|
||||||
</div>
|
|
||||||
</search-input>
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
${this._showIgnored
|
|
||||||
? ignoredConfigEntries.map(
|
|
||||||
(entry: ConfigEntryExtended) => html`
|
|
||||||
<ha-ignored-config-entry-card
|
|
||||||
.hass=${this.hass}
|
|
||||||
.manifest=${this._manifests[entry.domain]}
|
|
||||||
.entry=${entry}
|
|
||||||
@change=${this._handleFlowUpdated}
|
|
||||||
></ha-ignored-config-entry-card>
|
|
||||||
`
|
|
||||||
)
|
|
||||||
: ""}
|
|
||||||
${configEntriesInProgress.length
|
|
||||||
? configEntriesInProgress.map(
|
|
||||||
(flow: DataEntryFlowProgressExtended) => html`
|
|
||||||
<ha-config-flow-card
|
|
||||||
.hass=${this.hass}
|
|
||||||
.manifest=${this._manifests[flow.handler]}
|
|
||||||
.flow=${flow}
|
|
||||||
@change=${this._handleFlowUpdated}
|
|
||||||
></ha-config-flow-card>
|
|
||||||
`
|
|
||||||
)
|
|
||||||
: ""}
|
|
||||||
${this._showDisabled
|
|
||||||
? Array.from(disabledConfigEntries.entries()).map(
|
|
||||||
([domain, items]) =>
|
|
||||||
html`<ha-integration-card
|
|
||||||
data-domain=${domain}
|
|
||||||
entryDisabled
|
|
||||||
.hass=${this.hass}
|
|
||||||
.domain=${domain}
|
|
||||||
.items=${items}
|
|
||||||
.manifest=${this._manifests[domain]}
|
|
||||||
.entityRegistryEntries=${this._entityRegistryEntries}
|
|
||||||
></ha-integration-card> `
|
|
||||||
)
|
|
||||||
: ""}
|
|
||||||
${groupedConfigEntries.size
|
|
||||||
? Array.from(groupedConfigEntries.entries()).map(
|
|
||||||
([domain, items]) =>
|
|
||||||
html`<ha-integration-card
|
|
||||||
data-domain=${domain}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.domain=${domain}
|
|
||||||
.items=${items}
|
|
||||||
.manifest=${this._manifests[domain]}
|
|
||||||
.entityRegistryEntries=${this._entityRegistryEntries}
|
|
||||||
.supportsDiagnostics=${this._diagnosticHandlers
|
|
||||||
? this._diagnosticHandlers[domain]
|
|
||||||
: false}
|
|
||||||
.logInfo=${this._logInfos
|
|
||||||
? this._logInfos[domain]
|
|
||||||
: nothing}
|
|
||||||
></ha-integration-card>`
|
|
||||||
)
|
|
||||||
: this._filter &&
|
|
||||||
!configEntriesInProgress.length &&
|
|
||||||
!groupedConfigEntries.size &&
|
|
||||||
this._configEntries.length
|
|
||||||
? html`
|
|
||||||
<div class="empty-message">
|
|
||||||
<h1>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.none_found"
|
|
||||||
)}
|
|
||||||
</h1>
|
|
||||||
<p>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.none_found_detail"
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<mwc-button
|
|
||||||
@click=${this._createFlow}
|
|
||||||
unelevated
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.add_integration"
|
|
||||||
)}
|
|
||||||
></mwc-button>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: // If we have a filter, never show a card
|
|
||||||
this._filter
|
|
||||||
? ""
|
|
||||||
: // If we're showing 0 cards, show empty state text
|
|
||||||
(!this._showIgnored || ignoredConfigEntries.length === 0) &&
|
|
||||||
(!this._showDisabled || disabledConfigEntries.size === 0) &&
|
|
||||||
groupedConfigEntries.size === 0
|
|
||||||
? html`
|
|
||||||
<div class="empty-message">
|
|
||||||
<h1>
|
|
||||||
${this.hass.localize("ui.panel.config.integrations.none")}
|
|
||||||
</h1>
|
|
||||||
<p>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.no_integrations"
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<mwc-button
|
|
||||||
@click=${this._createFlow}
|
|
||||||
unelevated
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.add_integration"
|
|
||||||
)}
|
|
||||||
></mwc-button>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</div>
|
|
||||||
<ha-fab
|
|
||||||
slot="fab"
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.add_integration"
|
|
||||||
)}
|
|
||||||
extended
|
|
||||||
@click=${this._createFlow}
|
|
||||||
>
|
|
||||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
|
||||||
</ha-fab>
|
|
||||||
</hass-tabs-subpage>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _preventDefault(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _scanUSBDevices() {
|
|
||||||
if (!isComponentLoaded(this.hass, "usb")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await scanUSBDevices(this.hass);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _fetchManifests(integrations?: string[]) {
|
|
||||||
const fetched = await fetchIntegrationManifests(this.hass, integrations);
|
|
||||||
// Make a copy so we can keep track of previously loaded manifests
|
|
||||||
// for discovered flows (which are not part of these results)
|
|
||||||
const manifests = { ...this._manifests };
|
|
||||||
for (const manifest of fetched) {
|
|
||||||
manifests[manifest.domain] = manifest;
|
|
||||||
}
|
|
||||||
this._manifests = manifests;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _fetchIntegrationManifests(integrations: Set<string>) {
|
|
||||||
const manifestsToFetch: string[] = [];
|
|
||||||
for (const integration of integrations) {
|
|
||||||
if (integration in this._manifests) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (this._extraFetchedManifests) {
|
|
||||||
if (this._extraFetchedManifests.has(integration)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this._extraFetchedManifests = new Set();
|
|
||||||
}
|
|
||||||
this._extraFetchedManifests.add(integration);
|
|
||||||
manifestsToFetch.push(integration);
|
|
||||||
}
|
|
||||||
if (manifestsToFetch.length) {
|
|
||||||
await this._fetchManifests(manifestsToFetch);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleFlowUpdated() {
|
|
||||||
getConfigFlowInProgressCollection(this.hass.connection).refresh();
|
|
||||||
this._fetchManifests();
|
|
||||||
}
|
}
|
||||||
|
pageEl.route = this.routeTail;
|
||||||
private _createFlow() {
|
pageEl.configEntries = this._configEntries;
|
||||||
showAddIntegrationDialog(this, {
|
pageEl.configEntriesInProgress = this._configEntriesInProgress;
|
||||||
initialFilter: this._filter,
|
pageEl.narrow = this.narrow;
|
||||||
});
|
pageEl.isWide = this.isWide;
|
||||||
}
|
pageEl.showAdvanced = this.showAdvanced;
|
||||||
|
|
||||||
private _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
|
||||||
switch (ev.detail.index) {
|
|
||||||
case 0:
|
|
||||||
this._showIgnored = !this._showIgnored;
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
this._toggleShowDisabled();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _toggleShowDisabled() {
|
|
||||||
this._showDisabled = !this._showDisabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleSearchChange(ev: CustomEvent) {
|
|
||||||
this._filter = ev.detail.value;
|
|
||||||
history.replaceState({ filter: this._filter }, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _highlightEntry() {
|
|
||||||
await nextRender();
|
|
||||||
const entryId = this._searchParms.get("config_entry")!;
|
|
||||||
const configEntry = this._configEntries!.find(
|
|
||||||
(entry) => entry.entry_id === entryId
|
|
||||||
);
|
|
||||||
if (!configEntry) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const card: HaIntegrationCard = this.shadowRoot!.querySelector(
|
|
||||||
`[data-domain=${configEntry?.domain}]`
|
|
||||||
) as HaIntegrationCard;
|
|
||||||
if (card) {
|
|
||||||
card.scrollIntoView({
|
|
||||||
block: "center",
|
|
||||||
});
|
|
||||||
card.classList.add("highlight");
|
|
||||||
card.selectedConfigEntryId = entryId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _handleAdd(localizePromise: Promise<LocalizeFunc>) {
|
|
||||||
const brand = extractSearchParam("brand");
|
|
||||||
const domain = extractSearchParam("domain");
|
|
||||||
navigate("/config/integrations", { replace: true });
|
|
||||||
|
|
||||||
if (brand) {
|
|
||||||
showAddIntegrationDialog(this, {
|
|
||||||
brand,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!domain) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const descriptions = await getIntegrationDescriptions(this.hass);
|
|
||||||
const integrations = {
|
|
||||||
...descriptions.core.integration,
|
|
||||||
...descriptions.custom.integration,
|
|
||||||
};
|
|
||||||
|
|
||||||
const integration = findIntegration(integrations, domain);
|
|
||||||
|
|
||||||
if (integration?.config_flow) {
|
|
||||||
// Integration exists, so we can just create a flow
|
|
||||||
const localize = await localizePromise;
|
|
||||||
if (
|
|
||||||
await showConfirmationDialog(this, {
|
|
||||||
title: localize("ui.panel.config.integrations.confirm_new", {
|
|
||||||
integration: integration.name || domainToName(localize, domain),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
showAddIntegrationDialog(this, {
|
|
||||||
domain,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (integration?.supported_by) {
|
|
||||||
// Integration is a alias, so we can just create a flow
|
|
||||||
const localize = await localizePromise;
|
|
||||||
const supportedIntegration = findIntegration(
|
|
||||||
integrations,
|
|
||||||
integration.supported_by
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!supportedIntegration) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showConfirmationDialog(this, {
|
|
||||||
text: this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.config_flow.supported_brand_flow",
|
|
||||||
{
|
|
||||||
supported_brand: integration.name || domainToName(localize, domain),
|
|
||||||
flow_domain_name:
|
|
||||||
supportedIntegration.name ||
|
|
||||||
domainToName(localize, integration.supported_by),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
confirm: async () => {
|
|
||||||
if (
|
|
||||||
(PROTOCOL_INTEGRATIONS as ReadonlyArray<string>).includes(
|
|
||||||
integration.supported_by!
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
protocolIntegrationPicked(
|
|
||||||
this,
|
|
||||||
this.hass,
|
|
||||||
integration.supported_by!
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showConfigFlowDialog(this, {
|
|
||||||
dialogClosedCallback: () => {
|
|
||||||
this._handleFlowUpdated();
|
|
||||||
},
|
|
||||||
startFlowHandler: integration.supported_by,
|
|
||||||
manifest: await fetchIntegrationManifest(
|
|
||||||
this.hass,
|
|
||||||
integration.supported_by!
|
|
||||||
),
|
|
||||||
showAdvanced: this.hass.userData?.showAdvanced,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not an integration or supported brand, try helper else show alert
|
|
||||||
if (isHelperDomain(domain)) {
|
|
||||||
navigate(`/config/helpers/add?domain=${domain}`, {
|
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const helpers = {
|
|
||||||
...descriptions.core.helper,
|
|
||||||
...descriptions.custom.helper,
|
|
||||||
};
|
|
||||||
const helper = findIntegration(helpers, domain);
|
|
||||||
if (helper) {
|
|
||||||
navigate(`/config/helpers/add?domain=${domain}`, {
|
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showAlertDialog(this, {
|
|
||||||
title: this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.config_flow.error"
|
|
||||||
),
|
|
||||||
text: this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.config_flow.no_config_flow"
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return [
|
|
||||||
haStyle,
|
|
||||||
css`
|
|
||||||
:host([narrow]) hass-tabs-subpage {
|
|
||||||
--main-title-margin: 0;
|
|
||||||
}
|
|
||||||
ha-button-menu {
|
|
||||||
margin-left: 8px;
|
|
||||||
margin-inline-start: 8px;
|
|
||||||
margin-inline-end: initial;
|
|
||||||
direction: var(--direction);
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
||||||
grid-gap: 16px 16px;
|
|
||||||
padding: 8px 16px 16px;
|
|
||||||
margin-bottom: 64px;
|
|
||||||
}
|
|
||||||
.container > * {
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
.empty-message {
|
|
||||||
margin: auto;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.empty-message h1 {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
search-input {
|
|
||||||
--mdc-text-field-fill-color: var(--sidebar-background-color);
|
|
||||||
--mdc-text-field-idle-line-color: var(--divider-color);
|
|
||||||
--text-field-overflow: visible;
|
|
||||||
}
|
|
||||||
search-input.header {
|
|
||||||
display: block;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
margin-left: 8px;
|
|
||||||
margin-inline-start: 8px;
|
|
||||||
margin-inline-end: initial;
|
|
||||||
direction: var(--direction);
|
|
||||||
--mdc-ripple-color: transparant;
|
|
||||||
}
|
|
||||||
.search {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
width: 100%;
|
|
||||||
align-items: center;
|
|
||||||
height: 56px;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
.search search-input {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
.filters {
|
|
||||||
--mdc-text-field-fill-color: var(--input-fill-color);
|
|
||||||
--mdc-text-field-idle-line-color: var(--input-idle-line-color);
|
|
||||||
--mdc-shape-small: 4px;
|
|
||||||
--text-field-overflow: initial;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
}
|
|
||||||
.active-filters {
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding-top: 2px;
|
|
||||||
padding-bottom: 2px;
|
|
||||||
padding-right: 2px;
|
|
||||||
padding-left: 8px;
|
|
||||||
padding-inline-start: 8px;
|
|
||||||
padding-inline-end: 2px;
|
|
||||||
font-size: 14px;
|
|
||||||
width: max-content;
|
|
||||||
cursor: initial;
|
|
||||||
direction: var(--direction);
|
|
||||||
}
|
|
||||||
.active-filters mwc-button {
|
|
||||||
margin-left: 8px;
|
|
||||||
margin-inline-start: 8px;
|
|
||||||
margin-inline-end: initial;
|
|
||||||
direction: var(--direction);
|
|
||||||
}
|
|
||||||
.active-filters::before {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
opacity: 0.12;
|
|
||||||
border-radius: 4px;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
content: "";
|
|
||||||
}
|
|
||||||
.badge {
|
|
||||||
min-width: 20px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-radius: 50%;
|
|
||||||
font-weight: 400;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
line-height: 20px;
|
|
||||||
text-align: center;
|
|
||||||
padding: 0px 4px;
|
|
||||||
color: var(--text-primary-color);
|
|
||||||
position: absolute;
|
|
||||||
right: 0px;
|
|
||||||
top: 4px;
|
|
||||||
font-size: 0.65em;
|
|
||||||
}
|
|
||||||
.menu-badge-container {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
ha-button-menu {
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,99 @@
|
|||||||
|
import { mdiCog } from "@mdi/js";
|
||||||
|
import { css, html, LitElement, TemplateResult } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import "../../../components/ha-button";
|
||||||
|
import {
|
||||||
|
DisableConfigEntryResult,
|
||||||
|
enableConfigEntry,
|
||||||
|
} from "../../../data/config_entries";
|
||||||
|
import type { IntegrationManifest } from "../../../data/integration";
|
||||||
|
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||||
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import type { ConfigEntryExtended } from "./ha-config-integrations";
|
||||||
|
import "./ha-integration-action-card";
|
||||||
|
|
||||||
|
@customElement("ha-disabled-config-entry-card")
|
||||||
|
export class HaDisabledConfigEntryCard extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public entry!: ConfigEntryExtended;
|
||||||
|
|
||||||
|
@property() public manifest?: IntegrationManifest;
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<ha-integration-action-card
|
||||||
|
.hass=${this.hass}
|
||||||
|
.manifest=${this.manifest}
|
||||||
|
.banner=${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.disable.disabled_cause",
|
||||||
|
{
|
||||||
|
cause:
|
||||||
|
this.hass.localize(
|
||||||
|
`ui.panel.config.integrations.config_entry.disable.disabled_by.${this
|
||||||
|
.entry.disabled_by!}`
|
||||||
|
) || this.entry.disabled_by,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
.domain=${this.entry.domain}
|
||||||
|
.localizedDomainName=${this.entry.localized_domain_name}
|
||||||
|
.label=${this.entry.title || this.entry.localized_domain_name}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href=${`/config/integrations/integration/${this.entry.domain}`}
|
||||||
|
slot="header-button"
|
||||||
|
>
|
||||||
|
<ha-icon-button .path=${mdiCog}></ha-icon-button>
|
||||||
|
</a>
|
||||||
|
<ha-button
|
||||||
|
@click=${this._handleEnable}
|
||||||
|
.label=${this.hass.localize("ui.common.enable")}
|
||||||
|
></ha-button>
|
||||||
|
</ha-integration-action-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _handleEnable() {
|
||||||
|
const entryId = this.entry.entry_id;
|
||||||
|
|
||||||
|
let result: DisableConfigEntryResult;
|
||||||
|
try {
|
||||||
|
result = await enableConfigEntry(this.hass, entryId);
|
||||||
|
} catch (err: any) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.disable_error"
|
||||||
|
),
|
||||||
|
text: err.message,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.require_restart) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
text: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.enable_restart_confirm"
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
--state-color: var(--divider-color, #e0e0e0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-button {
|
||||||
|
--mdc-theme-primary: var(--primary-color);
|
||||||
|
}
|
||||||
|
a ha-icon-button {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-disabled-config-entry-card": HaDisabledConfigEntryCard;
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ import { customElement, property } from "lit/decorators";
|
|||||||
import type { IntegrationManifest } from "../../../data/integration";
|
import type { IntegrationManifest } from "../../../data/integration";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import "./ha-integration-header";
|
import "./ha-integration-header";
|
||||||
|
import "../../../components/ha-card";
|
||||||
|
|
||||||
@customElement("ha-integration-action-card")
|
@customElement("ha-integration-action-card")
|
||||||
export class HaIntegrationActionCard extends LitElement {
|
export class HaIntegrationActionCard extends LitElement {
|
||||||
@ -28,7 +29,9 @@ export class HaIntegrationActionCard extends LitElement {
|
|||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.localizedDomainName=${this.localizedDomainName}
|
.localizedDomainName=${this.localizedDomainName}
|
||||||
.manifest=${this.manifest}
|
.manifest=${this.manifest}
|
||||||
></ha-integration-header>
|
>
|
||||||
|
<span slot="header-button"><slot name="header-button"></slot></span>
|
||||||
|
</ha-integration-header>
|
||||||
<div class="filler"></div>
|
<div class="filler"></div>
|
||||||
<div class="actions"><slot></slot></div>
|
<div class="actions"><slot></slot></div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,9 @@
|
|||||||
import { mdiBugPlay, mdiCloud, mdiPackageVariant, mdiSyncOff } from "@mdi/js";
|
|
||||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||||
|
import { mdiCloud, mdiPackageVariant } from "@mdi/js";
|
||||||
import { css, html, LitElement, TemplateResult } from "lit";
|
import { css, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import { ConfigEntry } from "../../../data/config_entries";
|
|
||||||
import { domainToName, IntegrationManifest } from "../../../data/integration";
|
import { domainToName, IntegrationManifest } from "../../../data/integration";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { brandsUrl } from "../../../util/brands-url";
|
import { brandsUrl } from "../../../util/brands-url";
|
||||||
@ -14,16 +14,14 @@ export class HaIntegrationHeader extends LitElement {
|
|||||||
|
|
||||||
@property() public banner?: string;
|
@property() public banner?: string;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
@property() public localizedDomainName?: string;
|
@property() public localizedDomainName?: string;
|
||||||
|
|
||||||
@property() public domain!: string;
|
@property() public domain!: string;
|
||||||
|
|
||||||
@property() public label?: string;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public manifest?: IntegrationManifest;
|
@property({ attribute: false }) public manifest?: IntegrationManifest;
|
||||||
|
|
||||||
@property({ attribute: false }) public configEntry?: ConfigEntry;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public debugLoggingEnabled?: boolean;
|
@property({ attribute: false }) public debugLoggingEnabled?: boolean;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
@ -56,10 +54,7 @@ export class HaIntegrationHeader extends LitElement {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (this.manifest.iot_class?.startsWith("cloud_")) {
|
||||||
this.manifest.iot_class &&
|
|
||||||
this.manifest.iot_class.startsWith("cloud_")
|
|
||||||
) {
|
|
||||||
icons.push([
|
icons.push([
|
||||||
mdiCloud,
|
mdiCloud,
|
||||||
this.hass.localize(
|
this.hass.localize(
|
||||||
@ -67,24 +62,6 @@ export class HaIntegrationHeader extends LitElement {
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.configEntry?.pref_disable_polling) {
|
|
||||||
icons.push([
|
|
||||||
mdiSyncOff,
|
|
||||||
this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.config_entry.disabled_polling"
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.debugLoggingEnabled) {
|
|
||||||
icons.push([
|
|
||||||
mdiBugPlay,
|
|
||||||
this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.config_entry.debug_logging_enabled"
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@ -102,15 +79,17 @@ export class HaIntegrationHeader extends LitElement {
|
|||||||
@error=${this._onImageError}
|
@error=${this._onImageError}
|
||||||
@load=${this._onImageLoad}
|
@load=${this._onImageLoad}
|
||||||
/>
|
/>
|
||||||
<div class="info">
|
|
||||||
<div class="primary" role="heading">${primary}</div>
|
|
||||||
${secondary ? html`<div class="secondary">${secondary}</div>` : ""}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${icons.length === 0
|
${icons.length === 0
|
||||||
? ""
|
? ""
|
||||||
: html`
|
: html`
|
||||||
<div class="icons">
|
<div
|
||||||
|
class="icons ${classMap({
|
||||||
|
double: icons.length > 1,
|
||||||
|
cloud: Boolean(
|
||||||
|
this.manifest?.iot_class?.startsWith("cloud_")
|
||||||
|
),
|
||||||
|
})}"
|
||||||
|
>
|
||||||
${icons.map(
|
${icons.map(
|
||||||
([icon, description]) => html`
|
([icon, description]) => html`
|
||||||
<span>
|
<span>
|
||||||
@ -123,6 +102,13 @@ export class HaIntegrationHeader extends LitElement {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
|
<div class="info">
|
||||||
|
<div class="primary" role="heading">${primary}</div>
|
||||||
|
<div class="secondary">${secondary}</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-button">
|
||||||
|
<slot name="header-button"></slot>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -175,10 +161,12 @@ export class HaIntegrationHeader extends LitElement {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
.header-button {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
.primary {
|
.primary {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
margin-right: 2px;
|
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
@ -188,21 +176,30 @@ export class HaIntegrationHeader extends LitElement {
|
|||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
.icons {
|
.icons {
|
||||||
margin-right: 8px;
|
background: var(--warning-color);
|
||||||
margin-left: auto;
|
border: 1px solid var(--card-background-color);
|
||||||
height: 28px;
|
border-radius: 14px;
|
||||||
color: var(--text-on-state-color, var(--secondary-text-color));
|
color: var(--text-primary-color);
|
||||||
background-color: var(--state-color, #e0e0e0);
|
position: absolute;
|
||||||
border-bottom-left-radius: 4px;
|
left: 40px;
|
||||||
border-bottom-right-radius: 4px;
|
top: 40px;
|
||||||
display: flex;
|
display: flex;
|
||||||
float: right;
|
}
|
||||||
|
.icons.cloud {
|
||||||
|
background: var(--info-color);
|
||||||
|
}
|
||||||
|
.icons.double {
|
||||||
|
background: var(--warning-color);
|
||||||
|
left: 28px;
|
||||||
}
|
}
|
||||||
.icons ha-svg-icon {
|
.icons ha-svg-icon {
|
||||||
width: 20px;
|
width: 16px;
|
||||||
height: 20px;
|
height: 16px;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
}
|
}
|
||||||
|
.icons span:not(:first-child) ha-svg-icon {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
simple-tooltip {
|
simple-tooltip {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
@ -70,6 +70,12 @@ export const getMyRedirects = (hasSupervisor: boolean): Redirects => ({
|
|||||||
integrations: {
|
integrations: {
|
||||||
redirect: "/config/integrations",
|
redirect: "/config/integrations",
|
||||||
},
|
},
|
||||||
|
integration: {
|
||||||
|
redirect: "/config/integration",
|
||||||
|
params: {
|
||||||
|
domain: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
config_mqtt: {
|
config_mqtt: {
|
||||||
component: "mqtt",
|
component: "mqtt",
|
||||||
redirect: "/config/mqtt",
|
redirect: "/config/mqtt",
|
||||||
|
@ -3238,6 +3238,11 @@
|
|||||||
"confirm_delete_ignore": "This will make the integration appear in your discovered integrations again when it gets discovered. This might require a restart or take some time.",
|
"confirm_delete_ignore": "This will make the integration appear in your discovered integrations again when it gets discovered. This might require a restart or take some time.",
|
||||||
"stop_ignore": "Stop ignoring"
|
"stop_ignore": "Stop ignoring"
|
||||||
},
|
},
|
||||||
|
"integration_page": {
|
||||||
|
"entries": "Integration entries",
|
||||||
|
"no_entries": "No entries",
|
||||||
|
"attention_entries": "Needs attention"
|
||||||
|
},
|
||||||
"config_entry": {
|
"config_entry": {
|
||||||
"application_credentials": {
|
"application_credentials": {
|
||||||
"delete_title": "Application Credentials",
|
"delete_title": "Application Credentials",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user