Add base support for sub entries (#23160)
* Add base support for sub entries * add demo types * fix translations * Use sub entry name when deleting * Update show-dialog-sub-config-flow.ts * adjust for multiple sub types * WIP, not functional * add subentry_type * rename to supported_subentry_types * config_subentries -> config_entries_subentries * Add localized sub flow title * use Record * rename * more renamepull/23358/head^2
parent
03a415beff
commit
0d97afb3f2
|
@ -65,6 +65,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
|||
mockEntityRegistry(hass, [
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
config_subentry_id: null,
|
||||
device_id: "co2signal",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
|
@ -85,6 +86,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
|||
},
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
config_subentry_id: null,
|
||||
device_id: "co2signal",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
|
|
|
@ -11,6 +11,7 @@ export const mockConfigEntries = (hass: MockHomeAssistant) => {
|
|||
supports_remove_device: false,
|
||||
supports_unload: true,
|
||||
supports_reconfigure: true,
|
||||
supported_subentry_types: {},
|
||||
pref_disable_new_entities: false,
|
||||
pref_disable_polling: false,
|
||||
disabled_by: null,
|
||||
|
|
|
@ -48,6 +48,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
|||
area_id: "bedroom",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_1"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
|
@ -71,6 +72,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
|||
area_id: "backyard",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_2"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
|
@ -94,6 +96,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
|||
area_id: null,
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_3"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
|
|
|
@ -47,6 +47,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
|||
area_id: "bedroom",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_1"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
|
@ -70,6 +71,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
|||
area_id: "backyard",
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_2"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
|
@ -93,6 +95,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
|||
area_id: null,
|
||||
configuration_url: null,
|
||||
config_entries: ["config_entry_3"],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
disabled_by: null,
|
||||
entry_type: null,
|
||||
|
|
|
@ -32,6 +32,8 @@ const createConfigEntry = (
|
|||
supports_remove_device: false,
|
||||
supports_unload: true,
|
||||
supports_reconfigure: true,
|
||||
supported_subentry_types: {},
|
||||
num_subentries: 0,
|
||||
disabled_by: null,
|
||||
pref_disable_new_entities: false,
|
||||
pref_disable_polling: false,
|
||||
|
@ -188,6 +190,7 @@ const createEntityRegistryEntries = (
|
|||
): EntityRegistryEntry[] => [
|
||||
{
|
||||
config_entry_id: item.entry_id,
|
||||
config_subentry_id: null,
|
||||
device_id: "mock-device-id",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
|
@ -214,6 +217,7 @@ const createDeviceRegistryEntries = (
|
|||
{
|
||||
entry_type: null,
|
||||
config_entries: [item.entry_id],
|
||||
config_entries_subentries: {},
|
||||
connections: [],
|
||||
manufacturer: "ESPHome",
|
||||
model: "Mock Device",
|
||||
|
|
|
@ -19,6 +19,8 @@ export interface ConfigEntry {
|
|||
supports_remove_device: boolean;
|
||||
supports_unload: boolean;
|
||||
supports_reconfigure: boolean;
|
||||
supported_subentry_types: Record<string, { supports_reconfigure: boolean }>;
|
||||
num_subentries: number;
|
||||
pref_disable_new_entities: boolean;
|
||||
pref_disable_polling: boolean;
|
||||
disabled_by: "user" | null;
|
||||
|
@ -27,6 +29,30 @@ export interface ConfigEntry {
|
|||
error_reason_translation_placeholders: Record<string, string> | null;
|
||||
}
|
||||
|
||||
export interface SubEntry {
|
||||
subentry_id: string;
|
||||
subentry_type: string;
|
||||
title: string;
|
||||
unique_id: string;
|
||||
}
|
||||
|
||||
export const getSubEntries = (hass: HomeAssistant, entry_id: string) =>
|
||||
hass.callWS<SubEntry[]>({
|
||||
type: "config_entries/subentries/list",
|
||||
entry_id,
|
||||
});
|
||||
|
||||
export const deleteSubEntry = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
subentry_id: string
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "config_entries/subentries/delete",
|
||||
entry_id,
|
||||
subentry_id,
|
||||
});
|
||||
|
||||
export type ConfigEntryMutableParams = Partial<
|
||||
Pick<
|
||||
ConfigEntry,
|
||||
|
|
|
@ -2,7 +2,11 @@ import type { Connection } from "home-assistant-js-websocket";
|
|||
import type { HaFormSchema } from "../components/ha-form/types";
|
||||
import type { ConfigEntry } from "./config_entries";
|
||||
|
||||
export type FlowType = "config_flow" | "options_flow" | "repair_flow";
|
||||
export type FlowType =
|
||||
| "config_flow"
|
||||
| "config_subentries_flow"
|
||||
| "options_flow"
|
||||
| "repair_flow";
|
||||
|
||||
export interface DataEntryFlowProgressedEvent {
|
||||
type: "data_entry_flow_progressed";
|
||||
|
|
|
@ -17,6 +17,7 @@ export {
|
|||
export interface DeviceRegistryEntry extends RegistryEntry {
|
||||
id: string;
|
||||
config_entries: string[];
|
||||
config_entries_subentries: Record<string, (string | null)[]>;
|
||||
connections: [string, string][];
|
||||
identifiers: [string, string][];
|
||||
manufacturer: string | null;
|
||||
|
|
|
@ -50,6 +50,7 @@ export interface EntityRegistryEntry extends RegistryEntry {
|
|||
icon: string | null;
|
||||
platform: string;
|
||||
config_entry_id: string | null;
|
||||
config_subentry_id: string | null;
|
||||
device_id: string | null;
|
||||
area_id: string | null;
|
||||
labels: string[];
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import type { HomeAssistant } from "../types";
|
||||
import type { DataEntryFlowStep } from "./data_entry_flow";
|
||||
|
||||
const HEADERS = {
|
||||
"HA-Frontend-Base": `${location.protocol}//${location.host}`,
|
||||
};
|
||||
|
||||
export const createSubConfigFlow = (
|
||||
hass: HomeAssistant,
|
||||
configEntryId: string,
|
||||
subFlowType: string,
|
||||
subentry_id?: string
|
||||
) =>
|
||||
hass.callApi<DataEntryFlowStep>(
|
||||
"POST",
|
||||
"config/config_entries/subentries/flow",
|
||||
{
|
||||
handler: [configEntryId, subFlowType],
|
||||
show_advanced_options: Boolean(hass.userData?.showAdvanced),
|
||||
subentry_id,
|
||||
},
|
||||
HEADERS
|
||||
);
|
||||
|
||||
export const fetchSubConfigFlow = (hass: HomeAssistant, flowId: string) =>
|
||||
hass.callApi<DataEntryFlowStep>(
|
||||
"GET",
|
||||
`config/config_entries/subentries/flow/${flowId}`,
|
||||
undefined,
|
||||
HEADERS
|
||||
);
|
||||
|
||||
export const handleSubConfigFlowStep = (
|
||||
hass: HomeAssistant,
|
||||
flowId: string,
|
||||
data: Record<string, any>
|
||||
) =>
|
||||
hass.callApi<DataEntryFlowStep>(
|
||||
"POST",
|
||||
`config/config_entries/subentries/flow/${flowId}`,
|
||||
data,
|
||||
HEADERS
|
||||
);
|
||||
|
||||
export const deleteSubConfigFlow = (hass: HomeAssistant, flowId: string) =>
|
||||
hass.callApi("DELETE", `config/config_entries/subentries/flow/${flowId}`);
|
|
@ -63,6 +63,7 @@ export type TranslationCategory =
|
|||
| "entity_component"
|
||||
| "exceptions"
|
||||
| "config"
|
||||
| "config_subentries"
|
||||
| "config_panel"
|
||||
| "options"
|
||||
| "device_automation"
|
||||
|
|
|
@ -77,7 +77,7 @@ export class FlowPreviewGeneric extends LitElement {
|
|||
(await this._unsub)();
|
||||
this._unsub = undefined;
|
||||
}
|
||||
if (this.flowType === "repair_flow") {
|
||||
if (this.flowType !== "config_flow" && this.flowType !== "options_flow") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
|
|
@ -147,7 +147,7 @@ class FlowPreviewTemplate extends LitElement {
|
|||
(await this._unsub)();
|
||||
this._unsub = undefined;
|
||||
}
|
||||
if (this.flowType === "repair_flow") {
|
||||
if (this.flowType !== "config_flow" && this.flowType !== "options_flow") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
|
|
@ -16,7 +16,9 @@ export const loadConfigFlowDialog = loadDataEntryFlowDialog;
|
|||
|
||||
export const showConfigFlowDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: Omit<DataEntryFlowDialogParams, "flowConfig">
|
||||
dialogParams: Omit<DataEntryFlowDialogParams, "flowConfig"> & {
|
||||
entryId?: string;
|
||||
}
|
||||
): void =>
|
||||
showFlowDialog(element, dialogParams, {
|
||||
flowType: "config_flow",
|
||||
|
|
|
@ -148,7 +148,6 @@ export interface DataEntryFlowDialogParams {
|
|||
}) => void;
|
||||
flowConfig: FlowConfig;
|
||||
showAdvanced?: boolean;
|
||||
entryId?: string;
|
||||
dialogParentElement?: HTMLElement;
|
||||
navigateToResult?: boolean;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,275 @@
|
|||
import { html } from "lit";
|
||||
import type { ConfigEntry } from "../../data/config_entries";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import {
|
||||
createSubConfigFlow,
|
||||
deleteSubConfigFlow,
|
||||
fetchSubConfigFlow,
|
||||
handleSubConfigFlowStep,
|
||||
} from "../../data/sub_config_flow";
|
||||
import type { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow";
|
||||
import {
|
||||
loadDataEntryFlowDialog,
|
||||
showFlowDialog,
|
||||
} from "./show-dialog-data-entry-flow";
|
||||
|
||||
export const loadSubConfigFlowDialog = loadDataEntryFlowDialog;
|
||||
|
||||
export const showSubConfigFlowDialog = (
|
||||
element: HTMLElement,
|
||||
configEntry: ConfigEntry,
|
||||
flowType: string,
|
||||
dialogParams: Omit<DataEntryFlowDialogParams, "flowConfig"> & {
|
||||
subEntryId?: string;
|
||||
}
|
||||
): void =>
|
||||
showFlowDialog(element, dialogParams, {
|
||||
flowType: "config_subentries_flow",
|
||||
showDevices: true,
|
||||
createFlow: async (hass, handler) => {
|
||||
const [step] = await Promise.all([
|
||||
createSubConfigFlow(hass, handler, flowType, dialogParams.subEntryId),
|
||||
hass.loadFragmentTranslation("config"),
|
||||
hass.loadBackendTranslation("config_subentries", configEntry.domain),
|
||||
hass.loadBackendTranslation("selector", configEntry.domain),
|
||||
// Used as fallback if no header defined for step
|
||||
hass.loadBackendTranslation("title", configEntry.domain),
|
||||
]);
|
||||
return step;
|
||||
},
|
||||
fetchFlow: async (hass, flowId) => {
|
||||
const step = await fetchSubConfigFlow(hass, flowId);
|
||||
await hass.loadFragmentTranslation("config");
|
||||
await hass.loadBackendTranslation(
|
||||
"config_subentries",
|
||||
configEntry.domain
|
||||
);
|
||||
await hass.loadBackendTranslation("selector", configEntry.domain);
|
||||
return step;
|
||||
},
|
||||
handleFlowStep: handleSubConfigFlowStep,
|
||||
deleteFlow: deleteSubConfigFlow,
|
||||
|
||||
renderAbortDescription(hass, step) {
|
||||
const description = hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.abort.${step.reason}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
|
||||
return description
|
||||
? html`
|
||||
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
|
||||
`
|
||||
: step.reason;
|
||||
},
|
||||
|
||||
renderShowFormStepHeader(hass, step) {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.title`,
|
||||
step.description_placeholders
|
||||
) || hass.localize(`component.${configEntry.domain}.title`)
|
||||
);
|
||||
},
|
||||
|
||||
renderShowFormStepDescription(hass, step) {
|
||||
const description = hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.description`,
|
||||
step.description_placeholders
|
||||
);
|
||||
return description
|
||||
? html`
|
||||
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
|
||||
`
|
||||
: "";
|
||||
},
|
||||
|
||||
renderShowFormStepFieldLabel(hass, step, field, options) {
|
||||
if (field.type === "expandable") {
|
||||
return hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.sections.${field.name}.name`
|
||||
);
|
||||
}
|
||||
|
||||
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
|
||||
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.${prefix}data.${field.name}`
|
||||
) || field.name
|
||||
);
|
||||
},
|
||||
|
||||
renderShowFormStepFieldHelper(hass, step, field, options) {
|
||||
if (field.type === "expandable") {
|
||||
return hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.sections.${field.name}.description`
|
||||
);
|
||||
}
|
||||
|
||||
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
|
||||
|
||||
const description = hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.${prefix}data_description.${field.name}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
|
||||
return description
|
||||
? html`<ha-markdown breaks .content=${description}></ha-markdown>`
|
||||
: "";
|
||||
},
|
||||
|
||||
renderShowFormStepFieldError(hass, step, error) {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${step.translation_domain || step.translation_domain || configEntry.domain}.config_subentries.${flowType}.error.${error}`,
|
||||
step.description_placeholders
|
||||
) || error
|
||||
);
|
||||
},
|
||||
|
||||
renderShowFormStepFieldLocalizeValue(hass, _step, key) {
|
||||
return hass.localize(`component.${configEntry.domain}.selector.${key}`);
|
||||
},
|
||||
|
||||
renderShowFormStepSubmitButton(hass, step) {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.submit`
|
||||
) ||
|
||||
hass.localize(
|
||||
`ui.panel.config.integrations.config_flow.${
|
||||
step.last_step === false ? "next" : "submit"
|
||||
}`
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
renderExternalStepHeader(hass, step) {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.title`
|
||||
) ||
|
||||
hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.external_step.open_site"
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
renderExternalStepDescription(hass, step) {
|
||||
const description = hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.description`,
|
||||
step.description_placeholders
|
||||
);
|
||||
|
||||
return html`
|
||||
<p>
|
||||
${hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.external_step.description"
|
||||
)}
|
||||
</p>
|
||||
${description
|
||||
? html`
|
||||
<ha-markdown
|
||||
allowsvg
|
||||
breaks
|
||||
.content=${description}
|
||||
></ha-markdown>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
},
|
||||
|
||||
renderCreateEntryDescription(hass, step) {
|
||||
const description = hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.create_entry.${
|
||||
step.description || "default"
|
||||
}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
|
||||
return html`
|
||||
${description
|
||||
? html`
|
||||
<ha-markdown
|
||||
allowsvg
|
||||
breaks
|
||||
.content=${description}
|
||||
></ha-markdown>
|
||||
`
|
||||
: ""}
|
||||
<p>
|
||||
${hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.created_config",
|
||||
{ name: step.title }
|
||||
)}
|
||||
</p>
|
||||
`;
|
||||
},
|
||||
|
||||
renderShowFormProgressHeader(hass, step) {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.title`
|
||||
) || hass.localize(`component.${configEntry.domain}.title`)
|
||||
);
|
||||
},
|
||||
|
||||
renderShowFormProgressDescription(hass, step) {
|
||||
const description = hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.progress.${step.progress_action}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
return description
|
||||
? html`
|
||||
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
|
||||
`
|
||||
: "";
|
||||
},
|
||||
|
||||
renderMenuHeader(hass, step) {
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.title`,
|
||||
step.description_placeholders
|
||||
) || hass.localize(`component.${configEntry.domain}.title`)
|
||||
);
|
||||
},
|
||||
|
||||
renderMenuDescription(hass, step) {
|
||||
const description = hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.description`,
|
||||
step.description_placeholders
|
||||
);
|
||||
return description
|
||||
? html`
|
||||
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
|
||||
`
|
||||
: "";
|
||||
},
|
||||
|
||||
renderMenuOption(hass, step, option) {
|
||||
return hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.menu_options.${option}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
},
|
||||
|
||||
renderLoadingDescription(hass, reason, handler, step) {
|
||||
if (reason !== "loading_flow" && reason !== "loading_step") {
|
||||
return "";
|
||||
}
|
||||
const domain = step?.handler || handler;
|
||||
return hass.localize(
|
||||
`ui.panel.config.integrations.config_flow.loading.${reason}`,
|
||||
{
|
||||
integration: domain
|
||||
? domainToName(hass.localize, domain)
|
||||
: // when we are continuing a config flow, we only know the ID and not the domain
|
||||
hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.loading.fallback_title"
|
||||
),
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
|
@ -51,8 +51,8 @@ import "../../../components/ha-icon-button";
|
|||
import "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { ConfigEntry } from "../../../data/config_entries";
|
||||
import { sortConfigEntries } from "../../../data/config_entries";
|
||||
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
|
||||
import { getSubEntries, sortConfigEntries } from "../../../data/config_entries";
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
import type { DataTableFilters } from "../../../data/data_table_filters";
|
||||
import {
|
||||
|
@ -108,6 +108,8 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||
|
||||
@property({ attribute: false }) public entries!: ConfigEntry[];
|
||||
|
||||
@state() private _subEntries?: SubEntry[];
|
||||
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
entities!: EntityRegistryEntry[];
|
||||
|
@ -219,6 +221,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||
private _setFiltersFromUrl() {
|
||||
const domain = this._searchParms.get("domain");
|
||||
const configEntry = this._searchParms.get("config_entry");
|
||||
const subEntry = this._searchParms.get("sub_entry");
|
||||
const label = this._searchParms.has("label");
|
||||
|
||||
if (!domain && !configEntry && !label) {
|
||||
|
@ -243,6 +246,10 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||
value: configEntry ? [configEntry] : [],
|
||||
items: undefined,
|
||||
},
|
||||
sub_entry: {
|
||||
value: subEntry ? [subEntry] : [],
|
||||
items: undefined,
|
||||
},
|
||||
};
|
||||
this._filterLabel();
|
||||
}
|
||||
|
@ -334,6 +341,32 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||
if (configEntries.length === 1) {
|
||||
filteredConfigEntry = configEntries[0];
|
||||
}
|
||||
} else if (
|
||||
key === "sub_entry" &&
|
||||
Array.isArray(filter.value) &&
|
||||
filter.value.length
|
||||
) {
|
||||
if (
|
||||
!(
|
||||
Array.isArray(this._filters.config_entry?.value) &&
|
||||
this._filters.config_entry.value.length === 1
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const configEntryId = this._filters.config_entry.value[0];
|
||||
outputDevices = outputDevices.filter(
|
||||
(device) =>
|
||||
device.config_entries_subentries[configEntryId] &&
|
||||
(filter.value as string[]).some((subEntryId) =>
|
||||
device.config_entries_subentries[configEntryId].includes(
|
||||
subEntryId
|
||||
)
|
||||
)
|
||||
);
|
||||
if (!this._subEntries) {
|
||||
this._loadSubEntries(configEntryId);
|
||||
}
|
||||
} else if (
|
||||
key === "ha-filter-integrations" &&
|
||||
Array.isArray(filter.value) &&
|
||||
|
@ -755,7 +788,15 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||
${this.entries?.find(
|
||||
(entry) =>
|
||||
entry.entry_id === this._filters.config_entry!.value![0]
|
||||
)?.title || this._filters.config_entry.value[0]}
|
||||
)?.title || this._filters.config_entry.value[0]}${this._filters
|
||||
.config_entry.value.length === 1 &&
|
||||
Array.isArray(this._filters.sub_entry?.value) &&
|
||||
this._filters.sub_entry.value.length
|
||||
? html` (${this._subEntries?.find(
|
||||
(entry) =>
|
||||
entry.subentry_id === this._filters.sub_entry!.value![0]
|
||||
)?.title || this._filters.sub_entry!.value![0]})`
|
||||
: nothing}
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
<ha-filter-floor-areas
|
||||
|
@ -888,6 +929,10 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||
`;
|
||||
}
|
||||
|
||||
private async _loadSubEntries(entryId: string) {
|
||||
this._subEntries = await getSubEntries(this.hass, entryId);
|
||||
}
|
||||
|
||||
private _filterExpanded(ev) {
|
||||
if (ev.detail.expanded) {
|
||||
this._expandedFilter = ev.target.localName;
|
||||
|
|
|
@ -66,8 +66,8 @@ import "../../../components/ha-icon-button";
|
|||
import "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { ConfigEntry } from "../../../data/config_entries";
|
||||
import { getConfigEntries } from "../../../data/config_entries";
|
||||
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
|
||||
import { getConfigEntries, getSubEntries } from "../../../data/config_entries";
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
import type {
|
||||
DataTableFiltersItems,
|
||||
|
@ -146,6 +146,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||
|
||||
@state() private _entries?: ConfigEntry[];
|
||||
|
||||
@state() private _subEntries?: SubEntry[];
|
||||
|
||||
@state() private _manifests?: IntegrationManifest[];
|
||||
|
||||
@state()
|
||||
|
@ -522,6 +524,27 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||
if (configEntries.length === 1) {
|
||||
filteredConfigEntry = configEntries[0];
|
||||
}
|
||||
} else if (
|
||||
key === "sub_entry" &&
|
||||
Array.isArray(filter) &&
|
||||
filter.length
|
||||
) {
|
||||
if (
|
||||
!(
|
||||
Array.isArray(this._filters.config_entry) &&
|
||||
this._filters.config_entry.length === 1
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
filteredEntities = filteredEntities.filter(
|
||||
(entity) =>
|
||||
entity.config_subentry_id &&
|
||||
(filter as string[]).includes(entity.config_subentry_id)
|
||||
);
|
||||
if (!this._subEntries) {
|
||||
this._loadSubEntries(this._filters.config_entry[0]);
|
||||
}
|
||||
} else if (
|
||||
key === "ha-filter-integrations" &&
|
||||
Array.isArray(filter) &&
|
||||
|
@ -904,14 +927,22 @@ ${
|
|||
</ha-md-button-menu>
|
||||
${
|
||||
Array.isArray(this._filters.config_entry) &&
|
||||
this._filters.config_entry?.length
|
||||
this._filters.config_entry.length
|
||||
? html`<ha-alert slot="filter-pane">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.filtering_by_config_entry"
|
||||
)}
|
||||
${this._entries?.find(
|
||||
(entry) => entry.entry_id === this._filters.config_entry![0]
|
||||
)?.title || this._filters.config_entry[0]}
|
||||
)?.title || this._filters.config_entry[0]}${this._filters
|
||||
.config_entry.length === 1 &&
|
||||
Array.isArray(this._filters.sub_entry) &&
|
||||
this._filters.sub_entry.length
|
||||
? html` (${this._subEntries?.find(
|
||||
(entry) =>
|
||||
entry.subentry_id === this._filters.sub_entry![0]
|
||||
)?.title || this._filters.sub_entry[0]})`
|
||||
: nothing}
|
||||
</ha-alert>`
|
||||
: nothing
|
||||
}
|
||||
|
@ -1024,6 +1055,7 @@ ${
|
|||
private _setFiltersFromUrl() {
|
||||
const domain = this._searchParms.get("domain");
|
||||
const configEntry = this._searchParms.get("config_entry");
|
||||
const subEntry = this._searchParms.get("sub_entry");
|
||||
const label = this._searchParms.has("label");
|
||||
|
||||
if (!domain && !configEntry && !label) {
|
||||
|
@ -1036,6 +1068,7 @@ ${
|
|||
"ha-filter-states": [],
|
||||
"ha-filter-integrations": domain ? [domain] : [],
|
||||
config_entry: configEntry ? [configEntry] : [],
|
||||
sub_entry: subEntry ? [subEntry] : [],
|
||||
};
|
||||
this._filterLabel();
|
||||
}
|
||||
|
@ -1093,6 +1126,7 @@ ${
|
|||
hidden_by: null,
|
||||
area_id: null,
|
||||
config_entry_id: null,
|
||||
config_subentry_id: null,
|
||||
device_id: null,
|
||||
icon: null,
|
||||
readonly: true,
|
||||
|
@ -1384,6 +1418,10 @@ ${rejected
|
|||
this._entries = await getConfigEntries(this.hass);
|
||||
}
|
||||
|
||||
private async _loadSubEntries(entryId: string) {
|
||||
this._subEntries = await getSubEntries(this.hass, entryId);
|
||||
}
|
||||
|
||||
private _addDevice() {
|
||||
const { filteredConfigEntry, filteredDomains } =
|
||||
this._filteredEntitiesAndDomains(
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
mdiOpenInNew,
|
||||
mdiPackageVariant,
|
||||
mdiPlayCircleOutline,
|
||||
mdiPlus,
|
||||
mdiProgressHelper,
|
||||
mdiReload,
|
||||
mdiReloadAlert,
|
||||
|
@ -52,14 +53,17 @@ import { getSignedPath } from "../../../data/auth";
|
|||
import type {
|
||||
ConfigEntry,
|
||||
DisableConfigEntryResult,
|
||||
SubEntry,
|
||||
} from "../../../data/config_entries";
|
||||
import {
|
||||
ERROR_STATES,
|
||||
RECOVERABLE_STATES,
|
||||
deleteConfigEntry,
|
||||
deleteSubEntry,
|
||||
disableConfigEntry,
|
||||
enableConfigEntry,
|
||||
getConfigEntries,
|
||||
getSubEntries,
|
||||
reloadConfigEntry,
|
||||
updateConfigEntry,
|
||||
} from "../../../data/config_entries";
|
||||
|
@ -106,6 +110,7 @@ import { fileDownload } from "../../../util/file_download";
|
|||
import type { DataEntryFlowProgressExtended } from "./ha-config-integrations";
|
||||
import { showAddIntegrationDialog } from "./show-add-integration-dialog";
|
||||
import { QUALITY_SCALE_MAP } from "../../../data/integration_quality_scale";
|
||||
import { showSubConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-sub-config-flow";
|
||||
|
||||
export const renderConfigEntryError = (
|
||||
hass: HomeAssistant,
|
||||
|
@ -172,6 +177,8 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||
|
||||
@state() private _domainEntities: Record<string, string[]> = {};
|
||||
|
||||
@state() private _subEntries: Record<string, SubEntry[]> = {};
|
||||
|
||||
private _configPanel = memoizeOne(
|
||||
(domain: string, panels: HomeAssistant["panels"]): string | undefined =>
|
||||
Object.values(panels).find(
|
||||
|
@ -214,11 +221,18 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (changedProperties.has("domain")) {
|
||||
this.hass.loadBackendTranslation("title", [this.domain]);
|
||||
this.hass.loadBackendTranslation("config_subentries", [this.domain]);
|
||||
this._extraConfigEntries = undefined;
|
||||
this._fetchManifest();
|
||||
this._fetchDiagnostics();
|
||||
this._fetchEntitySources();
|
||||
}
|
||||
if (
|
||||
changedProperties.has("configEntries") ||
|
||||
changedProperties.has("_extraConfigEntries")
|
||||
) {
|
||||
this._fetchSubEntries();
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchEntitySources() {
|
||||
|
@ -673,6 +687,73 @@ 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`<a href=${url}
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.${localizeKey}`,
|
||||
{ count: items.length }
|
||||
)}</a
|
||||
>`
|
||||
);
|
||||
}
|
||||
|
||||
if (entities.length) {
|
||||
devicesLine.push(
|
||||
// no white space before/after template on purpose
|
||||
html`<a
|
||||
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}${subItem ? `&sub_entry=${subItem.subentry_id}` : ""}`}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.entities",
|
||||
{ count: entities.length }
|
||||
)}</a
|
||||
>`
|
||||
);
|
||||
}
|
||||
|
||||
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<typeof this.hass.localize> | undefined;
|
||||
let stateTextExtra: TemplateResult | string | undefined;
|
||||
|
@ -720,66 +801,13 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||
)}.`);
|
||||
}
|
||||
} else {
|
||||
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}`;
|
||||
devicesLine.push(
|
||||
// no white space before/after template on purpose
|
||||
html`<a href=${url}
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.${localizeKey}`,
|
||||
{ count: items.length }
|
||||
)}</a
|
||||
>`
|
||||
);
|
||||
}
|
||||
|
||||
if (entities.length) {
|
||||
devicesLine.push(
|
||||
// no white space before/after template on purpose
|
||||
html`<a
|
||||
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.entities",
|
||||
{ count: entities.length }
|
||||
)}</a
|
||||
>`
|
||||
);
|
||||
}
|
||||
|
||||
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],
|
||||
];
|
||||
}
|
||||
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`<ha-md-list-item
|
||||
class=${classMap({
|
||||
config_entry: true,
|
||||
|
@ -913,6 +941,21 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||
)}
|
||||
</ha-md-menu-item>
|
||||
|
||||
${Object.keys(item.supported_subentry_types).map(
|
||||
(flowType) =>
|
||||
html`<ha-md-menu-item
|
||||
@click=${this._addSubEntry}
|
||||
.entry=${item}
|
||||
.flowType=${flowType}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`component.${item.domain}.config_subentries.${flowType}.title`
|
||||
)}</ha-md-menu-item
|
||||
>`
|
||||
)}
|
||||
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||
|
||||
${this._diagnosticHandler && item.state === "loaded"
|
||||
|
@ -989,6 +1032,69 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||
`
|
||||
: nothing}
|
||||
</ha-md-button-menu>
|
||||
</ha-md-list-item>
|
||||
${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`<ha-md-list-item
|
||||
class="sub-entry"
|
||||
data-entry-id=${configEntry.entry_id}
|
||||
.configEntry=${configEntry}
|
||||
.subEntry=${subEntry}
|
||||
>
|
||||
<span slot="headline">${subEntry.title}</span>
|
||||
<span slot="supporting-text"
|
||||
>${this._renderDeviceLine(
|
||||
configEntry,
|
||||
devices,
|
||||
services,
|
||||
entities,
|
||||
subEntry
|
||||
)}</span
|
||||
>
|
||||
${configEntry.supported_subentry_types[subEntry.subentry_type]
|
||||
?.supports_reconfigure
|
||||
? html`
|
||||
<ha-button slot="end" @click=${this._handleReconfigureSub}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.configure"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
<ha-md-button-menu positioning="popover" slot="end">
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-md-menu-item class="warning" @click=${this._handleDeleteSub}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
class="warning"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.delete"
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
</ha-md-list-item>`;
|
||||
}
|
||||
|
||||
|
@ -1031,6 +1137,27 @@ 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;
|
||||
|
@ -1178,6 +1305,49 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||
);
|
||||
}
|
||||
|
||||
private async _handleReconfigureSub(ev: Event): Promise<void> {
|
||||
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<void> {
|
||||
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
|
||||
|
@ -1456,6 +1626,12 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||
});
|
||||
}
|
||||
|
||||
private async _addSubEntry(ev) {
|
||||
showSubConfigFlowDialog(this, ev.target.entry, ev.target.flowType, {
|
||||
startFlowHandler: ev.target.entry.entry_id,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@ -1585,6 +1761,9 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||
pointer-events: none;
|
||||
content: "";
|
||||
}
|
||||
ha-md-list-item.sub-entry {
|
||||
--md-list-item-leading-space: 50px;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
|
|
@ -207,6 +207,8 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
|||
supports_remove_device: false,
|
||||
supports_unload: false,
|
||||
supports_reconfigure: false,
|
||||
supported_subentry_types: {},
|
||||
num_subentries: 0,
|
||||
pref_disable_new_entities: false,
|
||||
pref_disable_polling: false,
|
||||
disabled_by: null,
|
||||
|
|
Loading…
Reference in New Issue