mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-27 03:06:41 +00:00
Add support for previews in data flows (#17533)
This commit is contained in:
parent
2483249b5f
commit
811edfcc0f
@ -2,6 +2,8 @@ import { Connection } from "home-assistant-js-websocket";
|
|||||||
import type { HaFormSchema } from "../components/ha-form/types";
|
import type { HaFormSchema } from "../components/ha-form/types";
|
||||||
import { ConfigEntry } from "./config_entries";
|
import { ConfigEntry } from "./config_entries";
|
||||||
|
|
||||||
|
export type FlowType = "config_flow" | "options_flow" | "repair_flow";
|
||||||
|
|
||||||
export interface DataEntryFlowProgressedEvent {
|
export interface DataEntryFlowProgressedEvent {
|
||||||
type: "data_entry_flow_progressed";
|
type: "data_entry_flow_progressed";
|
||||||
data: {
|
data: {
|
||||||
@ -30,6 +32,7 @@ export interface DataEntryFlowStepForm {
|
|||||||
errors: Record<string, string>;
|
errors: Record<string, string>;
|
||||||
description_placeholders?: Record<string, string>;
|
description_placeholders?: Record<string, string>;
|
||||||
last_step: boolean | null;
|
last_step: boolean | null;
|
||||||
|
preview?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataEntryFlowStepExternal {
|
export interface DataEntryFlowStepExternal {
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
HassEntityAttributeBase,
|
HassEntityAttributeBase,
|
||||||
HassEntityBase,
|
HassEntityBase,
|
||||||
|
UnsubscribeFunc,
|
||||||
} from "home-assistant-js-websocket";
|
} from "home-assistant-js-websocket";
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
interface GroupEntityAttributes extends HassEntityAttributeBase {
|
interface GroupEntityAttributes extends HassEntityAttributeBase {
|
||||||
entity_id: string[];
|
entity_id: string[];
|
||||||
@ -24,3 +26,20 @@ export const computeGroupDomain = (
|
|||||||
];
|
];
|
||||||
return uniqueDomains.length === 1 ? uniqueDomains[0] : undefined;
|
return uniqueDomains.length === 1 ? uniqueDomains[0] : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const subscribePreviewGroupSensor = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
flow_id: string,
|
||||||
|
flow_type: "config_flow" | "options_flow",
|
||||||
|
user_input: Record<string, any>,
|
||||||
|
callback: (preview: {
|
||||||
|
state: string;
|
||||||
|
attributes: Record<string, any>;
|
||||||
|
}) => void
|
||||||
|
): Promise<UnsubscribeFunc> =>
|
||||||
|
hass.connection.subscribeMessage(callback, {
|
||||||
|
type: "group/sensor/start_preview",
|
||||||
|
flow_id,
|
||||||
|
flow_type,
|
||||||
|
user_input,
|
||||||
|
});
|
||||||
|
71
src/dialogs/config-flow/previews/entity-preview-row.ts
Normal file
71
src/dialogs/config-flow/previews/entity-preview-row.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
|
||||||
|
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||||
|
import { isUnavailableState } from "../../../data/entity";
|
||||||
|
import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
|
||||||
|
@customElement("entity-preview-row")
|
||||||
|
class EntityPreviewRow extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private stateObj?: HassEntity;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this.stateObj) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
const stateObj = this.stateObj;
|
||||||
|
return html`<state-badge
|
||||||
|
.hass=${this.hass}
|
||||||
|
.stateObj=${stateObj}
|
||||||
|
></state-badge>
|
||||||
|
<div class="name" .title=${computeStateName(stateObj)}>
|
||||||
|
${computeStateName(stateObj)}
|
||||||
|
</div>
|
||||||
|
<div class="value">
|
||||||
|
${stateObj.attributes.device_class === SENSOR_DEVICE_CLASS_TIMESTAMP &&
|
||||||
|
!isUnavailableState(stateObj.state)
|
||||||
|
? html`
|
||||||
|
<hui-timestamp-display
|
||||||
|
.hass=${this.hass}
|
||||||
|
.ts=${new Date(stateObj.state)}
|
||||||
|
capitalize
|
||||||
|
></hui-timestamp-display>
|
||||||
|
`
|
||||||
|
: computeStateDisplay(
|
||||||
|
this.hass!.localize,
|
||||||
|
stateObj,
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config,
|
||||||
|
this.hass.entities
|
||||||
|
)}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
margin-left: 16px;
|
||||||
|
margin-right: 8px;
|
||||||
|
flex: 1 1 30%;
|
||||||
|
}
|
||||||
|
.value {
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"entity-preview-row": EntityPreviewRow;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
|
import { LitElement, html } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { FlowType } from "../../../data/data_entry_flow";
|
||||||
|
import { subscribePreviewGroupSensor } from "../../../data/group";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import "./entity-preview-row";
|
||||||
|
|
||||||
|
@customElement("flow-preview-group_sensor")
|
||||||
|
class FlowPreviewGroupSensor extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public flowType!: FlowType;
|
||||||
|
|
||||||
|
public handler!: string;
|
||||||
|
|
||||||
|
public stepId!: string;
|
||||||
|
|
||||||
|
@property() public flowId!: string;
|
||||||
|
|
||||||
|
@property() public stepData!: Record<string, any>;
|
||||||
|
|
||||||
|
@state() private _preview?: HassEntity;
|
||||||
|
|
||||||
|
private _unsub?: Promise<UnsubscribeFunc>;
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
if (this._unsub) {
|
||||||
|
this._unsub.then((unsub) => unsub());
|
||||||
|
this._unsub = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
willUpdate(changedProps) {
|
||||||
|
if (changedProps.has("stepData")) {
|
||||||
|
this._subscribePreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`<entity-preview-row
|
||||||
|
.hass=${this.hass}
|
||||||
|
.stateObj=${this._preview}
|
||||||
|
></entity-preview-row>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setPreview = (preview: {
|
||||||
|
state: string;
|
||||||
|
attributes: Record<string, any>;
|
||||||
|
}) => {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
this._preview = {
|
||||||
|
entity_id: "sensor.flow_preview",
|
||||||
|
last_changed: now,
|
||||||
|
last_updated: now,
|
||||||
|
context: { id: "", parent_id: null, user_id: null },
|
||||||
|
...preview,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
private async _subscribePreview() {
|
||||||
|
if (this._unsub) {
|
||||||
|
(await this._unsub)();
|
||||||
|
this._unsub = undefined;
|
||||||
|
}
|
||||||
|
if (this.flowType === "repair_flow") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.stepData.type) {
|
||||||
|
this._preview = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this._unsub = subscribePreviewGroupSensor(
|
||||||
|
this.hass,
|
||||||
|
this.flowId,
|
||||||
|
this.flowType,
|
||||||
|
this.stepData,
|
||||||
|
this._setPreview
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
this._preview = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"flow-preview-group_sensor": FlowPreviewGroupSensor;
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,7 @@ export const showConfigFlowDialog = (
|
|||||||
dialogParams: Omit<DataEntryFlowDialogParams, "flowConfig">
|
dialogParams: Omit<DataEntryFlowDialogParams, "flowConfig">
|
||||||
): void =>
|
): void =>
|
||||||
showFlowDialog(element, dialogParams, {
|
showFlowDialog(element, dialogParams, {
|
||||||
|
flowType: "config_flow",
|
||||||
loadDevicesAndAreas: true,
|
loadDevicesAndAreas: true,
|
||||||
createFlow: async (hass, handler) => {
|
createFlow: async (hass, handler) => {
|
||||||
const [step] = await Promise.all([
|
const [step] = await Promise.all([
|
||||||
|
@ -9,11 +9,14 @@ import {
|
|||||||
DataEntryFlowStepForm,
|
DataEntryFlowStepForm,
|
||||||
DataEntryFlowStepMenu,
|
DataEntryFlowStepMenu,
|
||||||
DataEntryFlowStepProgress,
|
DataEntryFlowStepProgress,
|
||||||
|
FlowType,
|
||||||
} from "../../data/data_entry_flow";
|
} from "../../data/data_entry_flow";
|
||||||
import type { IntegrationManifest } from "../../data/integration";
|
import type { IntegrationManifest } from "../../data/integration";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
|
|
||||||
export interface FlowConfig {
|
export interface FlowConfig {
|
||||||
|
flowType: FlowType;
|
||||||
|
|
||||||
loadDevicesAndAreas: boolean;
|
loadDevicesAndAreas: boolean;
|
||||||
|
|
||||||
createFlow(hass: HomeAssistant, handler: string): Promise<DataEntryFlowStep>;
|
createFlow(hass: HomeAssistant, handler: string): Promise<DataEntryFlowStep>;
|
||||||
|
@ -27,6 +27,7 @@ export const showOptionsFlowDialog = (
|
|||||||
manifest,
|
manifest,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
flowType: "options_flow",
|
||||||
loadDevicesAndAreas: false,
|
loadDevicesAndAreas: false,
|
||||||
createFlow: async (hass, handler) => {
|
createFlow: async (hass, handler) => {
|
||||||
const [step] = await Promise.all([
|
const [step] = await Promise.all([
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
import "@material/mwc-button";
|
|
||||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||||
|
import "@material/mwc-button";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
html,
|
html,
|
||||||
LitElement,
|
LitElement,
|
||||||
|
nothing,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import { isNavigationClick } from "../../common/dom/is-navigation-click";
|
||||||
import "../../components/ha-alert";
|
import "../../components/ha-alert";
|
||||||
import "../../components/ha-circular-progress";
|
import "../../components/ha-circular-progress";
|
||||||
import { computeInitialHaFormData } from "../../components/ha-form/compute-initial-ha-form-data";
|
import { computeInitialHaFormData } from "../../components/ha-form/compute-initial-ha-form-data";
|
||||||
@ -21,7 +24,7 @@ import type { DataEntryFlowStepForm } from "../../data/data_entry_flow";
|
|||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import type { FlowConfig } from "./show-dialog-data-entry-flow";
|
import type { FlowConfig } from "./show-dialog-data-entry-flow";
|
||||||
import { configFlowContentStyles } from "./styles";
|
import { configFlowContentStyles } from "./styles";
|
||||||
import { isNavigationClick } from "../../common/dom/is-navigation-click";
|
import { haStyle } from "../../resources/styles";
|
||||||
|
|
||||||
@customElement("step-flow-form")
|
@customElement("step-flow-form")
|
||||||
class StepFlowForm extends LitElement {
|
class StepFlowForm extends LitElement {
|
||||||
@ -66,6 +69,23 @@ class StepFlowForm extends LitElement {
|
|||||||
.localizeValue=${this._localizeValueCallback}
|
.localizeValue=${this._localizeValueCallback}
|
||||||
></ha-form>
|
></ha-form>
|
||||||
</div>
|
</div>
|
||||||
|
${step.preview
|
||||||
|
? html`<div class="preview">
|
||||||
|
<h3>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_flow.preview"
|
||||||
|
)}:
|
||||||
|
</h3>
|
||||||
|
${dynamicElement(`flow-preview-${this.step.preview}`, {
|
||||||
|
hass: this.hass,
|
||||||
|
flowType: this.flowConfig.flowType,
|
||||||
|
handler: step.handler,
|
||||||
|
stepId: step.step_id,
|
||||||
|
flowId: step.flow_id,
|
||||||
|
stepData,
|
||||||
|
})}
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
${this._loading
|
${this._loading
|
||||||
? html`
|
? html`
|
||||||
@ -93,6 +113,13 @@ class StepFlowForm extends LitElement {
|
|||||||
this.addEventListener("keydown", this._handleKeyDown);
|
this.addEventListener("keydown", this._handleKeyDown);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
|
super.willUpdate(changedProps);
|
||||||
|
if (changedProps.has("step") && this.step?.preview) {
|
||||||
|
import(`./previews/flow-preview-${this.step.preview}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _clickHandler(ev: MouseEvent) {
|
private _clickHandler(ev: MouseEvent) {
|
||||||
if (isNavigationClick(ev, false)) {
|
if (isNavigationClick(ev, false)) {
|
||||||
fireEvent(this, "flow-update", {
|
fireEvent(this, "flow-update", {
|
||||||
@ -199,6 +226,7 @@ class StepFlowForm extends LitElement {
|
|||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
|
haStyle,
|
||||||
configFlowContentStyles,
|
configFlowContentStyles,
|
||||||
css`
|
css`
|
||||||
.error {
|
.error {
|
||||||
|
@ -23,7 +23,8 @@ export const configFlowContentStyles = css`
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content,
|
||||||
|
.preview {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ export const showRepairsFlowDialog = (
|
|||||||
dialogClosedCallback,
|
dialogClosedCallback,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
flowType: "repair_flow",
|
||||||
loadDevicesAndAreas: false,
|
loadDevicesAndAreas: false,
|
||||||
createFlow: async (hass, handler) => {
|
createFlow: async (hass, handler) => {
|
||||||
const [step] = await Promise.all([
|
const [step] = await Promise.all([
|
||||||
|
@ -3572,6 +3572,7 @@
|
|||||||
"finish": "Finish",
|
"finish": "Finish",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
|
"preview": "Preview",
|
||||||
"found_following_devices": "We found the following devices",
|
"found_following_devices": "We found the following devices",
|
||||||
"yaml_only_title": "This device cannot be added from the UI",
|
"yaml_only_title": "This device cannot be added from the UI",
|
||||||
"yaml_only": "You can add this device by adding it to your ''configuration.yaml''. See the documentation for more information.",
|
"yaml_only": "You can add this device by adding it to your ''configuration.yaml''. See the documentation for more information.",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user