-
-

-
- ${this._manifest?.version != null
- ? html`
${this._manifest.version}
`
- : nothing}
- ${this._manifest?.is_built_in === false
- ? html`
`
- : nothing}
- ${this._manifest?.iot_class?.startsWith("cloud_")
- ? html`
-
${this.hass.localize(
- "ui.panel.config.integrations.config_entry.depends_on_cloud"
+ "ui.panel.config.integrations.config_entry.known_issues"
)}
-
`
- : nothing}
- ${normalEntries.length === 0 &&
- this._manifest &&
- !this._manifest.config_flow &&
- this.hass.config.components.find(
- (comp) => comp.split(".")[0] === this.domain
- )
- ? html`
- ${this.hass.localize(
- "ui.panel.config.integrations.config_entry.no_config_flow"
- )}
-
`
- : nothing}
-
-
-
- ${this._manifest?.is_built_in &&
- this._manifest.quality_scale &&
- Object.keys(QUALITY_SCALE_MAP).includes(
- this._manifest.quality_scale
- )
- ? html`
-
-
-
- ${this.hass.localize(
- QUALITY_SCALE_MAP[this._manifest.quality_scale]
- .translationKey
- )}
-
-
-
- `
- : nothing}
- ${devices.length > 0
- ? html`
-
-
- ${this.hass.localize(
- `ui.panel.config.integrations.config_entry.${
- services ? "services" : "devices"
- }`,
- { count: devices.length }
- )}
-
-
- `
- : nothing}
- ${numberOfEntities > 0
- ? html`
-
-
- ${this.hass.localize(
- `ui.panel.config.integrations.config_entry.entities`,
- { count: numberOfEntities }
- )}
-
-
- `
- : nothing}
- ${this._manifest
- ? html`
-
- ${this.hass.localize(
- "ui.panel.config.integrations.config_entry.documentation"
- )}
-
-
-
- `
- : nothing}
- ${this._manifest &&
- (this._manifest.is_built_in || this._manifest.issue_tracker)
- ? html`
-
- ${this.hass.localize(
- "ui.panel.config.integrations.config_entry.known_issues"
- )}
-
-
-
- `
- : nothing}
- ${this._logInfo
- ? html`
- ${this._logInfo.level === LogSeverity.DEBUG
- ? this.hass.localize(
- "ui.panel.config.integrations.config_entry.disable_debug_logging"
- )
- : this.hass.localize(
- "ui.panel.config.integrations.config_entry.enable_debug_logging"
- )}
+
- `
- : nothing}
+
+ `
+ : nothing}
+ ${this._logInfo
+ ? html`
+ ${this._logInfo.level === LogSeverity.DEBUG
+ ? this.hass.localize(
+ "ui.panel.config.integrations.config_entry.disable_debug_logging"
+ )
+ : this.hass.localize(
+ "ui.panel.config.integrations.config_entry.enable_debug_logging"
+ )}
+
+ `
+ : nothing}
+ `
+ : nothing}
+
+
+
-
- ${discoveryFlows.length
- ? html`
-
@@ -692,421 +674,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
ev.target.style.display = "none";
}
- private _renderDeviceLine(
- item: ConfigEntry,
- devices: DeviceRegistryEntry[],
- services: DeviceRegistryEntry[],
- entities: EntityRegistryEntry[],
- subItem?: SubEntry
- ) {
- let devicesLine: (TemplateResult | string)[] = [];
- for (const [items, localizeKey] of [
- [devices, "devices"],
- [services, "services"],
- ] as const) {
- if (items.length === 0) {
- continue;
- }
- const url =
- items.length === 1
- ? `/config/devices/device/${items[0].id}`
- : `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}${subItem ? `&sub_entry=${subItem.subentry_id}` : ""}`;
- devicesLine.push(
- // no white space before/after template on purpose
- html`
${this.hass.localize(
- `ui.panel.config.integrations.config_entry.${localizeKey}`,
- { count: items.length }
- )}`
- );
- }
-
- if (entities.length) {
- devicesLine.push(
- // no white space before/after template on purpose
- html`
${this.hass.localize(
- "ui.panel.config.integrations.config_entry.entities",
- { count: entities.length }
- )}`
- );
- }
-
- if (devicesLine.length === 0) {
- devicesLine = [
- this.hass.localize(
- "ui.panel.config.integrations.config_entry.no_devices_or_entities"
- ),
- ];
- } else if (devicesLine.length === 2) {
- devicesLine = [
- devicesLine[0],
- ` ${this.hass.localize("ui.common.and")} `,
- devicesLine[1],
- ];
- } else if (devicesLine.length === 3) {
- devicesLine = [
- devicesLine[0],
- ", ",
- devicesLine[1],
- ` ${this.hass.localize("ui.common.and")} `,
- devicesLine[2],
- ];
- }
- return devicesLine;
- }
-
- private _renderConfigEntry(item: ConfigEntry) {
- let stateText: Parameters
| undefined;
- let stateTextExtra: TemplateResult | string | undefined;
- let icon: string = mdiAlertCircle;
-
- if (!item.disabled_by && item.state === "not_loaded") {
- stateText = ["ui.panel.config.integrations.config_entry.not_loaded"];
- } else if (item.state === "setup_in_progress") {
- icon = mdiProgressHelper;
- stateText = [
- "ui.panel.config.integrations.config_entry.setup_in_progress",
- ];
- } else if (ERROR_STATES.includes(item.state)) {
- if (item.state === "setup_retry") {
- icon = mdiReloadAlert;
- }
- stateText = [
- `ui.panel.config.integrations.config_entry.state.${item.state}`,
- ];
- stateTextExtra = renderConfigEntryError(this.hass, item);
- }
-
- const devices = this._getConfigEntryDevices(item);
- const services = this._getConfigEntryServices(item);
- const entities = this._getConfigEntryEntities(item);
-
- let devicesLine: (TemplateResult | string)[] = [];
-
- if (item.disabled_by) {
- devicesLine.push(
- 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.${item.disabled_by}`
- ) || item.disabled_by,
- }
- )
- );
- if (item.state === "failed_unload") {
- devicesLine.push(`.
- ${this.hass.localize(
- "ui.panel.config.integrations.config_entry.disable_restart_confirm"
- )}.`);
- }
- } else {
- devicesLine = this._renderDeviceLine(item, devices, services, entities);
- }
-
- const configPanel = this._configPanel(item.domain, this.hass.panels);
-
- const subEntries = this._subEntries[item.entry_id] || [];
-
- return html`
-
- ${item.title || domainToName(this.hass.localize, item.domain)}
-
-
-
${devicesLine}
- ${stateText
- ? html`
-
-
-
- ${this.hass.localize(...stateText)}${stateTextExtra
- ? html`: ${stateTextExtra}`
- : nothing}
-
-
- `
- : nothing}
-
- ${item.disabled_by === "user"
- ? html`
- ${this.hass.localize("ui.common.enable")}
- `
- : configPanel &&
- (item.domain !== "matter" ||
- isDevVersion(this.hass.config.version)) &&
- !stateText
- ? html`
- ${this.hass.localize(
- "ui.panel.config.integrations.config_entry.configure"
- )}
- `
- : item.supports_options
- ? html`
-
- ${this.hass.localize(
- "ui.panel.config.integrations.config_entry.configure"
- )}
-
- `
- : nothing}
-
-
- ${item.disabled_by && devices.length
- ? html`
-
-
- ${this.hass.localize(
- `ui.panel.config.integrations.config_entry.devices`,
- { count: devices.length }
- )}
-
-
- `
- : nothing}
- ${item.disabled_by && services.length
- ? html`
-
- ${this.hass.localize(
- `ui.panel.config.integrations.config_entry.services`,
- { count: services.length }
- )}
-
- `
- : nothing}
- ${item.disabled_by && entities.length
- ? html`
-
-
- ${this.hass.localize(
- `ui.panel.config.integrations.config_entry.entities`,
- { count: entities.length }
- )}
-
-
- `
- : nothing}
- ${!item.disabled_by &&
- RECOVERABLE_STATES.includes(item.state) &&
- item.supports_unload &&
- item.source !== "system"
- ? html`
-
-
- ${this.hass.localize(
- "ui.panel.config.integrations.config_entry.reload"
- )}
-
- `
- : nothing}
-
-
-
- ${this.hass.localize(
- "ui.panel.config.integrations.config_entry.rename"
- )}
-
-
- ${Object.keys(item.supported_subentry_types).map(
- (flowType) =>
- html`
-
- ${this.hass.localize(
- `component.${item.domain}.config_subentries.${flowType}.initiate_flow.user`
- )}`
- )}
-
-
-
- ${this._diagnosticHandler && item.state === "loaded"
- ? html`
-
-
- ${this.hass.localize(
- "ui.panel.config.integrations.config_entry.download_diagnostics"
- )}
-
- `
- : nothing}
- ${!item.disabled_by &&
- item.supports_reconfigure &&
- item.source !== "system"
- ? html`
-
-
- ${this.hass.localize(
- "ui.panel.config.integrations.config_entry.reconfigure"
- )}
-
- `
- : nothing}
-
-
-
- ${this.hass.localize(
- "ui.panel.config.integrations.config_entry.system_options"
- )}
-
- ${item.disabled_by === "user"
- ? html`
-
-
- ${this.hass.localize("ui.common.enable")}
-
- `
- : item.source !== "system"
- ? html`
-
-
- ${this.hass.localize("ui.common.disable")}
-
- `
- : nothing}
- ${item.source !== "system"
- ? html`
-
-
- ${this.hass.localize(
- "ui.panel.config.integrations.config_entry.delete"
- )}
-
- `
- : nothing}
-
-
- ${subEntries.map((subEntry) => this._renderSubEntry(item, subEntry))}`;
- }
-
- private _renderSubEntry(configEntry: ConfigEntry, subEntry: SubEntry) {
- const devices = this._getConfigEntryDevices(configEntry).filter((device) =>
- device.config_entries_subentries[configEntry.entry_id]?.includes(
- subEntry.subentry_id
- )
- );
- const services = this._getConfigEntryServices(configEntry).filter(
- (device) =>
- device.config_entries_subentries[configEntry.entry_id]?.includes(
- subEntry.subentry_id
- )
- );
- const entities = this._getConfigEntryEntities(configEntry).filter(
- (entity) => entity.config_subentry_id === subEntry.subentry_id
- );
-
- return html`
- ${subEntry.title}
- ${this.hass.localize(
- `component.${configEntry.domain}.config_subentries.${subEntry.subentry_type}.entry_type`
- )}
- -
- ${this._renderDeviceLine(
- configEntry,
- devices,
- services,
- entities,
- subEntry
- )}
- ${configEntry.supported_subentry_types[subEntry.subentry_type]
- ?.supports_reconfigure
- ? html`
-
- ${this.hass.localize(
- "ui.panel.config.integrations.config_entry.configure"
- )}
-
- `
- : nothing}
-
-
-
-
- ${this.hass.localize(
- "ui.panel.config.integrations.config_entry.delete"
- )}
-
-
- `;
- }
-
private async _highlightEntry() {
await nextRender();
const entryId = this._searchParms.get("config_entry")!;
@@ -1146,27 +713,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
}
}
- private async _fetchSubEntries() {
- const subEntriesPromises = (
- this._extraConfigEntries || this.configEntries
- )?.map((entry) =>
- entry.num_subentries
- ? getSubEntries(this.hass, entry.entry_id).then((subEntries) => ({
- entry_id: entry.entry_id,
- subEntries,
- }))
- : undefined
- );
- if (subEntriesPromises) {
- const subEntries = await Promise.all(subEntriesPromises);
- this._subEntries = {};
- subEntries.forEach((entry) => {
- if (!entry) return;
- this._subEntries[entry.entry_id] = entry.subEntries;
- });
- }
- }
-
private async _fetchDiagnostics() {
if (!this.domain || !isComponentLoaded(this.hass, "diagnostics")) {
return;
@@ -1239,361 +785,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
}
);
- private _getConfigEntryEntities = (
- configEntry: ConfigEntry
- ): EntityRegistryEntry[] => {
- const entries = this._domainConfigEntries(
- this.domain,
- this._extraConfigEntries || this.configEntries
- );
- const entityRegistryEntries = this._getEntities(entries, this._entities);
- return entityRegistryEntries.filter(
- (entity) => entity.config_entry_id === configEntry.entry_id
- );
- };
-
- private _getConfigEntryDevices = (
- configEntry: ConfigEntry
- ): DeviceRegistryEntry[] => {
- const entries = this._domainConfigEntries(
- this.domain,
- this._extraConfigEntries || this.configEntries
- );
- const deviceRegistryEntries = this._getDevices(entries, this.hass.devices);
- return Object.values(deviceRegistryEntries).filter(
- (device) =>
- device.config_entries.includes(configEntry.entry_id) &&
- device.entry_type !== "service"
- );
- };
-
- private _getConfigEntryServices = (
- configEntry: ConfigEntry
- ): DeviceRegistryEntry[] => {
- const entries = this._domainConfigEntries(
- this.domain,
- this._extraConfigEntries || this.configEntries
- );
- const deviceRegistryEntries = this._getDevices(entries, this.hass.devices);
- return Object.values(deviceRegistryEntries).filter(
- (device) =>
- device.config_entries.includes(configEntry.entry_id) &&
- device.entry_type === "service"
- );
- };
-
- private _showOptions(ev) {
- showOptionsFlowDialog(
- this,
- ev.target.closest(".config_entry").configEntry,
- { manifest: this._manifest }
- );
- }
-
- private _handleRename(ev: Event): void {
- this._editEntryName(
- ((ev.target as HTMLElement).closest(".config_entry") as any).configEntry
- );
- }
-
- private _handleReload(ev: Event): void {
- this._reloadIntegration(
- ((ev.target as HTMLElement).closest(".config_entry") as any).configEntry
- );
- }
-
- private _handleReconfigure(ev: Event): void {
- this._reconfigureIntegration(
- ((ev.target as HTMLElement).closest(".config_entry") as any).configEntry
- );
- }
-
- private _handleDelete(ev: Event): void {
- this._removeIntegration(
- ((ev.target as HTMLElement).closest(".config_entry") as any).configEntry
- );
- }
-
- private async _handleReconfigureSub(ev: Event): Promise {
- const configEntry = (
- (ev.target as HTMLElement).closest(".sub-entry") as any
- ).configEntry;
- const subEntry = ((ev.target as HTMLElement).closest(".sub-entry") as any)
- .subEntry;
-
- showSubConfigFlowDialog(
- this,
- configEntry,
- subEntry.flowType || subEntry.subentry_type,
- {
- startFlowHandler: configEntry.entry_id,
- subEntryId: subEntry.subentry_id,
- }
- );
- }
-
- private async _handleDeleteSub(ev: Event): Promise {
- const configEntry = (
- (ev.target as HTMLElement).closest(".sub-entry") as any
- ).configEntry;
- const subEntry = ((ev.target as HTMLElement).closest(".sub-entry") as any)
- .subEntry;
- const confirmed = await showConfirmationDialog(this, {
- title: this.hass.localize(
- "ui.panel.config.integrations.config_entry.delete_confirm_title",
- { title: subEntry.title }
- ),
- text: this.hass.localize(
- "ui.panel.config.integrations.config_entry.delete_confirm_text"
- ),
- confirmText: this.hass!.localize("ui.common.delete"),
- dismissText: this.hass!.localize("ui.common.cancel"),
- destructive: true,
- });
-
- if (!confirmed) {
- return;
- }
- await deleteSubEntry(this.hass, configEntry.entry_id, subEntry.subentry_id);
- }
-
- private _handleDisable(ev: Event): void {
- this._disableIntegration(
- ((ev.target as HTMLElement).closest(".config_entry") as any).configEntry
- );
- }
-
- private _handleEnable(ev: Event): void {
- this._enableIntegration(
- ((ev.target as HTMLElement).closest(".config_entry") as any).configEntry
- );
- }
-
- private _handleSystemOptions(ev: Event): void {
- this._showSystemOptions(
- ((ev.target as HTMLElement).closest(".config_entry") as any).configEntry
- );
- }
-
- private _showSystemOptions(configEntry: ConfigEntry) {
- showConfigEntrySystemOptionsDialog(this, {
- entry: configEntry,
- manifest: this._manifest,
- });
- }
-
- private async _disableIntegration(configEntry: ConfigEntry) {
- const entryId = configEntry.entry_id;
-
- const confirmed = await showConfirmationDialog(this, {
- title: this.hass.localize(
- "ui.panel.config.integrations.config_entry.disable_confirm_title",
- { title: configEntry.title }
- ),
- text: this.hass.localize(
- "ui.panel.config.integrations.config_entry.disable_confirm_text"
- ),
- confirmText: this.hass!.localize("ui.common.disable"),
- dismissText: this.hass!.localize("ui.common.cancel"),
- destructive: true,
- });
-
- if (!confirmed) {
- return;
- }
- let result: DisableConfigEntryResult;
- try {
- result = await disableConfigEntry(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.disable_restart_confirm"
- ),
- });
- }
- }
-
- private async _enableIntegration(configEntry: ConfigEntry) {
- const entryId = configEntry.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"
- ),
- });
- }
- }
-
- private async _removeIntegration(configEntry: ConfigEntry) {
- const entryId = configEntry.entry_id;
-
- const applicationCredentialsId =
- await this._applicationCredentialForRemove(entryId);
-
- const confirmed = await showConfirmationDialog(this, {
- title: this.hass.localize(
- "ui.panel.config.integrations.config_entry.delete_confirm_title",
- { title: configEntry.title }
- ),
- text: this.hass.localize(
- "ui.panel.config.integrations.config_entry.delete_confirm_text"
- ),
- confirmText: this.hass!.localize("ui.common.delete"),
- dismissText: this.hass!.localize("ui.common.cancel"),
- destructive: true,
- });
-
- if (!confirmed) {
- return;
- }
- const result = await deleteConfigEntry(this.hass, entryId);
-
- if (result.require_restart) {
- showAlertDialog(this, {
- text: this.hass.localize(
- "ui.panel.config.integrations.config_entry.restart_confirm"
- ),
- });
- }
- if (applicationCredentialsId) {
- this._removeApplicationCredential(applicationCredentialsId);
- }
- }
-
- // Return an application credentials id for this config entry to prompt the
- // user for removal. This is best effort so we don't stop overall removal
- // if the integration isn't loaded or there is some other error.
- private async _applicationCredentialForRemove(entryId: string) {
- try {
- return (await fetchApplicationCredentialsConfigEntry(this.hass, entryId))
- .application_credentials_id;
- } catch (_err: any) {
- // We won't prompt the user to remove credentials
- return null;
- }
- }
-
- private async _removeApplicationCredential(applicationCredentialsId: string) {
- const confirmed = await showConfirmationDialog(this, {
- title: this.hass.localize(
- "ui.panel.config.integrations.config_entry.application_credentials.delete_title"
- ),
- text: html`${this.hass.localize(
- "ui.panel.config.integrations.config_entry.application_credentials.delete_prompt"
- )},
-
-
- ${this.hass.localize(
- "ui.panel.config.integrations.config_entry.application_credentials.delete_detail"
- )}
-
-
-
- ${this.hass.localize(
- "ui.panel.config.integrations.config_entry.application_credentials.learn_more"
- )}
- `,
- destructive: true,
- confirmText: this.hass.localize("ui.common.remove"),
- dismissText: this.hass.localize(
- "ui.panel.config.integrations.config_entry.application_credentials.dismiss"
- ),
- });
- if (!confirmed) {
- return;
- }
- try {
- await deleteApplicationCredential(this.hass, applicationCredentialsId);
- } catch (err: any) {
- showAlertDialog(this, {
- title: this.hass.localize(
- "ui.panel.config.integrations.config_entry.application_credentials.delete_error_title"
- ),
- text: err.message,
- });
- }
- }
-
- private async _reloadIntegration(configEntry: ConfigEntry) {
- const entryId = configEntry.entry_id;
-
- const result = await reloadConfigEntry(this.hass, entryId);
- const locale_key = result.require_restart
- ? "reload_restart_confirm"
- : "reload_confirm";
- showAlertDialog(this, {
- text: this.hass.localize(
- `ui.panel.config.integrations.config_entry.${locale_key}`
- ),
- });
- }
-
- private async _reconfigureIntegration(configEntry: ConfigEntry) {
- showConfigFlowDialog(this, {
- startFlowHandler: configEntry.domain,
- showAdvanced: this.hass.userData?.showAdvanced,
- manifest: await fetchIntegrationManifest(this.hass, configEntry.domain),
- entryId: configEntry.entry_id,
- navigateToResult: true,
- });
- }
-
- private async _editEntryName(configEntry: ConfigEntry) {
- const newName = await showPromptDialog(this, {
- title: this.hass.localize("ui.panel.config.integrations.rename_dialog"),
- defaultValue: configEntry.title,
- inputLabel: this.hass.localize(
- "ui.panel.config.integrations.rename_input_label"
- ),
- });
- if (newName === null) {
- return;
- }
- await updateConfigEntry(this.hass, configEntry.entry_id, {
- title: newName,
- });
- }
-
- private async _signUrl(ev) {
- const anchor = ev.currentTarget;
- ev.preventDefault();
- const signedUrl = await getSignedPath(
- this.hass,
- anchor.getAttribute("href")
- );
- fileDownload(signedUrl.path);
- }
-
private async _addIntegration() {
if (!this._manifest?.config_flow) {
showAlertDialog(this, {
@@ -1636,8 +827,33 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
}
private async _addSubEntry(ev) {
- showSubConfigFlowDialog(this, ev.target.entry, ev.target.flowType, {
- startFlowHandler: ev.target.entry.entry_id,
+ const flowType = ev.target.flowType;
+
+ const configEntries = this._domainConfigEntries(
+ this.domain,
+ this._extraConfigEntries || this.configEntries
+ ).filter((entry) => entry.source !== "ignore");
+
+ if (!configEntries.length) {
+ return;
+ }
+
+ if (configEntries.length === 1 && configEntries[0].state === "loaded") {
+ showSubConfigFlowDialog(this, configEntries[0], flowType, {
+ startFlowHandler: configEntries[0].entry_id,
+ });
+ return;
+ }
+
+ showPickConfigEntryDialog(this, {
+ domain: this.domain,
+ subFlowType: flowType,
+ configEntries,
+ configEntryPicked: (entry) => {
+ showSubConfigFlowDialog(this, entry, flowType, {
+ startFlowHandler: entry.entry_id,
+ });
+ },
});
}
@@ -1650,35 +866,49 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
flex-wrap: wrap;
margin: auto;
max-width: 1000px;
- margin-top: 32px;
- margin-bottom: 32px;
+ padding: 32px;
+ }
+ .container > * {
+ flex-grow: 1;
+ }
+ .header {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 24px;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 24px;
+ }
+ .title-container {
+ display: flex;
+ align-items: center;
+ }
+ .title {
+ display: flex;
+ gap: 8px;
+ flex-direction: column;
+ justify-content: space-between;
+ }
+ .title h1 {
+ font-family: Roboto;
+ font-size: 32px;
+ font-weight: 700;
+ line-height: 40px;
+ text-align: left;
+ text-underline-position: from-font;
+ text-decoration-skip-ink: none;
+ margin: 0;
+ }
+ .sub {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px 16px;
}
.card-content {
padding: 16px 0 8px;
}
- .column {
- width: 33%;
- flex-grow: 1;
- }
- .column.small {
- max-width: 300px;
- }
- .column,
- .fullwidth {
- padding: 8px;
- box-sizing: border-box;
- }
- .column > *:not(:first-child) {
- margin-top: 16px;
- }
-
- :host([narrow]) .column {
- width: 100%;
- max-width: unset;
- }
-
:host([narrow]) .container {
- margin-top: 0;
+ padding: 16px;
}
.card-header {
padding-bottom: 0;
@@ -1689,7 +919,11 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
.logo-container {
display: flex;
justify-content: center;
- margin-bottom: 8px;
+ margin-right: 16px;
+ padding: 0 8px;
+ }
+ .logo-container img {
+ width: 80px;
}
.version {
padding-top: 8px;
@@ -1710,12 +944,31 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
mask-position: left;
}
}
+ .actions {
+ display: flex;
+ gap: 8px;
+ }
+ .section {
+ width: 100%;
+ }
+ .section-header {
+ margin-inline-start: 16px;
+ margin-top: 6px;
+ margin-bottom: 6px;
+ font-family: Roboto;
+ font-size: 14px;
+ font-weight: 500;
+ line-height: 20px;
+ letter-spacing: 0.10000000149011612px;
+ text-align: left;
+ text-underline-position: from-font;
+ text-decoration-skip-ink: none;
+ color: var(--secondary-text-color);
+ }
.integration-info {
display: flex;
align-items: center;
- gap: 20px;
- padding: 0 20px;
- min-height: 48px;
+ gap: 8px;
}
.integration-info ha-svg-icon {
min-width: 24px;
@@ -1754,7 +1007,26 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
color: var(--mdc-theme-text-icon-on-background, rgba(0, 0, 0, 0.38));
animation: unset;
}
+ ha-md-list {
+ border: 1px solid var(--divider-color);
+ border-radius: 8px;
+ padding: 0;
+ }
+ .discovered {
+ --md-list-container-color: rgba(var(--rgb-success-color), 0.2);
+ }
+ .discovered ha-button {
+ --mdc-theme-primary: var(--success-color);
+ }
+ .attention {
+ --md-list-container-color: rgba(var(--rgb-warning-color), 0.2);
+ }
+ .attention ha-button {
+ --mdc-theme-primary: var(--warning-color);
+ }
ha-md-list-item {
+ --md-list-item-top-space: 4px;
+ --md-list-item-bottom-space: 4px;
position: relative;
}
ha-md-list-item.discovered {
@@ -1770,8 +1042,9 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
pointer-events: none;
content: "";
}
- ha-md-list-item.sub-entry {
- --md-list-item-leading-space: 50px;
+ ha-config-entry-row {
+ display: block;
+ margin-bottom: 16px;
}
a {
text-decoration: none;
@@ -1779,19 +1052,9 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
.highlight::after {
background-color: var(--info-color);
}
- .attention {
- primary-color: var(--error-color);
- }
.warning {
color: var(--error-color);
}
- .state-error {
- --state-message-color: var(--error-color);
- --text-on-state-color: var(--text-primary-color);
- }
- .state-error::after {
- background-color: var(--error-color);
- }
.state-failed-unload {
--state-message-color: var(--warning-color);
--text-on-state-color: var(--primary-text-color);
@@ -1837,6 +1100,13 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
margin-top: 8px;
margin-bottom: 8px;
}
+ a[slot="toolbar-icon"] {
+ color: var(--sidebar-icon-color);
+ }
+ ha-svg-icon.open-external {
+ min-width: 14px;
+ width: 14px;
+ }
`,
];
}
diff --git a/src/panels/config/integrations/ha-config-sub-entry-row.ts b/src/panels/config/integrations/ha-config-sub-entry-row.ts
new file mode 100644
index 0000000000..c30825a6c1
--- /dev/null
+++ b/src/panels/config/integrations/ha-config-sub-entry-row.ts
@@ -0,0 +1,250 @@
+import {
+ mdiChevronDown,
+ mdiChevronUp,
+ mdiCogOutline,
+ mdiDelete,
+ mdiDevices,
+ mdiDotsVertical,
+ mdiHandExtendedOutline,
+ mdiShapeOutline,
+} from "@mdi/js";
+import { css, html, LitElement, nothing } from "lit";
+import { customElement, property, state } from "lit/decorators";
+import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
+import { deleteSubEntry } from "../../../data/config_entries";
+import type { DeviceRegistryEntry } from "../../../data/device_registry";
+import type { DiagnosticInfo } from "../../../data/diagnostics";
+import type { EntityRegistryEntry } from "../../../data/entity_registry";
+import type { IntegrationManifest } from "../../../data/integration";
+import { showSubConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-sub-config-flow";
+import type { HomeAssistant } from "../../../types";
+import { showConfirmationDialog } from "../../lovelace/custom-card-helpers";
+import "./ha-config-entry-device-row";
+
+@customElement("ha-config-sub-entry-row")
+class HaConfigSubEntryRow extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property({ type: Boolean, reflect: true }) public narrow = false;
+
+ @property({ attribute: false }) public manifest?: IntegrationManifest;
+
+ @property({ attribute: false }) public diagnosticHandler?: DiagnosticInfo;
+
+ @property({ attribute: false }) public entities!: EntityRegistryEntry[];
+
+ @property({ attribute: false }) public entry!: ConfigEntry;
+
+ @property({ attribute: false }) public subEntry!: SubEntry;
+
+ @state() private _expanded = true;
+
+ protected render() {
+ const subEntry = this.subEntry;
+ const configEntry = this.entry;
+
+ const devices = this._getDevices();
+ const services = this._getServices();
+ const entities = this._getEntities();
+
+ return html`
+
+ ${devices.length || services.length
+ ? html``
+ : nothing}
+ ${subEntry.title}
+ ${this.hass.localize(
+ `component.${configEntry.domain}.config_subentries.${subEntry.subentry_type}.entry_type`
+ )}
+ ${configEntry.supported_subentry_types[subEntry.subentry_type]
+ ?.supports_reconfigure
+ ? html`
+
+
+ `
+ : nothing}
+
+
+ ${devices.length || services.length
+ ? html`
+
+
+ ${this.hass.localize(
+ `ui.panel.config.integrations.config_entry.devices`,
+ { count: devices.length }
+ )}
+
+
+ `
+ : nothing}
+ ${services.length
+ ? html`
+
+ ${this.hass.localize(
+ `ui.panel.config.integrations.config_entry.services`,
+ { count: services.length }
+ )}
+
+ `
+ : nothing}
+ ${entities.length
+ ? html`
+
+
+ ${this.hass.localize(
+ `ui.panel.config.integrations.config_entry.entities`,
+ { count: entities.length }
+ )}
+
+
+ `
+ : nothing}
+
+
+ ${this.hass.localize(
+ "ui.panel.config.integrations.config_entry.delete"
+ )}
+
+
+
+ ${this._expanded
+ ? html`
+ ${devices.map(
+ (device) =>
+ html``
+ )}
+ `
+ : nothing}
+ `;
+ }
+
+ private _toggleExpand() {
+ this._expanded = !this._expanded;
+ }
+
+ private _getEntities = (): EntityRegistryEntry[] =>
+ this.entities.filter(
+ (entity) => entity.config_subentry_id === this.subEntry.subentry_id
+ );
+
+ private _getDevices = (): DeviceRegistryEntry[] =>
+ Object.values(this.hass.devices).filter(
+ (device) =>
+ device.config_entries_subentries[this.entry.entry_id]?.includes(
+ this.subEntry.subentry_id
+ ) && device.entry_type !== "service"
+ );
+
+ private _getServices = (): DeviceRegistryEntry[] =>
+ Object.values(this.hass.devices).filter(
+ (device) =>
+ device.config_entries_subentries[this.entry.entry_id]?.includes(
+ this.subEntry.subentry_id
+ ) && device.entry_type === "service"
+ );
+
+ private async _handleReconfigureSub(): Promise {
+ showSubConfigFlowDialog(this, this.entry, this.subEntry.subentry_type, {
+ startFlowHandler: this.entry.entry_id,
+ subEntryId: this.subEntry.subentry_id,
+ });
+ }
+
+ private async _handleDeleteSub(): Promise {
+ const confirmed = await showConfirmationDialog(this, {
+ title: this.hass.localize(
+ "ui.panel.config.integrations.config_entry.delete_confirm_title",
+ { title: this.subEntry.title }
+ ),
+ text: this.hass.localize(
+ "ui.panel.config.integrations.config_entry.delete_confirm_text"
+ ),
+ confirmText: this.hass!.localize("ui.common.delete"),
+ dismissText: this.hass!.localize("ui.common.cancel"),
+ destructive: true,
+ });
+
+ if (!confirmed) {
+ return;
+ }
+ await deleteSubEntry(
+ this.hass,
+ this.entry.entry_id,
+ this.subEntry.subentry_id
+ );
+ }
+
+ static styles = css`
+ .expand-button {
+ margin: 0 -12px;
+ }
+ ha-md-list {
+ border: 1px solid var(--divider-color);
+ border-radius: var(--ha-card-border-radius, 12px);
+ padding: 0;
+ margin: 16px;
+ margin-top: 0;
+ }
+ ha-md-list-item.has-subentries {
+ border-bottom: 1px solid var(--divider-color);
+ }
+ `;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-config-sub-entry-row": HaConfigSubEntryRow;
+ }
+}
diff --git a/src/panels/config/integrations/show-pick-config-entry-dialog.ts b/src/panels/config/integrations/show-pick-config-entry-dialog.ts
new file mode 100644
index 0000000000..0bd3fbd604
--- /dev/null
+++ b/src/panels/config/integrations/show-pick-config-entry-dialog.ts
@@ -0,0 +1,20 @@
+import { fireEvent } from "../../../common/dom/fire_event";
+import type { ConfigEntry } from "../../../data/config_entries";
+
+export interface PickConfigEntryDialogParams {
+ domain: string;
+ subFlowType: string;
+ configEntries: ConfigEntry[];
+ configEntryPicked: (configEntry: ConfigEntry) => void;
+}
+
+export const showPickConfigEntryDialog = (
+ element: HTMLElement,
+ dialogParams?: PickConfigEntryDialogParams
+): void => {
+ fireEvent(element, "show-dialog", {
+ dialogTag: "dialog-pick-config-entry",
+ dialogImport: () => import("./dialog-pick-config-entry"),
+ dialogParams: dialogParams,
+ });
+};
diff --git a/src/resources/theme/color.globals.ts b/src/resources/theme/color.globals.ts
index e80932117e..56ceb72acb 100644
--- a/src/resources/theme/color.globals.ts
+++ b/src/resources/theme/color.globals.ts
@@ -31,6 +31,11 @@ export const colorStyles = css`
--rgb-text-primary-color: 255, 255, 255;
--rgb-card-background-color: 255, 255, 255;
+ --rgb-warning-color: 255, 166, 0;
+ --rgb-error-color: 219, 68, 55;
+ --rgb-success-color: 67, 160, 71;
+ --rgb-info-color: 3, 155, 229;
+
--scrollbar-thumb-color: rgb(194, 194, 194);
--error-color: #db4437;
diff --git a/src/translations/en.json b/src/translations/en.json
index 028dd1a6f6..3632d746f5 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -5319,11 +5319,18 @@
"dismiss": "Keep",
"learn_more": "Learn more about application credentials"
},
+ "device": {
+ "enable": "Enable device",
+ "disable": "Disable device",
+ "configure": "Configure device",
+ "delete": "Remove device"
+ },
"devices": "{count} {count, plural,\n one {device}\n other {devices}\n}",
"entities": "{count} {count, plural,\n one {entity}\n other {entities}\n}",
"services": "{count} {count, plural,\n one {service}\n other {services}\n}",
"entries": "{count} {count, plural,\n one {entry}\n other {entries}\n}",
"no_devices_or_entities": "No devices or entities",
+ "devices_without_subentry": "Devices that don't belong to a sub-entry",
"rename": "Rename",
"configure": "Configure",
"system_options": "System options",