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 { ConfigEntry } from "./config_entries";
export type FlowType = "config_flow" | "options_flow" | "repair_flow";
export interface DataEntryFlowProgressedEvent {
type: "data_entry_flow_progressed";
data: {
@ -30,6 +32,7 @@ export interface DataEntryFlowStepForm {
errors: Record<string, string>;
description_placeholders?: Record<string, string>;
last_step: boolean | null;
preview?: string;
}
export interface DataEntryFlowStepExternal {

View File

@ -1,8 +1,10 @@
import {
HassEntityAttributeBase,
HassEntityBase,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
import { computeDomain } from "../common/entity/compute_domain";
import { HomeAssistant } from "../types";
interface GroupEntityAttributes extends HassEntityAttributeBase {
entity_id: string[];
@ -24,3 +26,20 @@ export const computeGroupDomain = (
];
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">
): void =>
showFlowDialog(element, dialogParams, {
flowType: "config_flow",
loadDevicesAndAreas: true,
createFlow: async (hass, handler) => {
const [step] = await Promise.all([

View File

@ -9,11 +9,14 @@ import {
DataEntryFlowStepForm,
DataEntryFlowStepMenu,
DataEntryFlowStepProgress,
FlowType,
} from "../../data/data_entry_flow";
import type { IntegrationManifest } from "../../data/integration";
import type { HomeAssistant } from "../../types";
export interface FlowConfig {
flowType: FlowType;
loadDevicesAndAreas: boolean;
createFlow(hass: HomeAssistant, handler: string): Promise<DataEntryFlowStep>;

View File

@ -27,6 +27,7 @@ export const showOptionsFlowDialog = (
manifest,
},
{
flowType: "options_flow",
loadDevicesAndAreas: false,
createFlow: async (hass, handler) => {
const [step] = await Promise.all([

View File

@ -1,15 +1,18 @@
import "@material/mwc-button";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import "@material/mwc-button";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event";
import { isNavigationClick } from "../../common/dom/is-navigation-click";
import "../../components/ha-alert";
import "../../components/ha-circular-progress";
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 { FlowConfig } from "./show-dialog-data-entry-flow";
import { configFlowContentStyles } from "./styles";
import { isNavigationClick } from "../../common/dom/is-navigation-click";
import { haStyle } from "../../resources/styles";
@customElement("step-flow-form")
class StepFlowForm extends LitElement {
@ -66,6 +69,23 @@ class StepFlowForm extends LitElement {
.localizeValue=${this._localizeValueCallback}
></ha-form>
</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">
${this._loading
? html`
@ -93,6 +113,13 @@ class StepFlowForm extends LitElement {
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) {
if (isNavigationClick(ev, false)) {
fireEvent(this, "flow-update", {
@ -199,6 +226,7 @@ class StepFlowForm extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyle,
configFlowContentStyles,
css`
.error {

View File

@ -23,7 +23,8 @@ export const configFlowContentStyles = css`
box-sizing: border-box;
}
.content {
.content,
.preview {
margin-top: 20px;
padding: 0 24px;
}

View File

@ -27,6 +27,7 @@ export const showRepairsFlowDialog = (
dialogClosedCallback,
},
{
flowType: "repair_flow",
loadDevicesAndAreas: false,
createFlow: async (hass, handler) => {
const [step] = await Promise.all([

View File

@ -3572,6 +3572,7 @@
"finish": "Finish",
"submit": "Submit",
"next": "Next",
"preview": "Preview",
"found_following_devices": "We found the following devices",
"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.",