Cleanup of tracing graph (#9564)

This commit is contained in:
Bram Kragten 2021-08-11 01:23:07 +02:00 committed by GitHub
parent dc50e54afc
commit f686816c86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 474 additions and 570 deletions

View File

@ -5,7 +5,6 @@ import { classMap } from "lit/directives/class-map";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import "../ha-code-editor";
import "../ha-icon-button";
import type { NodeInfo } from "./hat-graph";
import "./hat-logbook-note";
import { LogbookEntry } from "../../data/logbook";
import {
@ -17,6 +16,7 @@ import {
import "../../panels/logbook/ha-logbook";
import { traceTabStyles } from "./trace-tab-styles";
import { HomeAssistant } from "../../types";
import type { NodeInfo } from "./hat-script-graph";
@customElement("ha-trace-path-details")
export class HaTracePathDetails extends LitElement {
@ -30,7 +30,7 @@ export class HaTracePathDetails extends LitElement {
@property({ attribute: false }) public selected!: NodeInfo;
@property() renderedNodes: Record<string, any> = {};
@property() public renderedNodes: Record<string, any> = {};
@property() public trackedNodes!: Record<string, any>;

View File

@ -1,11 +1,11 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import type { NodeInfo } from "./hat-graph";
import "./hat-logbook-note";
import "./hat-trace-timeline";
import type { LogbookEntry } from "../../data/logbook";
import type { TraceExtended } from "../../data/trace";
import type { HomeAssistant } from "../../types";
import type { NodeInfo } from "./hat-script-graph";
@customElement("ha-trace-timeline")
export class HaTraceTimeline extends LitElement {
@ -15,7 +15,7 @@ export class HaTraceTimeline extends LitElement {
@property({ attribute: false }) public logbookEntries!: LogbookEntry[];
@property() public selected!: NodeInfo;
@property({ attribute: false }) public selected!: NodeInfo;
protected render(): TemplateResult {
return html`

View File

@ -0,0 +1,186 @@
import { css, html, LitElement, svg } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { BRANCH_HEIGHT, SPACING } from "./hat-graph-const";
interface BranchConfig {
x: number;
height: number;
start: boolean;
end: boolean;
track: boolean;
}
/**
* @attribute active
* @attribute track
*/
@customElement("hat-graph-branch")
export class HatGraphBranch extends LitElement {
@property({ reflect: true, type: Boolean }) disabled?: boolean;
@property({ type: Boolean }) selected?: boolean;
@property({ type: Boolean }) start = false;
@property({ type: Boolean }) short = false;
@state() _branches: BranchConfig[] = [];
private _totalWidth = 0;
private _maxHeight = 0;
private _updateBranches(ev: Event) {
let total_width = 0;
const heights: number[] = [];
const branches: BranchConfig[] = [];
(ev.target as HTMLSlotElement).assignedElements().forEach((c) => {
const width = c.clientWidth;
const height = c.clientHeight;
branches.push({
x: width / 2 + total_width,
height,
start: c.hasAttribute("graphStart"),
end: c.hasAttribute("graphEnd"),
track: c.hasAttribute("track"),
});
total_width += width;
heights.push(height);
});
this._totalWidth = total_width;
this._maxHeight = Math.max(...heights);
this._branches = branches.sort((a, b) => {
if (a.track && !b.track) {
return 1;
}
if (a.track && b.track) {
return 0;
}
return -1;
});
}
render() {
return html`
<slot name="head"></slot>
${!this.start
? svg`
<svg
id="top"
width="${this._totalWidth}"
>
${this._branches.map((branch) =>
branch.start
? ""
: svg`
<path
class=${classMap({
track: branch.track,
})}
d="
M ${this._totalWidth / 2} 0
L ${branch.x} ${BRANCH_HEIGHT}
"/>
`
)}
</svg>
`
: ""}
<div id="branches">
<svg id="lines" width="${this._totalWidth}" height="${this._maxHeight}">
${this._branches.map((branch) => {
if (branch.end) return "";
return svg`
<path
class=${classMap({
track: branch.track,
})}
d="
M ${branch.x} ${branch.height}
v ${this._maxHeight - branch.height}
"/>
`;
})}
</svg>
<slot @slotchange=${this._updateBranches}></slot>
</div>
${!this.short
? svg`
<svg
id="bottom"
width="${this._totalWidth}"
>
${this._branches.map((branch) => {
if (branch.end) return "";
return svg`
<path
class=${classMap({
track: branch.track,
})}
d="
M ${branch.x} 0
V ${SPACING}
L ${this._totalWidth / 2} ${BRANCH_HEIGHT + SPACING}
"/>
`;
})}
</svg>
`
: ""}
`;
}
static get styles() {
return css`
:host {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
:host(:focus) {
outline: none;
}
#branches {
position: relative;
display: flex;
flex-direction: row;
align-items: start;
}
::slotted(*) {
z-index: 1;
}
::slotted([slot="head"]) {
margin-bottom: calc(var(--hat-graph-branch-height) / -2);
}
#lines {
position: absolute;
}
#top {
height: var(--hat-graph-branch-height);
}
#bottom {
height: calc(var(--hat-graph-branch-height) + var(--hat-graph-spacing));
}
path {
stroke: var(--stroke-clr);
stroke-width: 2;
fill: none;
}
path.track {
stroke: var(--track-clr);
}
:host([disabled]) path {
stroke: var(--disabled-clr);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hat-graph-branch": HatGraphBranch;
}
}

View File

@ -0,0 +1,3 @@
export const SPACING = 10;
export const NODE_SIZE = 30;
export const BRANCH_HEIGHT = 20;

View File

@ -1,7 +1,18 @@
import { css, LitElement, svg } from "lit";
import {
css,
LitElement,
PropertyValues,
html,
TemplateResult,
svg,
} from "lit";
import { customElement, property } from "lit/decorators";
import { NODE_SIZE, SPACING } from "./hat-graph";
import { NODE_SIZE, SPACING } from "./hat-graph-const";
/**
* @attribute active
* @attribute track
*/
@customElement("hat-graph-node")
export class HatGraphNode extends LitElement {
@property() iconPath?: string;
@ -10,31 +21,33 @@ export class HatGraphNode extends LitElement {
@property({ reflect: true, type: Boolean }) graphStart?: boolean;
@property({ reflect: true, type: Boolean }) nofocus?: boolean;
@property({ reflect: true, type: Boolean, attribute: "nofocus" })
noFocus = false;
@property({ reflect: true, type: Number }) badge?: number;
connectedCallback() {
super.connectedCallback();
if (!this.hasAttribute("tabindex") && !this.nofocus)
this.setAttribute("tabindex", "0");
protected updated(changedProps: PropertyValues) {
if (changedProps.has("noFocus")) {
if (!this.hasAttribute("tabindex") && !this.noFocus) {
this.setAttribute("tabindex", "0");
} else if (changedProps.get("noFocus") !== undefined && this.noFocus) {
this.removeAttribute("tabindex");
}
}
}
render() {
protected render(): TemplateResult {
const height = NODE_SIZE + (this.graphStart ? 2 : SPACING + 1);
const width = SPACING + NODE_SIZE;
return svg`
<svg
width="${width}px"
height="${height}px"
viewBox="-${Math.ceil(width / 2)} -${
this.graphStart
? Math.ceil(height / 2)
: Math.ceil((NODE_SIZE + SPACING * 2) / 2)
} ${width} ${height}"
>
${
this.graphStart
return html`
<svg
width="${width}px"
height="${height}px"
viewBox="-${Math.ceil(width / 2)} -${this.graphStart
? Math.ceil(height / 2)
: Math.ceil((NODE_SIZE + SPACING * 2) / 2)} ${width} ${height}"
>
${this.graphStart
? ``
: svg`
<path
@ -45,41 +58,31 @@ export class HatGraphNode extends LitElement {
"
line-caps="round"
/>
`
}
<g class="node">
<circle
cx="0"
cy="0"
r="${NODE_SIZE / 2}"
/>
}
${
this.badge
? svg`
`}
<g class="node">
<circle cx="0" cy="0" r=${NODE_SIZE / 2} />
}
${this.badge
? svg`
<g class="number">
<circle
cx="8"
cy="${-NODE_SIZE / 2}"
cy=${-NODE_SIZE / 2}
r="8"
></circle>
<text
x="8"
y="${-NODE_SIZE / 2}"
y=${-NODE_SIZE / 2}
text-anchor="middle"
alignment-baseline="middle"
>${this.badge > 9 ? "9+" : this.badge}</text>
</g>
`
: ""
}
<g
style="pointer-events: none"
transform="translate(${-12} ${-12})"
>
${this.iconPath ? svg`<path class="icon" d="${this.iconPath}"/>` : ""}
</g>
</g>
: ""}
<g style="pointer-events: none" transform="translate(${-12} ${-12})">
${this.iconPath ? svg`<path class="icon" d=${this.iconPath}/>` : ""}
</g>
</g>
</svg>
`;
}
@ -90,11 +93,11 @@ export class HatGraphNode extends LitElement {
display: flex;
flex-direction: column;
}
:host(.track) {
:host([track]) {
--stroke-clr: var(--track-clr);
--icon-clr: var(--default-icon-clr);
}
:host(.active) circle {
:host([active]) circle {
--stroke-clr: var(--active-clr);
--icon-clr: var(--default-icon-clr);
}
@ -111,7 +114,7 @@ export class HatGraphNode extends LitElement {
:host-context([disabled]) {
--stroke-clr: var(--disabled-clr);
}
:host([nofocus]):host-context(.active),
:host([nofocus]):host-context([active]),
:host([nofocus]):host-context(:focus) {
--circle-clr: var(--active-clr);
--icon-clr: var(--default-icon-clr);
@ -137,24 +140,6 @@ export class HatGraphNode extends LitElement {
path.icon {
fill: var(--icon-clr);
}
:host(.triggered) svg {
overflow: visible;
}
:host(.triggered) circle {
animation: glow 10s;
}
@keyframes glow {
0% {
filter: drop-shadow(0px 0px 5px rgba(var(--rgb-trigger-color), 0));
}
10% {
filter: drop-shadow(0px 0px 10px rgba(var(--rgb-trigger-color), 1));
}
100% {
filter: drop-shadow(0px 0px 5px rgba(var(--rgb-trigger-color), 0));
}
}
`;
}
}

View File

@ -1,27 +1,26 @@
import { css, LitElement, svg } from "lit";
import { css, LitElement, html } from "lit";
import { customElement, property } from "lit/decorators";
import { NODE_SIZE, SPACING } from "./hat-graph";
import { SPACING, NODE_SIZE } from "./hat-graph-const";
/**
* @attribute active
* @attribute track
*/
@customElement("hat-graph-spacer")
export class HatGraphSpacer extends LitElement {
@property({ reflect: true, type: Boolean }) disabled?: boolean;
render() {
return svg`
<svg
width="${SPACING}px"
height="${SPACING + NODE_SIZE + 1}px"
viewBox="-${SPACING / 2} 0 10 ${SPACING + NODE_SIZE + 1}"
>
<path
class="connector"
d="
return html`
<svg viewBox="-${SPACING / 2} 0 10 ${SPACING + NODE_SIZE + 1}">
<path
d="
M 0 ${SPACING + NODE_SIZE + 1}
L 0 0
V 0
"
line-caps="round"
/>
}
line-caps="round"
/>
}
</svg>
`;
}
@ -31,15 +30,21 @@ export class HatGraphSpacer extends LitElement {
:host {
display: flex;
flex-direction: column;
align-items: center;
}
:host(.track) {
svg {
width: var(--hat-graph-spacing);
height: calc(
var(--hat-graph-spacing) + var(--hat-graph-node-size) + 1px
);
}
:host([track]) {
--stroke-clr: var(--track-clr);
--icon-clr: var(--default-icon-clr);
}
:host-context([disabled]) {
--stroke-clr: var(--disabled-clr);
}
path.connector {
path {
stroke: var(--stroke-clr);
stroke-width: 2;
fill: none;

View File

@ -1,219 +0,0 @@
import { css, html, LitElement, svg } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
export const BRANCH_HEIGHT = 20;
export const SPACING = 10;
export const NODE_SIZE = 30;
const track_converter = {
fromAttribute: (value) => value.split(",").map((v) => parseInt(v)),
toAttribute: (value) =>
value instanceof Array ? value.join(",") : `${value}`,
};
export interface NodeInfo {
path: string;
config: any;
}
interface BranchConfig {
x: number;
height: number;
start: boolean;
end: boolean;
}
@customElement("hat-graph")
export class HatGraph extends LitElement {
@property({ type: Number }) _num_items = 0;
@property({ reflect: true, type: Boolean }) branching?: boolean;
@property({ converter: track_converter })
track_start?: number[];
@property({ converter: track_converter }) track_end?: number[];
@property({ reflect: true, type: Boolean }) disabled?: boolean;
@property({ type: Boolean }) selected?: boolean;
@property({ type: Boolean }) short = false;
async updateChildren() {
this._num_items = this.children.length;
}
render() {
const branches: BranchConfig[] = [];
let total_width = 0;
let max_height = 0;
let min_height = Number.POSITIVE_INFINITY;
if (this.branching) {
for (const c of Array.from(this.children)) {
if (c.slot === "head") continue;
const rect = c.getBoundingClientRect();
branches.push({
x: rect.width / 2 + total_width,
height: rect.height,
start: c.getAttribute("graphStart") != null,
end: c.getAttribute("graphEnd") != null,
});
total_width += rect.width;
max_height = Math.max(max_height, rect.height);
min_height = Math.min(min_height, rect.height);
}
}
return html`
<slot name="head" @slotchange=${this.updateChildren}> </slot>
${this.branching && branches.some((branch) => !branch.start)
? svg`
<svg
id="top"
width="${total_width}"
height="${BRANCH_HEIGHT}"
>
${branches.map((branch, i) => {
if (branch.start) return "";
return svg`
<path
class="${classMap({
line: true,
track: this.track_start?.includes(i) ?? false,
})}"
id="${this.track_start?.includes(i) ? "track-start" : ""}"
index=${i}
d="
M ${total_width / 2} 0
L ${branch.x} ${BRANCH_HEIGHT}
"/>
`;
})}
<use xlink:href="#track-start" />
</svg>
`
: ""}
<div id="branches">
${this.branching
? svg`
<svg
id="lines"
width="${total_width}"
height="${max_height}"
>
${branches.map((branch, i) => {
if (branch.end) return "";
return svg`
<path
class="${classMap({
line: true,
track: this.track_end?.includes(i) ?? false,
})}"
index=${i}
d="
M ${branch.x} ${branch.height}
l 0 ${max_height - branch.height}
"/>
`;
})}
</svg>
`
: ""}
<slot @slotchange=${this.updateChildren}></slot>
</div>
${this.branching && !this.short
? svg`
<svg
id="bottom"
width="${total_width}"
height="${BRANCH_HEIGHT + SPACING}"
>
${branches.map((branch, i) => {
if (branch.end) return "";
return svg`
<path
class="${classMap({
line: true,
track: this.track_end?.includes(i) ?? false,
})}"
id="${this.track_end?.includes(i) ? "track-end" : ""}"
index=${i}
d="
M ${branch.x} 0
L ${branch.x} ${SPACING}
L ${total_width / 2} ${BRANCH_HEIGHT + SPACING}
"/>
`;
})}
<use xlink:href="#track-end" />
</svg>
`
: ""}
`;
}
static get styles() {
return css`
:host {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
--stroke-clr: var(--stroke-color, var(--secondary-text-color));
--active-clr: var(--active-color, var(--primary-color));
--track-clr: var(--track-color, var(--accent-color));
--hover-clr: var(--hover-color, var(--primary-color));
--disabled-clr: var(--disabled-color, var(--disabled-text-color));
--default-trigger-color: 3, 169, 244;
--rgb-trigger-color: var(--trigger-color, var(--default-trigger-color));
--background-clr: var(--background-color, white);
--default-icon-clr: var(--icon-color, black);
--icon-clr: var(--stroke-clr);
}
:host(:focus) {
outline: none;
}
#branches {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
:host([branching]) #branches {
flex-direction: row;
align-items: start;
}
:host([branching]) ::slotted(*) {
z-index: 1;
}
:host([branching]) ::slotted([slot="head"]) {
margin-bottom: ${-BRANCH_HEIGHT / 2}px;
}
#lines {
position: absolute;
}
path.line {
stroke: var(--stroke-clr);
stroke-width: 2;
fill: none;
}
path.line.track {
stroke: var(--track-clr);
}
:host([disabled]) path.line {
stroke: var(--disabled-clr);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hat-graph": HatGraph;
}
}

