mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-09 02:49:51 +00:00
Refactoring automation trace graphs (#8763)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com> Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
@@ -7,13 +7,47 @@ import {
|
||||
css,
|
||||
} from "lit-element";
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../ha-svg-icon";
|
||||
import { AutomationTraceExtended } from "../../data/trace";
|
||||
import { bfsIterateTreeNodes, NodeInfo, TreeNode } from "./hat-graph";
|
||||
import { ActionHandler } from "./script-to-graph";
|
||||
import { mdiChevronDown, mdiChevronUp } from "@mdi/js";
|
||||
import {
|
||||
AutomationTraceExtended,
|
||||
ChooseActionTraceStep,
|
||||
ConditionTraceStep,
|
||||
} from "../../data/trace";
|
||||
import {
|
||||
mdiAbTesting,
|
||||
mdiArrowUp,
|
||||
mdiAsterisk,
|
||||
mdiCallSplit,
|
||||
mdiCheckboxBlankOutline,
|
||||
mdiCheckBoxOutline,
|
||||
mdiChevronDown,
|
||||
mdiChevronRight,
|
||||
mdiChevronUp,
|
||||
mdiClose,
|
||||
mdiCodeBrackets,
|
||||
mdiDevices,
|
||||
mdiExclamation,
|
||||
mdiRefresh,
|
||||
mdiTimerOutline,
|
||||
mdiTrafficLight,
|
||||
} from "@mdi/js";
|
||||
import "./hat-graph-node";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { NODE_SIZE, SPACING, NodeInfo } from "./hat-graph";
|
||||
import { Condition, Trigger } from "../../data/automation";
|
||||
import {
|
||||
Action,
|
||||
ChooseAction,
|
||||
DelayAction,
|
||||
DeviceAction,
|
||||
EventAction,
|
||||
RepeatAction,
|
||||
SceneAction,
|
||||
ServiceAction,
|
||||
WaitAction,
|
||||
WaitForTriggerAction,
|
||||
} from "../../data/script";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -27,27 +61,390 @@ class HatScriptGraph extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public selected;
|
||||
|
||||
private getActionHandler = memoizeOne((trace: AutomationTraceExtended) => {
|
||||
return new ActionHandler(
|
||||
trace.config.action,
|
||||
false,
|
||||
undefined,
|
||||
(nodeInfo) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(nodeInfo);
|
||||
fireEvent(this, "graph-node-selected", nodeInfo);
|
||||
this.requestUpdate();
|
||||
},
|
||||
this.selected,
|
||||
this.trace
|
||||
);
|
||||
});
|
||||
@property() trackedNodes: Record<string, any> = {};
|
||||
|
||||
private selectNode(config, path) {
|
||||
return () => {
|
||||
fireEvent(this, "graph-node-selected", { config, path });
|
||||
};
|
||||
}
|
||||
|
||||
private render_trigger(config: Trigger, i: number) {
|
||||
const path = `trigger/${i}`;
|
||||
const tracked = this.trace && path in this.trace.trace;
|
||||
if (tracked) {
|
||||
this.trackedNodes[path] = { config, path };
|
||||
}
|
||||
return html`
|
||||
<hat-graph-node
|
||||
graphStart
|
||||
@focus=${this.selectNode(config, path)}
|
||||
class=${classMap({
|
||||
track: tracked,
|
||||
active: this.selected === path,
|
||||
})}
|
||||
.iconPath=${mdiAsterisk}
|
||||
tabindex=${tracked ? "0" : "-1"}
|
||||
></hat-graph-node>
|
||||
`;
|
||||
}
|
||||
|
||||
private render_condition(config: Condition, i: number) {
|
||||
const path = `condition/${i}`;
|
||||
const trace = this.trace.trace[path] as ConditionTraceStep[] | undefined;
|
||||
const track_path =
|
||||
trace === undefined ? 0 : trace![0].result.result ? 1 : 2;
|
||||
if (trace) {
|
||||
this.trackedNodes[path] = { config, path };
|
||||
}
|
||||
return html`
|
||||
<hat-graph
|
||||
branching
|
||||
@focus=${this.selectNode(config, path)}
|
||||
class=${classMap({
|
||||
track: track_path,
|
||||
active: this.selected === path,
|
||||
})}
|
||||
.track_start=${[track_path]}
|
||||
.track_end=${[track_path]}
|
||||
tabindex=${trace === undefined ? "-1" : "0"}
|
||||
short
|
||||
>
|
||||
<hat-graph-node
|
||||
slot="head"
|
||||
class=${classMap({
|
||||
track: trace !== undefined,
|
||||
})}
|
||||
.iconPath=${mdiAbTesting}
|
||||
nofocus
|
||||
graphEnd
|
||||
></hat-graph-node>
|
||||
<div
|
||||
style=${`width: ${NODE_SIZE + SPACING}px;`}
|
||||
graphStart
|
||||
graphEnd
|
||||
></div>
|
||||
<div></div>
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiClose}
|
||||
graphEnd
|
||||
nofocus
|
||||
class=${classMap({
|
||||
track: track_path === 2,
|
||||
})}
|
||||
></hat-graph-node>
|
||||
</hat-graph>
|
||||
`;
|
||||
}
|
||||
|
||||
private render_choose_node(config: ChooseAction, path: string) {
|
||||
const trace = this.trace.trace[path] as ChooseActionTraceStep[] | undefined;
|
||||
const trace_path = trace
|
||||
? trace[0].result.choice === "default"
|
||||
? [config.choose.length]
|
||||
: [trace[0].result.choice]
|
||||
: [];
|
||||
return html`
|
||||
<hat-graph
|
||||
tabindex=${trace === undefined ? "-1" : "0"}
|
||||
branching
|
||||
.track_start=${trace_path}
|
||||
.track_end=${trace_path}
|
||||
@focus=${this.selectNode(config, path)}
|
||||
class=${classMap({
|
||||
track: trace !== undefined,
|
||||
active: this.selected === path,
|
||||
})}
|
||||
>
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiCallSplit}
|
||||
class=${classMap({
|
||||
track: trace !== undefined,
|
||||
})}
|
||||
slot="head"
|
||||
nofocus
|
||||
></hat-graph-node>
|
||||
|
||||
${config.choose.map((branch, i) => {
|
||||
const branch_path = `${path}/choose/${i}`;
|
||||
return html`
|
||||
<hat-graph>
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiCheckBoxOutline}
|
||||
nofocus
|
||||
class=${classMap({
|
||||
track: trace !== undefined && trace[0].result.choice === i,
|
||||
})}
|
||||
></hat-graph-node>
|
||||
${branch.sequence.map((action, j) =>
|
||||
this.render_node(action, `${branch_path}/sequence/${j}`)
|
||||
)}
|
||||
</hat-graph>
|
||||
`;
|
||||
})}
|
||||
<hat-graph>
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiCheckboxBlankOutline}
|
||||
nofocus
|
||||
class=${classMap({
|
||||
track:
|
||||
trace !== undefined && trace[0].result.choice === "default",
|
||||
})}
|
||||
></hat-graph-node>
|
||||
${config.default?.map((action, i) =>
|
||||
this.render_node(action, `${path}/default/${i}`)
|
||||
)}
|
||||
</hat-graph>
|
||||
</hat-graph>
|
||||
`;
|
||||
}
|
||||
|
||||
private render_condition_node(node: Condition, path: string) {
|
||||
const trace: any = this.trace.trace[path];
|
||||
const track_path = trace === undefined ? 0 : trace[0].result.result ? 1 : 2;
|
||||
return html`
|
||||
<hat-graph
|
||||
branching
|
||||
@focus=${this.selectNode(node, path)}
|
||||
class=${classMap({
|
||||
track: track_path,
|
||||
active: this.selected === path,
|
||||
})}
|
||||
.track_start=${[track_path]}
|
||||
.track_end=${[track_path]}
|
||||
tabindex=${trace === undefined ? "-1" : "0"}
|
||||
short
|
||||
>
|
||||
<hat-graph-node
|
||||
slot="head"
|
||||
class=${classMap({
|
||||
track: trace,
|
||||
})}
|
||||
.iconPath=${mdiAbTesting}
|
||||
nofocus
|
||||
graphEnd
|
||||
></hat-graph-node>
|
||||
<div
|
||||
style=${`width: ${NODE_SIZE + SPACING}px;`}
|
||||
graphStart
|
||||
graphEnd
|
||||
></div>
|
||||
<div></div>
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiClose}
|
||||
graphEnd
|
||||
nofocus
|
||||
class=${classMap({
|
||||
track: track_path === 2,
|
||||
})}
|
||||
></hat-graph-node>
|
||||
</hat-graph>
|
||||
`;
|
||||
}
|
||||
|
||||
private render_delay_node(node: DelayAction, path: string) {
|
||||
return html`
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiTimerOutline}
|
||||
@focus=${this.selectNode(node, path)}
|
||||
class=${classMap({
|
||||
track: path in this.trace.trace,
|
||||
active: this.selected === path,
|
||||
})}
|
||||
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
||||
></hat-graph-node>
|
||||
`;
|
||||
}
|
||||
|
||||
private render_device_node(node: DeviceAction, path: string) {
|
||||
return html`
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiDevices}
|
||||
@focus=${this.selectNode(node, path)}
|
||||
class=${classMap({
|
||||
track: path in this.trace.trace,
|
||||
active: this.selected === path,
|
||||
})}
|
||||
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
||||
></hat-graph-node>
|
||||
`;
|
||||
}
|
||||
|
||||
private render_event_node(node: EventAction, path: string) {
|
||||
return html`
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiExclamation}
|
||||
@focus=${this.selectNode(node, path)}
|
||||
class=${classMap({
|
||||
track: path in this.trace.trace,
|
||||
active: this.selected === path,
|
||||
})}
|
||||
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
||||
></hat-graph-node>
|
||||
`;
|
||||
}
|
||||
|
||||
private render_repeat_node(node: RepeatAction, path: string) {
|
||||
const trace: any = this.trace.trace[path];
|
||||
const track_path = trace ? [0, 1] : [];
|
||||
const repeats = this.trace?.trace[`${path}/repeat/sequence/0`]?.length;
|
||||
return html`
|
||||
<hat-graph
|
||||
.track_start=${track_path}
|
||||
.track_end=${track_path}
|
||||
tabindex=${trace === undefined ? "-1" : "0"}
|
||||
branching
|
||||
@focus=${this.selectNode(node, path)}
|
||||
class=${classMap({
|
||||
track: path in this.trace.trace,
|
||||
active: this.selected === path,
|
||||
})}
|
||||
>
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiRefresh}
|
||||
class=${classMap({
|
||||
track: trace,
|
||||
})}
|
||||
slot="head"
|
||||
nofocus
|
||||
></hat-graph-node>
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiArrowUp}
|
||||
nofocus
|
||||
class=${classMap({
|
||||
track: track_path.includes(1),
|
||||
})}
|
||||
.badge=${repeats}
|
||||
></hat-graph-node>
|
||||
<hat-graph>
|
||||
${node.repeat.sequence.map((action, i) =>
|
||||
this.render_node(action, `${path}/repeat/sequence/${i}`)
|
||||
)}
|
||||
</hat-graph>
|
||||
</hat-graph>
|
||||
`;
|
||||
}
|
||||
|
||||
private render_scene_node(node: SceneAction, path: string) {
|
||||
return html`
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiExclamation}
|
||||
@focus=${this.selectNode(node, path)}
|
||||
class=${classMap({
|
||||
track: path in this.trace.trace,
|
||||
active: this.selected === path,
|
||||
})}
|
||||
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
||||
></hat-graph-node>
|
||||
`;
|
||||
}
|
||||
|
||||
private render_service_node(node: ServiceAction, path: string) {
|
||||
return html`
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiChevronRight}
|
||||
@focus=${this.selectNode(node, path)}
|
||||
class=${classMap({
|
||||
track: path in this.trace.trace,
|
||||
active: this.selected === path,
|
||||
})}
|
||||
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
||||
></hat-graph-node>
|
||||
`;
|
||||
}
|
||||
|
||||
private render_wait_node(
|
||||
node: WaitAction | WaitForTriggerAction,
|
||||
path: string
|
||||
) {
|
||||
return html`
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiTrafficLight}
|
||||
@focus=${this.selectNode(node, path)}
|
||||
class=${classMap({
|
||||
track: path in this.trace.trace,
|
||||
active: this.selected === path,
|
||||
})}
|
||||
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
||||
></hat-graph-node>
|
||||
`;
|
||||
}
|
||||
|
||||
private render_other_node(node: Action, path: string) {
|
||||
return html`
|
||||
<hat-graph-node
|
||||
.iconPath=${mdiCodeBrackets}
|
||||
@focus=${this.selectNode(node, path)}
|
||||
class=${classMap({
|
||||
track: path in this.trace.trace,
|
||||
active: this.selected === path,
|
||||
})}
|
||||
></hat-graph-node>
|
||||
`;
|
||||
}
|
||||
|
||||
private render_node(node: Action, path: string) {
|
||||
const NODE_TYPES = {
|
||||
choose: this.render_choose_node,
|
||||
condition: this.render_condition_node,
|
||||
delay: this.render_delay_node,
|
||||
device_id: this.render_device_node,
|
||||
event: this.render_event_node,
|
||||
repeat: this.render_repeat_node,
|
||||
scene: this.render_scene_node,
|
||||
service: this.render_service_node,
|
||||
wait_template: this.render_wait_node,
|
||||
wait_for_trigger: this.render_wait_node,
|
||||
other: this.render_other_node,
|
||||
};
|
||||
|
||||
const type = Object.keys(NODE_TYPES).find((key) => key in node) || "other";
|
||||
const nodeEl = NODE_TYPES[type].bind(this)(node, path);
|
||||
if (this.trace && path in this.trace.trace) {
|
||||
this.trackedNodes[path] = { config: node, path };
|
||||
}
|
||||
return nodeEl;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const actionHandler = this.getActionHandler(this.trace);
|
||||
const paths = Object.keys(this.getTrackedNodes());
|
||||
const paths = Object.keys(this.trackedNodes);
|
||||
|
||||
const manual_triggered = this.trace && "trigger" in this.trace.trace;
|
||||
let track_path = manual_triggered ? undefined : [0];
|
||||
const trigger_nodes = (Array.isArray(this.trace.config.trigger)
|
||||
? this.trace.config.trigger
|
||||
: [this.trace.config.trigger]
|
||||
).map((trigger, i) => {
|
||||
if (this.trace && `trigger/${i}` in this.trace.trace) {
|
||||
track_path = [i];
|
||||
}
|
||||
return this.render_trigger(trigger, i);
|
||||
});
|
||||
|
||||
return html`
|
||||
<hat-graph .tree=${actionHandler.createGraph()}></hat-graph>
|
||||
<hat-graph class="parent">
|
||||
<div></div>
|
||||
<hat-graph
|
||||
branching
|
||||
id="trigger"
|
||||
.short=${trigger_nodes.length < 2}
|
||||
.track_start=${track_path}
|
||||
.track_end=${track_path}
|
||||
>
|
||||
${trigger_nodes}
|
||||
</hat-graph>
|
||||
<hat-graph id="condition">
|
||||
${(!this.trace.config.condition ||
|
||||
Array.isArray(this.trace.config.condition)
|
||||
? this.trace.config.condition
|
||||
: [this.trace.config.condition]
|
||||
)?.map((condition, i) => this.render_condition(condition, i))}
|
||||
</hat-graph>
|
||||
${(Array.isArray(this.trace.config.action)
|
||||
? this.trace.config.action
|
||||
: [this.trace.config.action]
|
||||
).map((action, i) => this.render_node(action, `action/${i}`))}
|
||||
</hat-graph>
|
||||
<div class="actions">
|
||||
<mwc-icon-button
|
||||
.disabled=${paths.length === 0 || paths[0] === this.selected}
|
||||
@@ -66,19 +463,11 @@ class HatScriptGraph extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
public selectPath(path: string) {
|
||||
const actionHandler = this.getActionHandler(this.trace!);
|
||||
let selected: NodeInfo | undefined;
|
||||
|
||||
for (const node of actionHandler.createGraph()) {
|
||||
if (node.nodeInfo?.path === path) {
|
||||
selected = node.nodeInfo;
|
||||
break;
|
||||
}
|
||||
protected update(changedProps: PropertyValues<this>) {
|
||||
if (changedProps.has("trace")) {
|
||||
this.trackedNodes = {};
|
||||
}
|
||||
|
||||
actionHandler.selected = selected?.path || "";
|
||||
this.requestUpdate();
|
||||
super.update(changedProps);
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues<this>) {
|
||||
@@ -93,22 +482,29 @@ class HatScriptGraph extends LitElement {
|
||||
if (this.selected === "" || !(this.selected in paths)) {
|
||||
// Find first tracked node with node info
|
||||
for (const path of paths) {
|
||||
if (tracked[path].nodeInfo) {
|
||||
fireEvent(this, "graph-node-selected", tracked[path].nodeInfo);
|
||||
if (tracked[path]) {
|
||||
fireEvent(this, "graph-node-selected", tracked[path]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has("selected")) {
|
||||
this.getActionHandler(this.trace).selected = this.selected;
|
||||
this.requestUpdate();
|
||||
if (this.trace) {
|
||||
const sortKeys = Object.keys(this.trace.trace);
|
||||
const keys = Object.keys(this.trackedNodes).sort(
|
||||
(a, b) => sortKeys.indexOf(a) - sortKeys.indexOf(b)
|
||||
);
|
||||
const sortedTrackedNodes = keys.reduce((obj, key) => {
|
||||
obj[key] = this.trackedNodes[key];
|
||||
return obj;
|
||||
}, {});
|
||||
this.trackedNodes = sortedTrackedNodes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getTrackedNodes() {
|
||||
return this._getTrackedNodes(this.trace);
|
||||
return this.trackedNodes;
|
||||
}
|
||||
|
||||
public previousTrackedNode() {
|
||||
@@ -116,8 +512,8 @@ class HatScriptGraph extends LitElement {
|
||||
const nodes = Object.keys(tracked);
|
||||
|
||||
for (let i = nodes.indexOf(this.selected) - 1; i >= 0; i--) {
|
||||
if (tracked[nodes[i]].nodeInfo) {
|
||||
fireEvent(this, "graph-node-selected", tracked[nodes[i]].nodeInfo);
|
||||
if (tracked[nodes[i]]) {
|
||||
fireEvent(this, "graph-node-selected", tracked[nodes[i]]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -127,25 +523,13 @@ class HatScriptGraph extends LitElement {
|
||||
const tracked = this.getTrackedNodes();
|
||||
const nodes = Object.keys(tracked);
|
||||
for (let i = nodes.indexOf(this.selected) + 1; i < nodes.length; i++) {
|
||||
if (tracked[nodes[i]].nodeInfo) {
|
||||
fireEvent(this, "graph-node-selected", tracked[nodes[i]].nodeInfo);
|
||||
if (tracked[nodes[i]]) {
|
||||
fireEvent(this, "graph-node-selected", tracked[nodes[i]]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _getTrackedNodes = memoizeOne((trace) => {
|
||||
const tracked: Record<string, TreeNode> = {};
|
||||
for (const node of bfsIterateTreeNodes(
|
||||
this.getActionHandler(trace).createGraph()
|
||||
)) {
|
||||
if (node.isTracked && node.nodeInfo) {
|
||||
tracked[node.nodeInfo.path] = node;
|
||||
}
|
||||
}
|
||||
return tracked;
|
||||
});
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
:host {
|
||||
@@ -155,6 +539,9 @@ class HatScriptGraph extends LitElement {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.parent {
|
||||
margin-left: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user