mirror of
https://github.com/home-assistant/frontend.git
synced 2026-11-01 18:45:10 +00:00
Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e82170a11a | ||
|
|
178831c7c2 | ||
|
|
215e6a2514 | ||
|
|
d3dc40398d | ||
|
|
0c6bc28ed1 | ||
|
|
e7a74930cc | ||
|
|
3bd106e4cc | ||
|
|
48960e377b | ||
|
|
83893d942b | ||
|
|
ee89c980d0 | ||
|
|
216044b5f0 | ||
|
|
2051456f19 | ||
|
|
9a9f505ddd | ||
|
|
412912d01c |
No files matched your search
@@ -44,14 +44,21 @@ export const lorawanFixtures: ConnectivityFixtures = {
|
||||
],
|
||||
commands: ["lorawan/"],
|
||||
manifests: [
|
||||
manifest("lorawan", "LoRaWAN", { integration_type: "helper" }),
|
||||
manifest("lorawan", "LoRaWAN", {
|
||||
config_flow: false,
|
||||
integration_type: "helper",
|
||||
}),
|
||||
manifest("sensecap_lorawan", "SenseCAP", {
|
||||
integration_type: "device",
|
||||
dependencies: ["lorawan"],
|
||||
}),
|
||||
manifest("chirpstack", "ChirpStack", { integration_type: "hub" }),
|
||||
manifest("chirpstack", "ChirpStack", {
|
||||
integration_type: "hub",
|
||||
dependencies: ["lorawan"],
|
||||
}),
|
||||
manifest("thethingsnetwork_lorawan", "The Things Network", {
|
||||
integration_type: "hub",
|
||||
dependencies: ["lorawan"],
|
||||
}),
|
||||
],
|
||||
configEntries: [
|
||||
|
||||
@@ -13,16 +13,23 @@ export const modbusFixtures: ConnectivityFixtures = {
|
||||
components: ["modbus"],
|
||||
commands: ["modbus/"],
|
||||
manifests: [
|
||||
manifest("modbus", "Modbus", {
|
||||
config_flow: false,
|
||||
iot_class: "local_polling",
|
||||
}),
|
||||
manifest("flexit", "Flexit", {
|
||||
integration_type: "device",
|
||||
dependencies: ["modbus"],
|
||||
iot_class: "local_polling",
|
||||
}),
|
||||
manifest("solaredge_modbus", "SolarEdge Modbus", {
|
||||
integration_type: "device",
|
||||
dependencies: ["modbus"],
|
||||
iot_class: "local_polling",
|
||||
}),
|
||||
manifest("fronius", "Fronius", {
|
||||
integration_type: "device",
|
||||
dependencies: ["modbus"],
|
||||
iot_class: "local_polling",
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -19,6 +19,7 @@ import { until } from "lit/directives/until";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { QueryParamsController } from "../../../common/controllers/query-params-controller";
|
||||
import { ContextSubscriptionController } from "../../../common/decorators/consume";
|
||||
import type { HASSDomTargetEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
|
||||
import {
|
||||
@@ -37,7 +38,9 @@ import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/input/ha-input-search";
|
||||
import type { HaInputSearch } from "../../../components/input/ha-input-search";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/item/ha-list-item-base";
|
||||
import "../../../components/item/ha-list-item-button";
|
||||
import "../../../components/list/ha-list-base";
|
||||
import { getSignedPath } from "../../../data/auth";
|
||||
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
|
||||
@@ -57,7 +60,9 @@ import {
|
||||
getCoreLogFileDownloadUnavailableReason,
|
||||
getErrorLogDownloadUrl,
|
||||
} from "../../../data/error_log";
|
||||
import { manifestsContext } from "../../../data/context";
|
||||
import type {
|
||||
DomainManifestLookup,
|
||||
IntegrationLogInfo,
|
||||
IntegrationManifest,
|
||||
} from "../../../data/integration";
|
||||
@@ -160,6 +165,11 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _manifest?: IntegrationManifest;
|
||||
|
||||
/** The manifests of all loaded integrations, to find the dependents. */
|
||||
@state() private _loadedManifests?: DomainManifestLookup;
|
||||
|
||||
private _manifestsConsumer?: ContextSubscriptionController<DomainManifestLookup>;
|
||||
|
||||
@state() private _extraConfigEntries?: ConfigEntry[];
|
||||
|
||||
@state() private _diagnosticHandler?: DiagnosticInfo;
|
||||
@@ -396,6 +406,13 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
this.domain
|
||||
);
|
||||
|
||||
// Without the dependents, a dependency looks like a YAML integration.
|
||||
const dependents =
|
||||
this._manifest && !this._manifest.config_flow
|
||||
? this._getDependents(this.domain, this._loadedManifests)
|
||||
: [];
|
||||
const showDependents = normalEntries.length === 0 && dependents.length > 0;
|
||||
|
||||
const documentationLink = this._manifest?.is_built_in
|
||||
? documentationUrl(this.hass, `/integrations/${this._manifest.domain}`)
|
||||
: this._manifest?.documentation;
|
||||
@@ -581,6 +598,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
${
|
||||
normalEntries.length === 0 &&
|
||||
!showDependents &&
|
||||
this._manifest &&
|
||||
!this._manifest.config_flow &&
|
||||
this.hass.config.components.find(
|
||||
@@ -702,6 +720,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
${
|
||||
this._manifest?.integration_type !== "hardware" &&
|
||||
!showDependents &&
|
||||
(!this._manifest?.single_config_entry ||
|
||||
(normalData.length === 0 && attentionData.length === 0))
|
||||
? html`<ha-button
|
||||
@@ -880,61 +899,127 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<div class="section">
|
||||
<h3 class="section-header">
|
||||
${
|
||||
this._manifest?.integration_type
|
||||
? this.hass.localize(
|
||||
`ui.panel.config.integrations.integration_page.entries_${this._manifest.integration_type}`
|
||||
)
|
||||
: this.hass.localize(
|
||||
`ui.panel.config.integrations.integration_page.entries`
|
||||
)
|
||||
}
|
||||
</h3>
|
||||
${
|
||||
filteredNormalData.length === 0
|
||||
? html`<div class="card-content no-entries">
|
||||
${
|
||||
this._filter
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.integrations.none_found"
|
||||
)
|
||||
: this._manifest &&
|
||||
!this._manifest.config_flow &&
|
||||
this.hass.config.components.find(
|
||||
(comp) => comp.split(".")[0] === this.domain
|
||||
)
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.integrations.integration_page.yaml_entry"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.integrations.integration_page.no_entries"
|
||||
)
|
||||
}
|
||||
</div>`
|
||||
: html`
|
||||
${filteredNormalData.map(
|
||||
(data) =>
|
||||
html`<ha-config-entry-row
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.manifest=${this._manifest}
|
||||
.diagnosticHandler=${this._diagnosticHandler}
|
||||
.entities=${this._entities}
|
||||
.data=${data}
|
||||
data-entry-id=${data.entry.entry_id}
|
||||
></ha-config-entry-row>`
|
||||
${
|
||||
showDependents
|
||||
? html`<div class="section">
|
||||
<h3 class="section-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.integration_page.used_by"
|
||||
)}
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</h3>
|
||||
${this._renderDependents(dependents)}
|
||||
</div>`
|
||||
: html`<div class="section">
|
||||
<h3 class="section-header">
|
||||
${
|
||||
this._manifest?.integration_type
|
||||
? this.hass.localize(
|
||||
`ui.panel.config.integrations.integration_page.entries_${this._manifest.integration_type}`
|
||||
)
|
||||
: this.hass.localize(
|
||||
`ui.panel.config.integrations.integration_page.entries`
|
||||
)
|
||||
}
|
||||
</h3>
|
||||
${
|
||||
filteredNormalData.length === 0
|
||||
? html`<div class="card-content no-entries">
|
||||
${
|
||||
this._filter
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.integrations.none_found"
|
||||
)
|
||||
: this._manifest &&
|
||||
!this._manifest.config_flow &&
|
||||
this.hass.config.components.find(
|
||||
(comp) => comp.split(".")[0] === this.domain
|
||||
)
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.integrations.integration_page.yaml_entry"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.integrations.integration_page.no_entries"
|
||||
)
|
||||
}
|
||||
</div>`
|
||||
: html`
|
||||
${filteredNormalData.map(
|
||||
(data) =>
|
||||
html`<ha-config-entry-row
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.manifest=${this._manifest}
|
||||
.diagnosticHandler=${this._diagnosticHandler}
|
||||
.entities=${this._entities}
|
||||
.data=${data}
|
||||
data-entry-id=${data.entry.entry_id}
|
||||
></ha-config-entry-row>`
|
||||
)}
|
||||
`
|
||||
}
|
||||
</div>`
|
||||
}
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
// Only an integration without a config flow needs the loaded manifests, so
|
||||
// other integration pages do not make the context fetch them.
|
||||
private _watchLoadedManifests(): void {
|
||||
if (this._manifestsConsumer) {
|
||||
return;
|
||||
}
|
||||
this._manifestsConsumer = new ContextSubscriptionController(
|
||||
this,
|
||||
manifestsContext,
|
||||
(manifests) => {
|
||||
this._loadedManifests = manifests;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _getDependents = memoizeOne(
|
||||
(domain: string, manifests?: DomainManifestLookup) =>
|
||||
Object.values(manifests ?? {}).filter(
|
||||
// System integrations, like Default Configuration, are not useful to list.
|
||||
(manifest) =>
|
||||
manifest.integration_type !== "system" &&
|
||||
manifest.dependencies?.includes(domain)
|
||||
)
|
||||
);
|
||||
|
||||
private _renderDependents(dependents: IntegrationManifest[]) {
|
||||
return html`<ha-list-base>
|
||||
${dependents.map(
|
||||
(manifest) =>
|
||||
html`<ha-list-item-button
|
||||
class="dependent"
|
||||
href=${`/config/integrations/integration/${manifest.domain}`}
|
||||
>
|
||||
<img
|
||||
slot="start"
|
||||
alt=""
|
||||
src=${brandsUrl(
|
||||
{
|
||||
domain: manifest.domain,
|
||||
type: "icon@2x",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
)}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<div slot="headline">
|
||||
${domainToName(this.hass.localize, manifest.domain, manifest)}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>`
|
||||
)}
|
||||
</ha-list-base>`;
|
||||
}
|
||||
|
||||
private _onImageLoad(ev) {
|
||||
ev.target.style.display = "inline-block";
|
||||
}
|
||||
@@ -975,6 +1060,9 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
return;
|
||||
}
|
||||
this._manifest = await fetchIntegrationManifest(this.hass, this.domain);
|
||||
if (!this._manifest.config_flow) {
|
||||
this._watchLoadedManifests();
|
||||
}
|
||||
if (
|
||||
this._manifest.integration_type &&
|
||||
!["device", "hub", "service"].includes(this._manifest.integration_type)
|
||||
@@ -1624,6 +1712,17 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
ha-list-item-base.discovered {
|
||||
--ha-row-item-min-height: 72px;
|
||||
}
|
||||
ha-list-item-button.dependent {
|
||||
--ha-row-item-min-height: 72px;
|
||||
}
|
||||
ha-list-item-button.dependent + ha-list-item-button.dependent {
|
||||
border-top: var(--ha-border-width-sm) solid var(--divider-color);
|
||||
}
|
||||
ha-list-item-button.dependent img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
object-fit: contain;
|
||||
}
|
||||
ha-list-item-base.config_entry {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -243,6 +243,14 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
] => {
|
||||
const entryDomains = new Set(configEntries.map((entry) => entry.domain));
|
||||
|
||||
// An integration loaded as a dependency of another is not set up in YAML.
|
||||
const dependencies = new Set<string>();
|
||||
for (const component of components) {
|
||||
manifests[component.split(".")[0]]?.dependencies?.forEach((domain) =>
|
||||
dependencies.add(domain)
|
||||
);
|
||||
}
|
||||
|
||||
const domains = new Set<string>();
|
||||
|
||||
for (const component of components) {
|
||||
@@ -250,6 +258,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
const manifest = manifests[componentDomain];
|
||||
if (
|
||||
!entryDomains.has(componentDomain) &&
|
||||
!dependencies.has(componentDomain) &&
|
||||
manifest &&
|
||||
!manifest.config_flow &&
|
||||
(!manifest.integration_type ||
|
||||
|
||||
@@ -7808,6 +7808,7 @@
|
||||
"entries_virtual": "[%key:ui::panel::config::integrations::integration_page::entries%]",
|
||||
"entries_entity": "[%key:ui::panel::config::integrations::integration_page::entries%]",
|
||||
"no_entries": "No entries",
|
||||
"used_by": "Used by",
|
||||
"yaml_entry": "This integration was not set up via the UI, you have either set it up in YAML or it is a dependency set up by another integration. If you want to configure it, you will need to do so in your configuration.yaml file.",
|
||||
"attention_entries": "Needs attention",
|
||||
"add_entry": "Add entry",
|
||||
|
||||
Reference in new issue
Block a user