mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-15 21:36:36 +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,
|
||||
descriptor: {
|
||||
set(this: LitElement, value: number) {
|
||||
history.replaceState({ scrollPosition: value }, "");
|
||||
this[`__${String(element.key)}`] = value;
|
||||
},
|
||||
get(this: LitElement) {
|
||||
return this[`__${String(element.key)}`];
|
||||
return (
|
||||
this[`__${String(element.key)}`] || history.state?.scrollPosition
|
||||
);
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
@ -21,12 +24,17 @@ export const restoreScroll =
|
||||
const connectedCallback = cls.prototype.connectedCallback;
|
||||
cls.prototype.connectedCallback = function () {
|
||||
connectedCallback.call(this);
|
||||
if (this[element.key]) {
|
||||
const target = this.renderRoot.querySelector(selector);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
target.scrollTop = this[element.key];
|
||||
const scrollPos = this[element.key];
|
||||
if (scrollPos) {
|
||||
this.updateComplete.then(() => {
|
||||
const target = this.renderRoot.querySelector(selector);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
target.scrollTop = scrollPos;
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
|
@ -5,6 +5,13 @@ import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-list-item")
|
||||
export class HaListItem extends ListItemBase {
|
||||
protected renderRipple() {
|
||||
if (this.noninteractive) {
|
||||
return "";
|
||||
}
|
||||
return super.renderRipple();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
styles,
|
||||
@ -32,6 +39,7 @@ export class HaListItem extends ListItemBase {
|
||||
}
|
||||
.mdc-deprecated-list-item__meta {
|
||||
display: var(--mdc-list-item-meta-display);
|
||||
align-items: center;
|
||||
}
|
||||
:host([multiline-secondary]) {
|
||||
height: auto;
|
||||
@ -60,6 +68,9 @@ export class HaListItem extends ListItemBase {
|
||||
:host([disabled]) {
|
||||
color: var(--disabled-text-color);
|
||||
}
|
||||
:host([noninteractive]) {
|
||||
pointer-events: unset;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -96,7 +96,7 @@ export class HaRelatedItems extends LitElement {
|
||||
}
|
||||
return html`
|
||||
<a
|
||||
href=${`/config/integrations#config_entry=${relatedConfigEntryId}`}
|
||||
href=${`/config/integrations/integration/${entry.domain}#config_entry=${relatedConfigEntryId}`}
|
||||
@click=${this._navigateAwayClose}
|
||||
>
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
|
@ -3,6 +3,14 @@ import { LocalizeFunc } from "../common/translations/localize";
|
||||
import { HomeAssistant } from "../types";
|
||||
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 =
|
||||
| "device"
|
||||
| "helper"
|
||||
|
@ -9,7 +9,14 @@ import {
|
||||
mdiPlusCircle,
|
||||
} from "@mdi/js";
|
||||
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 { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
@ -261,6 +268,9 @@ export class HaConfigDevicePage extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.devices || !this.deviceId) {
|
||||
return nothing;
|
||||
}
|
||||
const device = this._device(this.deviceId, this.devices);
|
||||
|
||||
if (!device) {
|
||||
@ -290,7 +300,29 @@ export class HaConfigDevicePage extends LitElement {
|
||||
: undefined;
|
||||
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 || [])];
|
||||
if (Array.isArray(this._diagnosticDownloadLinks)) {
|
||||
|
@ -128,6 +128,13 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "domain": {
|
||||
filterTexts.push(
|
||||
`${this.hass.localize(
|
||||
"ui.panel.config.integrations.integration"
|
||||
)} "${domainToName(localize, value)}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
return filterTexts.length ? filterTexts : undefined;
|
||||
@ -187,6 +194,15 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
startLength = outputDevices.length;
|
||||
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) {
|
||||
@ -383,8 +399,11 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
|
||||
public willUpdate(changedProps) {
|
||||
if (changedProps.has("_searchParms")) {
|
||||
if (this._searchParms.get("config_entry")) {
|
||||
// If we are requested to show the devices for a given config entry,
|
||||
if (
|
||||
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.
|
||||
this._showDisabled = true;
|
||||
}
|
||||
@ -548,7 +567,9 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
showMatterAddDeviceDialog(this);
|
||||
return;
|
||||
}
|
||||
showAddIntegrationDialog(this);
|
||||
showAddIntegrationDialog(this, {
|
||||
domain: this._searchParms.get("domain") || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private _showZJSAddDeviceDialog(filteredConfigEntry: ConfigEntry) {
|
||||
|
@ -230,7 +230,7 @@ export class EnergyGridSettings extends LitElement {
|
||||
/>
|
||||
<span class="content">${this._co2ConfigEntry.title}</span>
|
||||
<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>
|
||||
</a>
|
||||
|
@ -159,6 +159,14 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "domain": {
|
||||
this._showDisabled = true;
|
||||
filterTexts.push(
|
||||
`${this.hass.localize(
|
||||
"ui.panel.config.integrations.integration"
|
||||
)} "${domainToName(localize, value)}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
return filterTexts.length ? filterTexts : undefined;
|
||||
@ -368,6 +376,22 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
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) {
|
||||
|
@ -60,7 +60,7 @@ export class HaConfigFlowCard extends LitElement {
|
||||
}`
|
||||
)}
|
||||
></mwc-button>
|
||||
<ha-button-menu>
|
||||
<ha-button-menu slot="header-button">
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
@ -186,6 +186,9 @@ export class HaConfigFlowCard extends LitElement {
|
||||
text-decoration: none;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-button-menu {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-svg-icon[slot="meta"] {
|
||||
width: 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 { 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 { 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 { 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 {
|
||||
ConfigEntry,
|
||||
subscribeConfigEntries,
|
||||
} from "../../../data/config_entries";
|
||||
import {
|
||||
getConfigFlowInProgressCollection,
|
||||
localizeConfigFlowTitle,
|
||||
subscribeConfigFlowInProgress,
|
||||
} from "../../../data/config_flow";
|
||||
import type { DataEntryFlowProgress } from "../../../data/data_entry_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 { DataEntryFlowProgress } from "../../../data/data_entry_flow";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
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 { 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 "./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 type { HomeAssistant } from "../../../types";
|
||||
|
||||
import "./ha-config-integration-page";
|
||||
import "./ha-config-integrations-dashboard";
|
||||
|
||||
export interface ConfigEntryUpdatedEvent {
|
||||
entry: ConfigEntry;
|
||||
@ -85,6 +30,10 @@ export interface ConfigEntryRemovedEvent {
|
||||
entryId: string;
|
||||
}
|
||||
|
||||
export interface DataEntryFlowProgressExtended extends DataEntryFlowProgress {
|
||||
localized_title?: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
@ -93,30 +42,12 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataEntryFlowProgressExtended extends DataEntryFlowProgress {
|
||||
localized_title?: string;
|
||||
}
|
||||
|
||||
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")
|
||||
class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
class HaConfigIntegrations extends SubscribeMixin(HassRouterPage) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
|
||||
@ -125,66 +56,32 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
|
||||
@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[];
|
||||
|
||||
@property()
|
||||
private _configEntriesInProgress: DataEntryFlowProgressExtended[] = [];
|
||||
private _configEntriesInProgress?: DataEntryFlowProgressExtended[];
|
||||
|
||||
@state()
|
||||
private _entityRegistryEntries: EntityRegistryEntry[] = [];
|
||||
private _loadTranslationsPromise?: Promise<unknown>;
|
||||
|
||||
@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>> {
|
||||
public hassSubscribe() {
|
||||
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(
|
||||
this.hass,
|
||||
(messages) => {
|
||||
async (messages) => {
|
||||
await this._loadTranslationsPromise;
|
||||
let fullUpdate = false;
|
||||
const newEntries: ConfigEntryExtended[] = [];
|
||||
messages.forEach((message) => {
|
||||
@ -219,718 +116,60 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
return;
|
||||
}
|
||||
const existingEntries = fullUpdate ? [] : this._configEntries;
|
||||
this._configEntries = [...existingEntries!, ...newEntries].sort(
|
||||
(conf1, conf2) =>
|
||||
caseInsensitiveStringCompare(
|
||||
conf1.localized_domain_name + conf1.title,
|
||||
conf2.localized_domain_name + conf2.title,
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
this._configEntries = [...existingEntries!, ...newEntries];
|
||||
},
|
||||
{ type: ["device", "hub", "service"] }
|
||||
),
|
||||
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;
|
||||
subscribeConfigFlowInProgress(this.hass, async (flowsInProgress) => {
|
||||
const integrations: Set<string> = new Set();
|
||||
flowsInProgress.forEach((flow) => {
|
||||
// To render title placeholders
|
||||
if (flow.context.title_placeholders) {
|
||||
integrations.add(flow.handler);
|
||||
}
|
||||
});
|
||||
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) {
|
||||
super.firstUpdated(changed);
|
||||
const localizePromise = this.hass.loadBackendTranslation(
|
||||
this._loadTranslationsPromise = this.hass.loadBackendTranslation(
|
||||
"title",
|
||||
undefined,
|
||||
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;
|
||||
}
|
||||
|
||||
protected updatePageEl(pageEl) {
|
||||
pageEl.hass = this.hass;
|
||||
|
||||
if (this._currentPage === "integration") {
|
||||
if (this.routeTail.path) {
|
||||
pageEl.domain = this.routeTail.path.substring(1);
|
||||
} else if (window.location.search) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.has("domain")) {
|
||||
const domain = urlParams.get("domain");
|
||||
pageEl.domain = domain;
|
||||
navigate(`/config/integrations/integration/${domain}`);
|
||||
}
|
||||
this._diagnosticHandlers = handlers;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changed: PropertyValues) {
|
||||
super.updated(changed);
|
||||
if (
|
||||
this._searchParms.has("config_entry") &&
|
||||
changed.has("_configEntries") &&
|
||||
!changed.get("_configEntries") &&
|
||||
this._configEntries
|
||||
) {
|
||||
this._highlightEntry();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._configEntries) {
|
||||
return html`<hass-loading-screen
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></hass-loading-screen>`;
|
||||
}
|
||||
const [
|
||||
groupedConfigEntries,
|
||||
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();
|
||||
}
|
||||
|
||||
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")!;
|
||||
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);
|
||||
}
|
||||
`,
|
||||
];
|
||||
pageEl.route = this.routeTail;
|
||||
pageEl.configEntries = this._configEntries;
|
||||
pageEl.configEntriesInProgress = this._configEntriesInProgress;
|
||||
pageEl.narrow = this.narrow;
|
||||
pageEl.isWide = this.isWide;
|
||||
pageEl.showAdvanced = this.showAdvanced;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 { HomeAssistant } from "../../../types";
|
||||
import "./ha-integration-header";
|
||||
import "../../../components/ha-card";
|
||||
|
||||
@customElement("ha-integration-action-card")
|
||||
export class HaIntegrationActionCard extends LitElement {
|
||||
@ -28,7 +29,9 @@ export class HaIntegrationActionCard extends LitElement {
|
||||
.label=${this.label}
|
||||
.localizedDomainName=${this.localizedDomainName}
|
||||
.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="actions"><slot></slot></div>
|
||||
</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 { mdiCloud, mdiPackageVariant } from "@mdi/js";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { ConfigEntry } from "../../../data/config_entries";
|
||||
import { domainToName, IntegrationManifest } from "../../../data/integration";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
@ -14,16 +14,14 @@ export class HaIntegrationHeader extends LitElement {
|
||||
|
||||
@property() public banner?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public localizedDomainName?: string;
|
||||
|
||||
@property() public domain!: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public manifest?: IntegrationManifest;
|
||||
|
||||
@property({ attribute: false }) public configEntry?: ConfigEntry;
|
||||
|
||||
@property({ attribute: false }) public debugLoggingEnabled?: boolean;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@ -56,10 +54,7 @@ export class HaIntegrationHeader extends LitElement {
|
||||
]);
|
||||
}
|
||||
|
||||
if (
|
||||
this.manifest.iot_class &&
|
||||
this.manifest.iot_class.startsWith("cloud_")
|
||||
) {
|
||||
if (this.manifest.iot_class?.startsWith("cloud_")) {
|
||||
icons.push([
|
||||
mdiCloud,
|
||||
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`
|
||||
@ -102,15 +79,17 @@ export class HaIntegrationHeader extends LitElement {
|
||||
@error=${this._onImageError}
|
||||
@load=${this._onImageLoad}
|
||||
/>
|
||||
<div class="info">
|
||||
<div class="primary" role="heading">${primary}</div>
|
||||
${secondary ? html`<div class="secondary">${secondary}</div>` : ""}
|
||||
</div>
|
||||
|
||||
${icons.length === 0
|
||||
? ""
|
||||
: html`
|
||||
<div class="icons">
|
||||
<div
|
||||
class="icons ${classMap({
|
||||
double: icons.length > 1,
|
||||
cloud: Boolean(
|
||||
this.manifest?.iot_class?.startsWith("cloud_")
|
||||
),
|
||||
})}"
|
||||
>
|
||||
${icons.map(
|
||||
([icon, description]) => html`
|
||||
<span>
|
||||
@ -123,6 +102,13 @@ export class HaIntegrationHeader extends LitElement {
|
||||
)}
|
||||
</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>
|
||||
`;
|
||||
}
|
||||
@ -175,10 +161,12 @@ export class HaIntegrationHeader extends LitElement {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.header-button {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.primary {
|
||||
font-size: 16px;
|
||||
margin-top: 16px;
|
||||
margin-right: 2px;
|
||||
font-weight: 400;
|
||||
word-break: break-word;
|
||||
color: var(--primary-text-color);
|
||||
@ -188,21 +176,30 @@ export class HaIntegrationHeader extends LitElement {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.icons {
|
||||
margin-right: 8px;
|
||||
margin-left: auto;
|
||||
height: 28px;
|
||||
color: var(--text-on-state-color, var(--secondary-text-color));
|
||||
background-color: var(--state-color, #e0e0e0);
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
background: var(--warning-color);
|
||||
border: 1px solid var(--card-background-color);
|
||||
border-radius: 14px;
|
||||
color: var(--text-primary-color);
|
||||
position: absolute;
|
||||
left: 40px;
|
||||
top: 40px;
|
||||
display: flex;
|
||||
float: right;
|
||||
}
|
||||
.icons.cloud {
|
||||
background: var(--info-color);
|
||||
}
|
||||
.icons.double {
|
||||
background: var(--warning-color);
|
||||
left: 28px;
|
||||
}
|
||||
.icons ha-svg-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 4px;
|
||||
}
|
||||
.icons span:not(:first-child) ha-svg-icon {
|
||||
margin-left: 0;
|
||||
}
|
||||
simple-tooltip {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
@ -70,6 +70,12 @@ export const getMyRedirects = (hasSupervisor: boolean): Redirects => ({
|
||||
integrations: {
|
||||
redirect: "/config/integrations",
|
||||
},
|
||||
integration: {
|
||||
redirect: "/config/integration",
|
||||
params: {
|
||||
domain: "string",
|
||||
},
|
||||
},
|
||||
config_mqtt: {
|
||||
component: "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.",
|
||||
"stop_ignore": "Stop ignoring"
|
||||
},
|
||||
"integration_page": {
|
||||
"entries": "Integration entries",
|
||||
"no_entries": "No entries",
|
||||
"attention_entries": "Needs attention"
|
||||
},
|
||||
"config_entry": {
|
||||
"application_credentials": {
|
||||
"delete_title": "Application Credentials",
|
||||
|
Loading…
x
Reference in New Issue
Block a user