Add support for previews in data flows (#17533)

This commit is contained in:
Bram Kragten 2023-08-22 10:30:31 +02:00 committed by GitHub
parent 2483249b5f
commit 811edfcc0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 224 additions and 3 deletions

View File

@ -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 {

View File

@ -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,
});

View 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;
}
}

View File

@ -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;
}
}

View File

@ -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([

View File

@ -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>;

View File

@ -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([

View File

@ -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 {

View File

@ -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;
} }

View File

@ -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([

View File

@ -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.",