Compare commits

...
Author SHA1 Message Date
Claude c892ac8439 Always show the settings panel button
An entry row links to the panel only while the entry is loaded. When
all entries are disabled, not loaded, or still setting up, the page had
no link to the panel. The integration page now always shows the
settings panel button.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EzDS5Gc7EVw1NsLpEB7yza
2026-10-31 20:22:09 +00:00
Claude 85cd098047 Link the integration page to the integration's settings panel
The Connectivity settings panels, like Modbus and Infrared, are not
reachable from the integration page unless the integration has a config
entry. A user that opens the integration page looks for these settings.

The integration page now shows a button that opens the panel of the
integration when the integration has no entries. With entries, each
entry row already links to the panel. The button uses the panel name
from the Connectivity page. LoRaWAN, Modbus, Radio frequency, and USB
(Serial) are added to the map of integrations with a panel.

An integration with a panel and no config flow is set up in its panel.
For such an integration, the button is the primary action, and the page
does not show the YAML texts, the empty entries section, or the
"Add entry" button.

The demo Infrared and Radio frequency manifests now match core, so
these pages show it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EzDS5Gc7EVw1NsLpEB7yza
2026-10-31 19:59:19 +00:00
5 changed files with 108 additions and 48 deletions

No files matched your search

@@ -59,6 +59,10 @@ const REGISTRY_ENTRIES = [
export const infraredFixtures: ConnectivityFixtures = {
components: ["infrared"],
manifests: [
manifest("infrared", "Infrared", {
config_flow: false,
integration_type: "entity",
}),
manifest("broadlink", "Broadlink", {
integration_type: "hub",
iot_class: "local_polling",
@@ -42,7 +42,13 @@ const REGISTRY_ENTRIES = [
export const radioFrequencyFixtures: ConnectivityFixtures = {
components: ["radio_frequency"],
commands: ["radio_frequency/"],
manifests: [manifest("esphome", "ESPHome", { integration_type: "device" })],
manifests: [
manifest("radio_frequency", "Radio frequency", {
config_flow: false,
integration_type: "entity",
}),
manifest("esphome", "ESPHome", { integration_type: "device" }),
],
configEntries: [
{ type: "device", entry: configEntry(ENTRY_ID, "esphome", "RF Bridge") },
],
+4
View File
@@ -14,10 +14,14 @@ export const integrationsWithPanel = {
bluetooth: "config/bluetooth",
dhcp: "config/dhcp",
infrared: "config/infrared",
lorawan: "config/lorawan",
matter: "config/matter",
modbus: "config/modbus",
mqtt: "config/mqtt",
radio_frequency: "config/radio-frequency",
ssdp: "config/ssdp",
thread: "config/thread",
usb: "config/serial",
zeroconf: "config/zeroconf",
zha: "config/zha/dashboard",
zwave_js: "config/zwave_js/dashboard",
@@ -17,6 +17,7 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, queryAll, state } from "lit/decorators";
import { until } from "lit/directives/until";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../../common/array/ensure-array";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { QueryParamsController } from "../../../common/controllers/query-params-controller";
import type { HASSDomTargetEvent } from "../../../common/dom/fire_event";
@@ -65,6 +66,7 @@ import {
LogSeverity,
domainToName,
fetchIntegrationManifest,
getConfigPanelPath,
integrationIssuesUrl,
setIntegrationLogLevel,
subscribeLogInfo,
@@ -82,6 +84,7 @@ import type { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { documentationUrl } from "../../../util/documentation-url";
import { fileDownload } from "../../../util/file_download";
import { configSections } from "../config-sections";
import "./ha-config-entry-row";
import type { HaConfigEntryRow } from "./ha-config-entry-row";
import type { DataEntryFlowProgressExtended } from "./ha-config-integrations";
@@ -396,6 +399,14 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
this.domain
);
const configPanel = isComponentLoaded(this.hass.config, this.domain)
? getConfigPanelPath(this.domain, this.hass.panels)
: undefined;
// Without a config flow, the panel is where the integration is set up.
const setUpInPanel = Boolean(
configPanel && this._manifest && !this._manifest.config_flow
);
const documentationLink = this._manifest?.is_built_in
? documentationUrl(this.hass, `/integrations/${this._manifest.domain}`)
: this._manifest?.documentation;
@@ -581,6 +592,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
}
${
normalEntries.length === 0 &&
!setUpInPanel &&
this._manifest &&
!this._manifest.config_flow &&
this.hass.config.components.find(
@@ -689,6 +701,21 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
</div>
</div>
<div class="actions">
${
configPanel
? html`<ha-button
.appearance=${setUpInPanel ? "accent" : "outlined"}
href=${`/${configPanel}`}
>
${this.hass.localize(
"ui.panel.config.integrations.integration_page.open_config_panel",
{
name: this._configPanelName(this.domain, configPanel),
}
)}
</ha-button>`
: nothing
}
${
canAddDevice
? html`
@@ -702,6 +729,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
}
${
this._manifest?.integration_type !== "hardware" &&
!setUpInPanel &&
(!this._manifest?.single_config_entry ||
(normalData.length === 0 && attentionData.length === 0))
? html`<ha-button
@@ -880,56 +908,59 @@ 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">
${
setUpInPanel && normalEntries.length === 0
? nothing
: html`<div class="section">
<h3 class="section-header">
${
this._filter
this._manifest?.integration_type
? this.hass.localize(
"ui.panel.config.integrations.none_found"
`ui.panel.config.integrations.integration_page.entries_${this._manifest.integration_type}`
)
: this.hass.localize(
`ui.panel.config.integrations.integration_page.entries`
)
: 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>
</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>
`;
@@ -1388,6 +1419,20 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
}
);
private _configPanelName(domain: string, configPanel: string): string {
// Use the name the Connectivity page shows, like "Zigbee" for ZHA.
const page = configSections.connectivity.find(
(section) =>
ensureArray(section.component)?.includes(domain) ||
section.path === `/${configPanel}`
);
return page
? this.hass.localize(
`ui.panel.config.dashboard.${page.translationKey}.main`
)
: domainToName(this.hass.localize, domain, this._manifest);
}
private _addDevice() {
protocolIntegrationPicked(this, this.hass, this.domain);
}
+2 -1
View File
@@ -7818,7 +7818,8 @@
"add_hardware": "Add hardware",
"add_entity": "[%key:ui::panel::config::integrations::integration_page::add_entry%]",
"add_system": "[%key:ui::panel::config::integrations::integration_page::add_entry%]",
"add_virtual": "[%key:ui::panel::config::integrations::integration_page::add_entry%]"
"add_virtual": "[%key:ui::panel::config::integrations::integration_page::add_entry%]",
"open_config_panel": "{name} settings"
},
"config_entry": {
"application_credentials": {