View File

@ -19,7 +19,6 @@ import {
} from "@mdi/js";
import { css, html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../common/dom/fire_event";
import { ensureArray } from "../../common/ensure-array";
import { Condition, Trigger } from "../../data/automation";
@ -41,9 +40,15 @@ import {
TraceExtended,
} from "../../data/trace";
import "../ha-svg-icon";
import { NodeInfo, NODE_SIZE, SPACING } from "./hat-graph";
import "./hat-graph-node";
import "./hat-graph-spacer";
import "./hat-graph-branch";
import { NODE_SIZE, SPACING, BRANCH_HEIGHT } from "./hat-graph-const";
export interface NodeInfo {
path: string;
config: any;
}
declare global {
interface HASSDomEvents {
@ -52,14 +57,14 @@ declare global {
}
@customElement("hat-script-graph")
class HatScriptGraph extends LitElement {
export class HatScriptGraph extends LitElement {
@property({ attribute: false }) public trace!: TraceExtended;
@property({ attribute: false }) public selected;
@property({ attribute: false }) public selected?: string;
@property() renderedNodes: Record<string, any> = {};
public renderedNodes: Record<string, NodeInfo> = {};
@property() trackedNodes: Record<string, any> = {};
public trackedNodes: Record<string, NodeInfo> = {};
private selectNode(config, path) {
return () => {
@ -69,72 +74,54 @@ class HatScriptGraph extends LitElement {
private render_trigger(config: Trigger, i: number) {
const path = `trigger/${i}`;
const tracked = this.trace && path in this.trace.trace;
const track = this.trace && path in this.trace.trace;
this.renderedNodes[path] = { config, path };
if (tracked) {
if (track) {
this.trackedNodes[path] = this.renderedNodes[path];
}
return html`
<hat-graph-node
graphStart
?track=${track}
@focus=${this.selectNode(config, path)}
class=${classMap({
track: tracked,
active: this.selected === path,
})}
?active=${this.selected === path}
.iconPath=${mdiAsterisk}
tabindex=${tracked ? "0" : "-1"}
tabindex=${track ? "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?.[0].result === undefined ? 0 : trace[0].result.result ? 1 : 2;
this.renderedNodes[path] = { config, path };
if (trace) {
if (this.trace && path in this.trace.trace) {
this.trackedNodes[path] = this.renderedNodes[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 ? "-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>
`;
return this.render_condition_node(config, path);
}
private typeRenderers = {
condition: this.render_condition_node,
delay: this.render_delay_node,
event: this.render_event_node,
scene: this.render_scene_node,
service: this.render_service_node,
wait_template: this.render_wait_node,
wait_for_trigger: this.render_wait_node,
repeat: this.render_repeat_node,
choose: this.render_choose_node,
device_id: this.render_device_node,
other: this.render_other_node,
};
private render_action_node(node: Action, path: string, graphStart = false) {
const type =
Object.keys(this.typeRenderers).find((key) => key in node) || "other";
this.renderedNodes[path] = { config: node, path };
if (this.trace && path in this.trace.trace) {
this.trackedNodes[path] = this.renderedNodes[path];
}
return this.typeRenderers[type].bind(this)(node, path, graphStart);
}
private render_choose_node(
@ -143,30 +130,25 @@ class HatScriptGraph extends LitElement {
graphStart = false
) {
const trace = this.trace.trace[path] as ChooseActionTraceStep[] | undefined;
const trace_path =
trace !== undefined
? trace[0].result === undefined || trace[0].result.choice === "default"
? [Array.isArray(config.choose) ? config.choose.length : 0]
: [trace[0].result.choice]
: [];
const trace_path = trace
? trace.map((trc) =>
trc.result === undefined || trc.result.choice === "default"
? "default"
: trc.result.choice
)
: [];
const track_default = trace_path.includes("default");
return html`
<hat-graph
<hat-graph-branch
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,
})}
?track=${trace !== undefined}
?active=${this.selected === path}
>
<hat-graph-node
.graphStart=${graphStart}
.iconPath=${mdiCallSplit}
class=${classMap({
track: trace !== undefined,
})}
?track=${trace !== undefined}
slot="head"
nofocus
></hat-graph-node>
@ -174,46 +156,39 @@ class HatScriptGraph extends LitElement {
${config.choose
? ensureArray(config.choose)?.map((branch, i) => {
const branch_path = `${path}/choose/${i}`;
const track_this =
trace !== undefined && trace[0].result?.choice === i;
const track_this = trace_path.includes(i);
this.renderedNodes[branch_path] = { config, path: branch_path };
if (track_this) {
this.trackedNodes[branch_path] =
this.renderedNodes[branch_path];
}
return html`
<hat-graph>
<div class="graph-container" ?track=${track_this}>
<hat-graph-node
.iconPath=${!trace || track_this
? mdiCheckBoxOutline
: mdiCheckboxBlankOutline}
@focus=${this.selectNode(config, branch_path)}
class=${classMap({
active: this.selected === branch_path,
track: track_this,
})}
?track=${track_this}
?active=${this.selected === branch_path}
></hat-graph-node>
${ensureArray(branch.sequence).map((action, j) =>
this.render_node(action, `${branch_path}/sequence/${j}`)
this.render_action_node(
action,
`${branch_path}/sequence/${j}`
)
)}
</hat-graph>
</div>
`;
})
: ""}
<hat-graph>
<hat-graph-spacer
class=${classMap({
track:
trace !== undefined &&
(trace[0].result === undefined ||
trace[0].result.choice === "default"),
})}
></hat-graph-spacer>
<div ?track=${track_default}>
<hat-graph-spacer ?track=${track_default}></hat-graph-spacer>
${ensureArray(config.default)?.map((action, i) =>
this.render_node(action, `${path}/default/${i}`)
this.render_action_node(action, `${path}/default/${i}`)
)}
</hat-graph>
</hat-graph>
</div>
</hat-graph-branch>
`;
}
@ -222,41 +197,52 @@ class HatScriptGraph extends LitElement {
path: string,
graphStart = false
) {
const trace = (this.trace.trace[path] as ConditionTraceStep[]) || undefined;
const track_path =
trace?.[0].result === undefined ? 0 : trace[0].result.result ? 1 : 2;
const trace = this.trace.trace[path] as ConditionTraceStep[] | undefined;
let track = false;
let trackPass = false;
let trackFailed = false;
if (trace) {
for (const trc of trace) {
if (trc.result) {
track = true;
if (trc.result.result) {
trackPass = true;
} else {
trackFailed = true;
}
}
if (trackPass && trackFailed) {
break;
}
}
}
return html`
<hat-graph
branching
<hat-graph-branch
@focus=${this.selectNode(node, path)}
class=${classMap({
track: track_path,
active: this.selected === path,
})}
.track_start=${[track_path]}
.track_end=${[track_path]}
?track=${track}
?active=${this.selected === path}
tabindex=${trace === undefined ? "-1" : "0"}
short
>
<hat-graph-node
.graphStart=${graphStart}
slot="head"
class=${classMap({
track: Boolean(trace),
})}
?track=${track}
.iconPath=${mdiAbTesting}
nofocus
></hat-graph-node>
<div style=${`width: ${NODE_SIZE + SPACING}px;`}></div>
<div></div>
<div
style=${`width: ${NODE_SIZE + SPACING}px;`}
graphStart
graphEnd
></div>
<div ?track=${trackPass}></div>
<hat-graph-node
.iconPath=${mdiClose}
nofocus
class=${classMap({
track: track_path === 2,
})}
?track=${trackFailed}
></hat-graph-node>
</hat-graph>
</hat-graph-branch>
`;
}
@ -270,10 +256,8 @@ class HatScriptGraph extends LitElement {
.graphStart=${graphStart}
.iconPath=${mdiTimerOutline}
@focus=${this.selectNode(node, path)}
class=${classMap({
track: path in this.trace.trace,
active: this.selected === path,
})}
?track=${path in this.trace.trace}
?active=${this.selected === path}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node>
`;
@ -289,10 +273,8 @@ class HatScriptGraph extends LitElement {
.graphStart=${graphStart}
.iconPath=${mdiDevices}
@focus=${this.selectNode(node, path)}
class=${classMap({
track: path in this.trace.trace,
active: this.selected === path,
})}
?track=${path in this.trace.trace}
?active=${this.selected === path}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node>
`;
@ -308,10 +290,8 @@ class HatScriptGraph extends LitElement {
.graphStart=${graphStart}
.iconPath=${mdiExclamation}
@focus=${this.selectNode(node, path)}
class=${classMap({
track: path in this.trace.trace,
active: this.selected === path,
})}
?track=${path in this.trace.trace}
?active=${this.selected === path}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node>
`;
@ -323,43 +303,33 @@ class HatScriptGraph extends LitElement {
graphStart = false
) {
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}
<hat-graph-branch
tabindex=${trace === undefined ? "-1" : "0"}
branching
@focus=${this.selectNode(node, path)}
class=${classMap({
track: path in this.trace.trace,
active: this.selected === path,
})}
?track=${path in this.trace.trace}
?active=${this.selected === path}
>
<hat-graph-node
.graphStart=${graphStart}
.iconPath=${mdiRefresh}
class=${classMap({
track: trace,
})}
?track=${path in this.trace.trace}
slot="head"
nofocus
></hat-graph-node>
<hat-graph-node
.iconPath=${mdiArrowUp}
?track=${repeats > 1}
nofocus
class=${classMap({
track: track_path.includes(1),
})}
.badge=${repeats}
.badge=${repeats > 1 ? repeats : undefined}
></hat-graph-node>
<hat-graph>
<div ?track=${trace}>
${ensureArray(node.repeat.sequence).map((action, i) =>
this.render_node(action, `${path}/repeat/sequence/${i}`)
this.render_action_node(action, `${path}/repeat/sequence/${i}`)
)}
</hat-graph>
</hat-graph>
</div>
</hat-graph-branch>
`;
}
@ -373,10 +343,8 @@ class HatScriptGraph extends LitElement {
.graphStart=${graphStart}
.iconPath=${mdiExclamation}
@focus=${this.selectNode(node, path)}
class=${classMap({
track: path in this.trace.trace,
active: this.selected === path,
})}
?track=${path in this.trace.trace}
?active=${this.selected === path}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node>
`;
@ -392,10 +360,8 @@ class HatScriptGraph extends LitElement {
.graphStart=${graphStart}
.iconPath=${mdiChevronRight}
@focus=${this.selectNode(node, path)}
class=${classMap({
track: path in this.trace.trace,
active: this.selected === path,
})}
?track=${path in this.trace.trace}
?active=${this.selected === path}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node>
`;
@ -411,10 +377,8 @@ class HatScriptGraph extends LitElement {
.graphStart=${graphStart}
.iconPath=${mdiTrafficLight}
@focus=${this.selectNode(node, path)}
class=${classMap({
track: path in this.trace.trace,
active: this.selected === path,
})}
?track=${path in this.trace.trace}
?active=${this.selected === path}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node>
`;
@ -426,95 +390,55 @@ class HatScriptGraph extends LitElement {
.graphStart=${graphStart}
.iconPath=${mdiCodeBrackets}
@focus=${this.selectNode(node, path)}
class=${classMap({
track: path in this.trace.trace,
active: this.selected === path,
})}
?track=${path in this.trace.trace}
?active=${this.selected === path}
></hat-graph-node>
`;
}
private render_node(node: Action, path: string, graphStart = false) {
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, graphStart);
this.renderedNodes[path] = { config: node, path };
if (this.trace && path in this.trace.trace) {
this.trackedNodes[path] = this.renderedNodes[path];
}
return nodeEl;
}
protected render() {
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 =
"trigger" in this.trace.config
? ensureArray(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);
})
? ensureArray(this.trace.config.trigger).map((trigger, i) =>
this.render_trigger(trigger, i)
)
: undefined;
try {
return html`
<hat-graph class="parent">
<div></div>
<div class="parent graph-container">
${trigger_nodes
? html`<hat-graph
branching
id="trigger"
.short=${trigger_nodes.length < 2}
.track_start=${track_path}
.track_end=${track_path}
>
? html`<hat-graph-branch start .short=${trigger_nodes.length < 2}>
${trigger_nodes}
</hat-graph>`
</hat-graph-branch>`
: ""}
${"condition" in this.trace.config
? html`<hat-graph id="condition">
${ensureArray(this.trace.config.condition)?.map(
(condition, i) => this.render_condition(condition!, i)
)}
</hat-graph>`
? html`${ensureArray(this.trace.config.condition)?.map(
(condition, i) => this.render_condition(condition, i)
)}`
: ""}
${"action" in this.trace.config
? html`${ensureArray(this.trace.config.action).map((action, i) =>
this.render_node(action, `action/${i}`)
this.render_action_node(action, `action/${i}`)
)}`
: ""}
${"sequence" in this.trace.config
? html`${ensureArray(this.trace.config.sequence).map((action, i) =>
this.render_node(action, `sequence/${i}`, i === 0)
this.render_action_node(action, `sequence/${i}`, i === 0)
)}`
: ""}
</hat-graph>
</div>
<div class="actions">
<mwc-icon-button
.disabled=${paths.length === 0 || paths[0] === this.selected}
@click=${this.previousTrackedNode}
@click=${this._previousTrackedNode}
>
<ha-svg-icon .path=${mdiChevronUp}></ha-svg-icon>
</mwc-icon-button>
<mwc-icon-button
.disabled=${paths.length === 0 ||
paths[paths.length - 1] === this.selected}
@click=${this.nextTrackedNode}
@click=${this._nextTrackedNode}
>
<ha-svg-icon .path=${mdiChevronDown}></ha-svg-icon>
</mwc-icon-button>
@ -545,44 +469,39 @@ class HatScriptGraph extends LitElement {
protected updated(changedProps: PropertyValues<this>) {
super.updated(changedProps);
// Select first node if new trace loaded but no selection given.
if (changedProps.has("trace")) {
const tracked = this.trackedNodes;
const paths = Object.keys(tracked);
if (!changedProps.has("trace")) {
return;
}
// If trace changed and we have no or an invalid selection, select first option.
if (this.selected === "" || !(this.selected in paths)) {
// Find first tracked node with node info
for (const path of paths) {
if (tracked[path]) {
fireEvent(this, "graph-node-selected", tracked[path]);
break;
}
// If trace changed and we have no or an invalid selection, select first option.
if (!this.selected || !(this.selected in this.trackedNodes)) {
const firstNode = this.trackedNodes[Object.keys(this.trackedNodes)[0]];
if (firstNode) {
fireEvent(this, "graph-node-selected", firstNode);
}
}
if (this.trace) {
const sortKeys = Object.keys(this.trace.trace);
const keys = Object.keys(this.renderedNodes).sort(
(a, b) => sortKeys.indexOf(a) - sortKeys.indexOf(b)
);
const sortedTrackedNodes = {};
const sortedRenderedNodes = {};
for (const key of keys) {
sortedRenderedNodes[key] = this.renderedNodes[key];
if (key in this.trackedNodes) {
sortedTrackedNodes[key] = this.trackedNodes[key];
}
}
if (this.trace) {
const sortKeys = Object.keys(this.trace.trace);
const keys = Object.keys(this.renderedNodes).sort(
(a, b) => sortKeys.indexOf(a) - sortKeys.indexOf(b)
);
const sortedTrackedNodes = {};
const sortedRenderedNodes = {};
for (const key of keys) {
sortedRenderedNodes[key] = this.renderedNodes[key];
if (key in this.trackedNodes) {
sortedTrackedNodes[key] = this.trackedNodes[key];
}
}
this.renderedNodes = sortedRenderedNodes;
this.trackedNodes = sortedTrackedNodes;
}
this.renderedNodes = sortedRenderedNodes;
this.trackedNodes = sortedTrackedNodes;
}
}
public previousTrackedNode() {
private _previousTrackedNode() {
const nodes = Object.keys(this.trackedNodes);
const prevIndex = nodes.indexOf(this.selected) - 1;
const prevIndex = nodes.indexOf(this.selected!) - 1;
if (prevIndex >= 0) {
fireEvent(
this,
@ -592,9 +511,9 @@ class HatScriptGraph extends LitElement {
}
}
public nextTrackedNode() {
private _nextTrackedNode() {
const nodes = Object.keys(this.trackedNodes);
const nextIndex = nodes.indexOf(this.selected) + 1;
const nextIndex = nodes.indexOf(this.selected!) + 1;
if (nextIndex < nodes.length) {
fireEvent(
this,
@ -608,6 +527,25 @@ class HatScriptGraph extends LitElement {
return css`
:host {
display: flex;
--stroke-clr: var(--stroke-color, var(--secondary-text-color));
--active-clr: var(--active-color, var(--primary-color));
--track-clr: var(--track-color, var(--accent-color));
--hover-clr: var(--hover-color, var(--primary-color));
--disabled-clr: var(--disabled-color, var(--disabled-text-color));
--default-trigger-color: 3, 169, 244;
--rgb-trigger-color: var(--trigger-color, var(--default-trigger-color));
--background-clr: var(--background-color, white);
--default-icon-clr: var(--icon-color, black);
--icon-clr: var(--stroke-clr);
--hat-graph-spacing: ${SPACING}px;
--hat-graph-node-size: ${NODE_SIZE}px;
--hat-graph-branch-height: ${BRANCH_HEIGHT}px;
}
.graph-container {
display: flex;
flex-direction: column;
align-items: center;
}
.actions {
display: flex;

View File

@ -6,12 +6,11 @@ import {
mdiRefresh,
} from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { formatDateTimeWithSeconds } from "../../../common/datetime/format_date_time";
import type { NodeInfo } from "../../../components/trace/hat-graph";
import "../../../components/trace/hat-script-graph";
import { AutomationEntity } from "../../../data/automation";
import { getLogbookDataForContext, LogbookEntry } from "../../../data/logbook";
@ -31,6 +30,10 @@ import "../../../components/trace/ha-trace-logbook";
import "../../../components/trace/ha-trace-path-details";
import "../../../components/trace/ha-trace-timeline";
import { traceTabStyles } from "../../../components/trace/trace-tab-styles";
import type {
HatScriptGraph,
NodeInfo,
} from "../../../components/trace/hat-script-graph";
@customElement("ha-automation-trace")
export class HaAutomationTrace extends LitElement {
@ -65,12 +68,14 @@ export class HaAutomationTrace extends LitElement {
| "logbook"
| "blueprint" = "details";
@query("hat-script-graph") private _graph?: HatScriptGraph;
protected render(): TemplateResult {
const stateObj = this._entityId
? this.hass.states[this._entityId]
: undefined;
const graph = this.shadowRoot!.querySelector("hat-script-graph");
const graph = this._graph;
const trackedNodes = graph?.trackedNodes;
const renderedNodes = graph?.renderedNodes;
@ -294,7 +299,6 @@ export class HaAutomationTrace extends LitElement {
if (changedProps.has("_runId") && this._runId) {
this._trace = undefined;
this._logbookEntries = undefined;
this.shadowRoot!.querySelector("select")!.value = this._runId;
this._loadTrace();
}
@ -427,14 +431,13 @@ export class HaAutomationTrace extends LitElement {
this._logbookEntries = traceInfo.logbookEntries;
}
private _showTab(ev) {
private _showTab(ev: Event) {
this._view = (ev.target as any).view;
}
private _timelinePathPicked(ev) {
private _timelinePathPicked(ev: CustomEvent) {
const path = ev.detail.value;
const nodes =
this.shadowRoot!.querySelector("hat-script-graph")!.trackedNodes;
const nodes = this._graph!.trackedNodes;
if (nodes[path]) {
this._selected = nodes[path];
}

View File

@ -6,12 +6,11 @@ import {
mdiRefresh,
} from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { formatDateTimeWithSeconds } from "../../../common/datetime/format_date_time";
import type { NodeInfo } from "../../../components/trace/hat-graph";
import "../../../components/trace/hat-script-graph";
import { getLogbookDataForContext, LogbookEntry } from "../../../data/logbook";
import { ScriptEntity } from "../../../data/script";
@ -31,6 +30,10 @@ import "../../../components/trace/ha-trace-config";
import "../../../components/trace/ha-trace-logbook";
import "../../../components/trace/ha-trace-path-details";
import "../../../components/trace/ha-trace-timeline";
import type {
HatScriptGraph,
NodeInfo,
} from "../../../components/trace/hat-script-graph";
@customElement("ha-script-trace")
export class HaScriptTrace extends LitElement {
@ -63,12 +66,14 @@ export class HaScriptTrace extends LitElement {
| "logbook"
| "blueprint" = "details";
@query("hat-script-graph") private _graph?: HatScriptGraph;
protected render(): TemplateResult {
const stateObj = this.scriptEntityId
? this.hass.states[this.scriptEntityId]
: undefined;
const graph = this.shadowRoot!.querySelector("hat-script-graph");
const graph = this._graph;
const trackedNodes = graph?.trackedNodes;
const renderedNodes = graph?.renderedNodes;
@ -274,10 +279,10 @@ export class HaScriptTrace extends LitElement {
this._loadTraces(params.get("run_id") || undefined);
}
protected updated(changedProps) {
super.updated(changedProps);
public willUpdate(changedProps) {
super.willUpdate(changedProps);
// Only reset if automationId has changed and we had one before.
// Only reset if scriptEntityId has changed and we had one before.
if (changedProps.get("scriptEntityId")) {
this._traces = undefined;
this._runId = undefined;
@ -291,7 +296,6 @@ export class HaScriptTrace extends LitElement {
if (changedProps.has("_runId") && this._runId) {
this._trace = undefined;
this._logbookEntries = undefined;
this.shadowRoot!.querySelector("select")!.value = this._runId;
this._loadTrace();
}
}
@ -417,14 +421,13 @@ export class HaScriptTrace extends LitElement {
this._logbookEntries = traceInfo.logbookEntries;
}
private _showTab(ev) {
private _showTab(ev: Event) {
this._view = (ev.target as any).view;
}
private _timelinePathPicked(ev) {
private _timelinePathPicked(ev: CustomEvent) {
const path = ev.detail.value;
const nodes =
this.shadowRoot!.querySelector("hat-script-graph")!.trackedNodes;
const nodes = this._graph!.trackedNodes;
if (nodes[path]) {
this._selected = nodes[path];
}