diff --git a/src/components/trace/ha-trace-path-details.ts b/src/components/trace/ha-trace-path-details.ts index 5ba95e8cfe..57b00ef5ac 100644 --- a/src/components/trace/ha-trace-path-details.ts +++ b/src/components/trace/ha-trace-path-details.ts @@ -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 = {}; + @property() public renderedNodes: Record = {}; @property() public trackedNodes!: Record; diff --git a/src/components/trace/ha-trace-timeline.ts b/src/components/trace/ha-trace-timeline.ts index 2680254f93..1223732a14 100644 --- a/src/components/trace/ha-trace-timeline.ts +++ b/src/components/trace/ha-trace-timeline.ts @@ -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` diff --git a/src/components/trace/hat-graph-branch.ts b/src/components/trace/hat-graph-branch.ts new file mode 100644 index 0000000000..4670f2f5a7 --- /dev/null +++ b/src/components/trace/hat-graph-branch.ts @@ -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` + + ${!this.start + ? svg` + + ${this._branches.map((branch) => + branch.start + ? "" + : svg` + + ` + )} + + ` + : ""} +
+ + ${this._branches.map((branch) => { + if (branch.end) return ""; + return svg` + + `; + })} + + +
+ + ${!this.short + ? svg` + + ${this._branches.map((branch) => { + if (branch.end) return ""; + return 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; + } +} diff --git a/src/components/trace/hat-graph-const.ts b/src/components/trace/hat-graph-const.ts new file mode 100644 index 0000000000..06a59da94f --- /dev/null +++ b/src/components/trace/hat-graph-const.ts @@ -0,0 +1,3 @@ +export const SPACING = 10; +export const NODE_SIZE = 30; +export const BRANCH_HEIGHT = 20; diff --git a/src/components/trace/hat-graph-node.ts b/src/components/trace/hat-graph-node.ts index bb418dc23e..ba0ad0a250 100644 --- a/src/components/trace/hat-graph-node.ts +++ b/src/components/trace/hat-graph-node.ts @@ -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` - - ${ - this.graphStart + return html` + + ${this.graphStart ? `` : svg` - ` - } - - - } - ${ - this.badge - ? svg` + `} + + + } + ${this.badge + ? svg` ${this.badge > 9 ? "9+" : this.badge} ` - : "" - } - - ${this.iconPath ? svg`` : ""} - - + : ""} + + ${this.iconPath ? 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)); - } - } `; } } diff --git a/src/components/trace/hat-graph-spacer.ts b/src/components/trace/hat-graph-spacer.ts index e30c05bd5a..37c79a18bb 100644 --- a/src/components/trace/hat-graph-spacer.ts +++ b/src/components/trace/hat-graph-spacer.ts @@ -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` - - + - } + line-caps="round" + /> + } `; } @@ -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; diff --git a/src/components/trace/hat-graph.ts b/src/components/trace/hat-graph.ts deleted file mode 100644 index 540ecdc44e..0000000000 --- a/src/components/trace/hat-graph.ts +++ /dev/null @@ -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` - - ${this.branching && branches.some((branch) => !branch.start) - ? svg` - - ${branches.map((branch, i) => { - if (branch.start) return ""; - return svg` - - `; - })} - - - ` - : ""} -
- ${this.branching - ? svg` - - ${branches.map((branch, i) => { - if (branch.end) return ""; - return svg` - - `; - })} - - ` - : ""} - -
- - ${this.branching && !this.short - ? svg` - - ${branches.map((branch, i) => { - if (branch.end) return ""; - return 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; - } -} diff --git a/src/components/trace/hat-script-graph.ts b/src/components/trace/hat-script-graph.ts index 70af9c6b2b..d869dd48e8 100644 --- a/src/components/trace/hat-script-graph.ts +++ b/src/components/trace/hat-script-graph.ts @@ -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 = {}; + public renderedNodes: Record = {}; - @property() trackedNodes: Record = {}; + public trackedNodes: Record = {}; 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` `; } 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` - - -
-
- -
- `; + 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` - @@ -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` - +
${ensureArray(branch.sequence).map((action, j) => - this.render_node(action, `${branch_path}/sequence/${j}`) + this.render_action_node( + action, + `${branch_path}/sequence/${j}` + ) )} - +
`; }) : ""} - - +
+ ${ensureArray(config.default)?.map((action, i) => - this.render_node(action, `${path}/default/${i}`) + this.render_action_node(action, `${path}/default/${i}`) )} - - +
+ `; } @@ -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` - -
-
+
+
-
+ `; } @@ -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"} > `; @@ -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"} > `; @@ -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"} > `; @@ -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` - 1} nofocus - class=${classMap({ - track: track_path.includes(1), - })} - .badge=${repeats} + .badge=${repeats > 1 ? repeats : undefined} > - +
${ensureArray(node.repeat.sequence).map((action, i) => - this.render_node(action, `${path}/repeat/sequence/${i}`) + this.render_action_node(action, `${path}/repeat/sequence/${i}`) )} - - +
+ `; } @@ -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"} > `; @@ -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"} > `; @@ -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"} > `; @@ -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} > `; } - 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` - -
+
${trigger_nodes - ? html` + ? html` ${trigger_nodes} - ` + ` : ""} ${"condition" in this.trace.config - ? html` - ${ensureArray(this.trace.config.condition)?.map( - (condition, i) => this.render_condition(condition!, i) - )} - ` + ? 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) )}` : ""} - +
@@ -545,44 +469,39 @@ class HatScriptGraph extends LitElement { protected updated(changedProps: PropertyValues) { 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; diff --git a/src/panels/config/automation/ha-automation-trace.ts b/src/panels/config/automation/ha-automation-trace.ts index eb007735dd..0ad7147da0 100644 --- a/src/panels/config/automation/ha-automation-trace.ts +++ b/src/panels/config/automation/ha-automation-trace.ts @@ -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]; } diff --git a/src/panels/config/script/ha-script-trace.ts b/src/panels/config/script/ha-script-trace.ts index 520e7707e4..ca0a916b92 100644 --- a/src/panels/config/script/ha-script-trace.ts +++ b/src/panels/config/script/ha-script-trace.ts @@ -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]; }