1/*
2 * Copyright (C) 2022 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16import { BaseElement, element } from '../../../base-ui/BaseElement';
17import { TraceRow } from './base/TraceRow';
18import { dpr } from './base/Extension';
19import {
20  drawFlagLineSegment,
21  drawLines,
22  drawLinkLines,
23  drawLogsLineSegment,
24  drawWakeUp,
25  drawWakeUpList,
26  PairPoint,
27  Rect,
28} from '../../database/ui-worker/ProcedureWorkerCommon';
29import { Flag } from './timer-shaft/Flag';
30import { TimerShaftElement } from './TimerShaftElement';
31import { CpuStruct } from '../../database/ui-worker/cpu/ProcedureWorkerCPU';
32import { WakeupBean } from '../../bean/WakeupBean';
33import { LitIcon } from '../../../base-ui/icon/LitIcon';
34
35const maxScale = 0.8; //收藏最大高度为界面最大高度的80%
36const topHeight = 150; // 顶部cpu使用率部分高度固定为150px
37const minHeight = 40; //泳道最低高度为40
38const mouseMoveRange = 5;
39
40@element('sp-chart-list')
41export class SpChartList extends BaseElement {
42  private static COLLECT_G1 = '1';
43  private static COLLECT_G2 = '2';
44  private collectEl1: HTMLDivElement | null | undefined;
45  private collectEl2: HTMLDivElement | null | undefined;
46  private groupTitle1: HTMLDivElement | null | undefined;
47  private groupTitle2: HTMLDivElement | null | undefined;
48  private icon1: LitIcon | null | undefined;
49  private icon2: LitIcon | null | undefined;
50  private removeCollectIcon1: LitIcon | null | undefined;
51  private removeCollectIcon2: LitIcon | null | undefined;
52  private rootEl: HTMLDivElement | null | undefined;
53  private fragmentGroup1: DocumentFragment = document.createDocumentFragment();
54  private fragmentGroup2: DocumentFragment = document.createDocumentFragment();
55  private canvas: HTMLCanvasElement | null | undefined; //绘制收藏泳道图
56  private canvasCtx: CanvasRenderingContext2D | undefined | null;
57  private canResize: boolean = false;
58  private isPress: boolean = false;
59  private startPageY = 0;
60  private startClientHeight: number = 0;
61  private scrollTimer: unknown;
62  private collect1Expand: boolean = true;
63  private collect2Expand: boolean = true;
64  // @ts-ignore
65  private collectRowList1: Array<TraceRow<unknown>> = [];
66  // @ts-ignore
67  private collectRowList2: Array<TraceRow<unknown>> = [];
68  private maxHeight = 0;
69  private manualHeight = 0;
70
71  initElements(): void {
72    this.collectEl1 = this.shadowRoot?.querySelector<HTMLDivElement>('#collect-group-1');
73    this.collectEl2 = this.shadowRoot?.querySelector<HTMLDivElement>('#collect-group-2');
74    this.groupTitle1 = this.shadowRoot?.querySelector<HTMLDivElement>('#group-1-title');
75    this.groupTitle2 = this.shadowRoot?.querySelector<HTMLDivElement>('#group-2-title');
76    this.icon1 = this.shadowRoot?.querySelector<LitIcon>('#group_1_expand');
77    this.icon2 = this.shadowRoot?.querySelector<LitIcon>('#group_2_expand');
78    this.removeCollectIcon1 = this.shadowRoot?.querySelector<LitIcon>('#group_1_collect');
79    this.removeCollectIcon2 = this.shadowRoot?.querySelector<LitIcon>('#group_2_collect');
80    this.rootEl = this.shadowRoot?.querySelector<HTMLDivElement>('.root');
81    this.canvas = this.shadowRoot?.querySelector<HTMLCanvasElement>('.panel-canvas');
82    this.canvasCtx = this.canvas?.getContext('2d'); //@ts-ignore
83    window.subscribe(window.SmartEvent.UI.RowHeightChange, (data: { expand: number; value: number }) => {
84      this.resizeHeight();
85      if (!data.expand) {
86        let offset = this.scrollTop - data.value;
87        offset = offset < 0 ? 0 : offset;
88        this.scrollTop = offset;
89      }
90      this.refreshFavoriteCanvas();
91    });
92    this.initChartListListener();
93  }
94
95  private initChartListListener(): void {
96    const foldCollect1 = (): void => {
97      this.collect1Expand = !this.collect1Expand;
98      if (this.collect1Expand) {
99        this.icon1!.style.transform = 'rotateZ(0deg)';
100        this.collectEl1?.appendChild(this.fragmentGroup1);
101      } else {
102        this.icon1!.style.transform = 'rotateZ(-90deg)';
103        this.collectRowList1.forEach((row) => this.fragmentGroup1.appendChild(row));
104      }
105      this.resizeHeight();
106    };
107    this.icon1?.addEventListener('click', () => foldCollect1());
108    const foldCollect2 = (): void => {
109      this.collect2Expand = !this.collect2Expand;
110      if (this.collect2Expand) {
111        this.icon2!.style.transform = 'rotateZ(0deg)';
112        this.collectEl2?.appendChild(this.fragmentGroup2);
113        this.scrollTop = this.scrollHeight;
114      } else {
115        this.icon2!.style.transform = 'rotateZ(-90deg)';
116        this.collectRowList2.forEach((row) => this.fragmentGroup2.appendChild(row));
117        this.scrollTop = 0;
118      }
119      this.resizeHeight();
120    };
121    this.icon2?.addEventListener('click', () => foldCollect2());
122    document.addEventListener('keyup', (e) => {
123      if (e.key.toLowerCase() === 'b' && e.ctrlKey === false) {
124        // 收藏夹有泳道时 为true
125        const hasChildNode1 = this.collectEl1?.hasChildNodes() || this.fragmentGroup1.hasChildNodes();
126        const hasChildNode2 = this.collectEl2?.hasChildNodes() || this.fragmentGroup2.hasChildNodes();
127        // 两个收藏夹都有泳道时
128        if (hasChildNode1 && hasChildNode2) {
129          const flag = this.collect1Expand === this.collect2Expand;
130          if (flag) {
131            foldCollect1();
132            foldCollect2();
133          } else {
134            // 两收藏夹的折叠状态不一致 优先一起折叠
135            if (this.collect1Expand) {
136              foldCollect1();
137            }
138            else {
139              foldCollect2();
140            }
141          }
142          return;
143        }
144        // 只影响有泳道的收藏夹
145        if (hasChildNode1) {
146          foldCollect1();
147        }
148        if (hasChildNode2) {
149          foldCollect2();
150        }
151      }
152    });
153
154    this.removeCollectIcon1?.addEventListener('click', () => {
155      Array.from(this.collectRowList1).forEach(row => {
156        row.collectEL?.click();
157      });
158    });
159    this.removeCollectIcon2?.addEventListener('click', () => {
160      Array.from(this.collectRowList2).forEach(row => {
161        row.collectEL?.click();
162      });
163    });
164  }
165
166  removeAllCollectRow(): void {
167    Array.from(this.collectRowList1).forEach(row => {
168      row.collectEL?.click();
169    });
170    Array.from(this.collectRowList2).forEach(row => {
171      row.collectEL?.click();
172    });
173  }
174
175  private resizeHeight(): void {
176    this.maxHeight = 0;
177    // @ts-ignore
178    this.collectEl1!.childNodes.forEach((item) => (this.maxHeight += (item as unknown).clientHeight));
179    // @ts-ignore
180    this.collectEl2!.childNodes.forEach((item) => (this.maxHeight += (item as unknown).clientHeight));
181    if (this.groupTitle1) {
182      this.maxHeight += this.groupTitle1.clientHeight;
183    }
184    if (this.groupTitle2) {
185      this.maxHeight += this.groupTitle2.clientHeight;
186    }
187
188    this.maxHeight = Math.min(this.getMaxLimitHeight(), this.maxHeight);
189    if (this.manualHeight > 0) {
190      this.style.height = `${Math.min(this.maxHeight, this.manualHeight)}px`;
191    } else {
192      this.style.height = `${this.maxHeight}px`;
193    }
194  }
195
196  private getMaxLimitHeight(): number {
197    return (this.parentElement!.clientHeight - topHeight) * maxScale;
198  }
199
200  // @ts-ignore
201  getCollectRows(filter?: (row: TraceRow<unknown>) => boolean): Array<TraceRow<unknown>> | [] {
202    if (filter) {
203      return [...this.collectRowList1.filter(filter), ...this.collectRowList2.filter(filter)];
204    } else {
205      return this.getAllCollectRows();
206    }
207  }
208
209  getRowScrollTop(): number {
210    return this.rootEl?.scrollTop || 0;
211  }
212
213  // @ts-ignore
214  expandSearchRowGroup(row: TraceRow<unknown>): void {
215    this.updateGroupDisplay();
216    if (row.collectGroup === SpChartList.COLLECT_G1) {
217      if (!this.collect1Expand) {
218        this.collect1Expand = true;
219        this.icon1!.style.transform = 'rotateZ(0deg)';
220        this.collectEl1?.appendChild(this.fragmentGroup1);
221      }
222    } else {
223      if (!this.collect2Expand) {
224        this.collect2Expand = true;
225        this.icon2!.style.transform = 'rotateZ(0deg)';
226        this.collectEl2?.appendChild(this.fragmentGroup2);
227        this.scrollTop = this.scrollHeight;
228      }
229    }
230    this.resizeHeight();
231  }
232
233  // @ts-ignore
234  getCollectRow(filter: (row: TraceRow<unknown>) => boolean): TraceRow<unknown> | undefined {
235    return this.collectRowList1.find(filter) || this.collectRowList2.find(filter);
236  }
237
238  // @ts-ignore
239  getAllCollectRows(): Array<TraceRow<unknown>> {
240    return [...this.collectRowList1, ...this.collectRowList2];
241  }
242
243  getCollectRowsInfo(group: string): unknown {
244    return (group === SpChartList.COLLECT_G1 ? this.collectRowList1 : this.collectRowList2).map((row) => {
245      let rowJson = {
246        type: row.rowType,
247        name: row.name,
248        id: row.rowId,
249        parents: [],
250      };
251      this.getRowParent(rowJson, row);
252      rowJson.parents.reverse();
253      return rowJson;
254    });
255  }
256
257  // @ts-ignore
258  getRowParent(obj: unknown, row: TraceRow<unknown>): void {
259    if (row.parentRowEl) {
260      // @ts-ignore
261      if (obj.parents) {
262        let parent: unknown = {
263          type: row.parentRowEl.rowType,
264          name: row.parentRowEl.name,
265          id: row.parentRowEl.rowId,
266        };
267        // @ts-ignore
268        (obj.parents as Array<unknown>).push(parent);
269      } else {
270        // @ts-ignore
271        obj.parents = [parent];
272      }
273      this.getRowParent(obj, row.parentRowEl);
274    }
275  }
276
277  // @ts-ignore
278  getAllSelectCollectRows(): Array<TraceRow<unknown>> {
279    // @ts-ignore
280    const rows: Array<TraceRow<unknown>> = [];
281    for (const row of this.collectRowList1) {
282      if (row.checkType === '2') {
283        rows.push(row);
284      }
285    }
286    for (const row of this.collectRowList2) {
287      if (row.checkType === '2') {
288        rows.push(row);
289      }
290    }
291    return rows;
292  }
293
294  insertRowBefore(node: Node, child: Node): void {
295    // @ts-ignore
296    if (child === null || (child as TraceRow<unknown>).collectGroup === (node as TraceRow<unknown>).collectGroup) {
297      // @ts-ignore
298      if ((node as TraceRow<unknown>).collectGroup === SpChartList.COLLECT_G1) {
299        this.collectEl1!.insertBefore(node, child);
300        // @ts-ignore
301        this.collectRowList1 = Array.from(this.collectEl1!.children) as TraceRow<unknown>[];
302      } else {
303        this.collectEl2!.insertBefore(node, child);
304        // @ts-ignore
305        this.collectRowList2 = Array.from(this.collectEl2!.children) as TraceRow<unknown>[];
306      }
307    }
308  }
309
310  reset(): void {
311    this.maxHeight = 0;
312    this.clearRect();
313    this.collect1Expand = true;
314    this.collect2Expand = true;
315    this.icon1!.style.transform = 'rotateZ(0deg)';
316    this.icon2!.style.transform = 'rotateZ(0deg)';
317    this.collectRowList1.forEach((row) => {
318      row.clearMemory();
319    });
320    this.collectRowList2.forEach((row) => {
321      row.clearMemory();
322    });
323    this.collectRowList1 = [];
324    this.collectRowList2 = [];
325    this.fragmentGroup1 = document.createDocumentFragment();
326    this.fragmentGroup2 = document.createDocumentFragment();
327    this.collectEl1!.innerHTML = '';
328    this.collectEl2!.innerHTML = '';
329    this.updateGroupDisplay();
330    this.style.height = 'auto';
331  }
332
333  context(): CanvasRenderingContext2D | undefined | null {
334    return this.canvasCtx;
335  }
336
337  getCanvas(): HTMLCanvasElement | null | undefined {
338    return this.canvas;
339  }
340
341  connectedCallback(): void {
342    super.connectedCallback();
343    const vessel = this.parentNode as HTMLDivElement;
344    vessel.addEventListener('mousedown', this.onMouseDown);
345    vessel.addEventListener('mouseup', this.onMouseUp);
346    vessel.addEventListener('mousemove', this.onMouseMove);
347    this.addEventListener('scroll', this.onScroll, { passive: true });
348  }
349
350  disconnectedCallback(): void {
351    super.disconnectedCallback();
352    const vessel = this.parentNode as HTMLDivElement;
353    vessel.removeEventListener('mousedown', this.onMouseDown);
354    vessel.removeEventListener('mouseup', this.onMouseUp);
355    vessel.removeEventListener('mousemove', this.onMouseMove);
356    this.removeEventListener('scroll', this.onScroll);
357  }
358
359  onScroll = (ev: Event): void => {
360    this.canvas!.style.transform = `translateY(${this.scrollTop}px)`;
361    if (this.scrollTimer) {
362      // @ts-ignore
363      clearTimeout(this.scrollTimer);
364    }
365    this.scrollTimer = setTimeout(() => {
366      TraceRow.range!.refresh = true;
367      window.publish(window.SmartEvent.UI.RefreshCanvas, {});
368    }, 100);
369    window.publish(window.SmartEvent.UI.RefreshCanvas, {});
370  };
371
372  onMouseDown = (ev: MouseEvent): void => {
373    this.isPress = true;
374    this.startPageY = ev.pageY;
375    this.startClientHeight = this.clientHeight;
376    if (this.containPoint(ev)) {
377      if (
378        this.getBoundingClientRect().bottom > ev.pageY - mouseMoveRange &&
379        this.getBoundingClientRect().bottom < ev.pageY + mouseMoveRange
380      ) {
381        this.style.cursor = 'row-resize';
382        this.canResize = true;
383      } else {
384        this.style.cursor = 'default';
385        this.canResize = false;
386      }
387      // @ts-ignore
388      (window as unknown).collectResize = this.canResize;
389    }
390  };
391
392  onMouseMove = (ev: MouseEvent): void => {
393    if (this.containPoint(ev)) {
394      let inResizeArea =
395        this.getBoundingClientRect().bottom > ev.pageY - mouseMoveRange &&
396        this.getBoundingClientRect().bottom < ev.pageY + mouseMoveRange;
397      if ((this.isPress && this.canResize) || inResizeArea) {
398        this.style.cursor = 'row-resize';
399      } else {
400        this.style.cursor = 'default';
401      }
402    }
403    //防止点击触发move时间
404    if (Math.abs(ev.pageY - this.startPageY) < 2) {
405      return;
406    }
407    if (this.canResize && this.isPress) {
408      // @ts-ignore
409      (window as unknown).collectResize = true;
410      // 拖动超过所有泳道最大高度 或小于一个泳道的高度,不支持拖动
411      let newHeight = this.startClientHeight + ev.pageY - this.startPageY;
412      if (newHeight > this.maxHeight) {
413        newHeight = this.maxHeight;
414      }
415      if (newHeight > this.getMaxLimitHeight()) {
416        newHeight = this.getMaxLimitHeight();
417      }
418      if (newHeight < minHeight) {
419        newHeight = minHeight;
420      }
421      this!.style.height = `${newHeight}px`;
422      this.manualHeight = newHeight;
423    } else {
424      // @ts-ignore
425      (window as unknown).collectResize = false;
426    }
427  };
428
429  onMouseUp = (ev: MouseEvent): void => {
430    this.isPress = false;
431    this.canResize = false;
432    this.style.cursor = 'default';
433    // @ts-ignore
434    (window as unknown).collectResize = false;
435    if (this.style.display === 'flex') {
436      this.refreshFavoriteCanvas();
437    }
438  };
439
440  // @ts-ignore
441  insertRow(row: TraceRow<unknown>, group: string, updateGroup: boolean): void {
442    this.style.display = 'flex';
443    let collectGroup = !updateGroup && row.collectGroup ? row.collectGroup : group;
444    if (row.collectGroup !== SpChartList.COLLECT_G1 && row.collectGroup !== SpChartList.COLLECT_G2) {
445      row.collectGroup = group;
446    }
447    if (updateGroup) {
448      row.collectGroup = group;
449    }
450    if (collectGroup === SpChartList.COLLECT_G1) {
451      if (!this.collect1Expand) {
452        this.collect1Expand = true;
453        this.icon1!.style.transform = 'rotateZ(0deg)';
454      }
455      if (this.collectRowList1.indexOf(row) === -1) {
456        this.collectRowList1.push(row);
457      }
458      if (!this.fragmentGroup1.contains(row)) {
459        this.fragmentGroup1.appendChild(row);
460      }
461      this.collectEl1?.appendChild(this.fragmentGroup1);
462      this.scrollTo({ top: this.collectEl1?.clientHeight });
463    } else {
464      if (!this.collect2Expand) {
465        this.collect2Expand = true;
466        this.icon2!.style.transform = 'rotateZ(0deg)';
467      }
468      if (this.collectRowList2.indexOf(row) === -1) {
469        this.collectRowList2.push(row);
470      }
471      if (!this.fragmentGroup2.contains(row)) {
472        this.fragmentGroup2.appendChild(row);
473      }
474      this.collectEl2!.appendChild(this.fragmentGroup2);
475      this.scrollTo({ top: this.scrollHeight });
476    }
477    this.updateGroupDisplay();
478    this.resizeHeight();
479    this.refreshFavoriteCanvas();
480    row.currentContext = this.canvasCtx;
481  }
482
483  // @ts-ignore
484  deleteRow(row: TraceRow<unknown>, clearCollectGroup: boolean): void {
485    if (row.collectGroup === SpChartList.COLLECT_G1) {
486      this.collectRowList1.splice(this.collectRowList1.indexOf(row), 1);
487      if (!this.fragmentGroup1.contains(row)) {
488        this.fragmentGroup1.appendChild(row);
489      }
490      this.fragmentGroup1.removeChild(row);
491    } else {
492      this.collectRowList2.splice(this.collectRowList2.indexOf(row), 1);
493      if (!this.fragmentGroup2.contains(row)) {
494        this.fragmentGroup2.appendChild(row);
495      }
496      this.fragmentGroup2.removeChild(row);
497    }
498    if (clearCollectGroup) {
499      row.collectGroup = undefined;
500    }
501    this.updateGroupDisplay();
502    this.resizeHeight();
503    this.scrollTop = 0;
504    this.refreshFavoriteCanvas();
505    row.currentContext = undefined;
506    if (this.collectRowList1.length === 0 && this.collectRowList2.length === 0) {
507      this.style.height = 'auto';
508      this.style.display = 'none';
509      this.manualHeight = 0;
510    }
511  }
512
513  hideCollectArea(): void {
514    if (this.collect1Expand) {
515      this.collectRowList1.forEach((row) => this.fragmentGroup1.appendChild(row));
516    }
517    if (this.collect2Expand) {
518      this.collectRowList2.forEach((row) => this.fragmentGroup2.appendChild(row));
519    }
520    this.groupTitle1!.style.display = 'none';
521    this.groupTitle2!.style.display = 'none';
522    this.resizeHeight();
523  }
524
525  showCollectArea(): void {
526    if (this.collect1Expand) {
527      this.collectEl1?.appendChild(this.fragmentGroup1);
528    }
529    if (this.collect2Expand) {
530      this.collectEl2?.appendChild(this.fragmentGroup2);
531    }
532    this.updateGroupDisplay();
533    this.resizeHeight();
534  }
535
536  updateGroupDisplay(): void {
537    this.groupTitle1!.style.display = this.collectRowList1.length === 0 ? 'none' : 'flex';
538    this.groupTitle2!.style.display = this.collectRowList2.length === 0 ? 'none' : 'flex';
539  }
540
541  hasCollectRow(): boolean {
542    return this.collectRowList2.length > 0 || this.collectRowList1.length > 0;
543  }
544
545  clearRect(): void {
546    this.canvasCtx?.clearRect(0, 0, this.canvas?.clientWidth ?? 0, this.canvas?.clientHeight ?? 0);
547  }
548
549  drawLines(xs: number[] | undefined, color: string): void {
550    drawLines(this.canvasCtx!, xs ?? [], this.clientHeight, color);
551  }
552
553  drawFlagLineSegment(
554    hoverFlag: Flag | undefined | null,
555    selectFlag: Flag | undefined | null,
556    tse: TimerShaftElement
557  ): void {
558    drawFlagLineSegment(
559      this.canvasCtx,
560      hoverFlag,
561      selectFlag,
562      new Rect(0, 0, TraceRow.FRAME_WIDTH, this.canvas?.clientHeight!),
563      tse
564    );
565  }
566
567  drawWakeUp(): void {
568    drawWakeUp(
569      this.canvasCtx,
570      CpuStruct.wakeupBean,
571      TraceRow.range!.startNS,
572      TraceRow.range!.endNS,
573      TraceRow.range!.totalNS,
574      new Rect(0, 0, TraceRow.FRAME_WIDTH, this.canvas?.clientHeight!)
575    );
576  }
577
578  drawWakeUpList(bean: WakeupBean): void {
579    drawWakeUpList(this.canvasCtx, bean, TraceRow.range!.startNS, TraceRow.range!.endNS, TraceRow.range!.totalNS, {
580      x: 0,
581      y: 0,
582      width: TraceRow.FRAME_WIDTH,
583      height: this.canvas!.clientHeight!,
584    } as Rect);
585  }
586
587  drawLogsLineSegment(bean: Flag | null | undefined, timeShaft: TimerShaftElement): void {
588    drawLogsLineSegment(
589      this.canvasCtx,
590      bean,
591      {
592        x: 0,
593        y: 0,
594        width: TraceRow.FRAME_WIDTH,
595        height: this.canvas!.clientHeight,
596      },
597      timeShaft
598    );
599  }
600
601  drawLinkLines(nodes: PairPoint[][], tse: TimerShaftElement, isFavorite: boolean, favoriteHeight: number): void {
602    drawLinkLines(this.canvasCtx!, nodes, tse, isFavorite, favoriteHeight);
603  }
604
605  refreshFavoriteCanvas(): void {
606    this.canvas!.style.width = `${this.clientWidth - 248}px`;
607    this.canvas!.style.left = `248px`;
608    this.canvas!.width = this.canvas?.clientWidth! * dpr();
609    this.canvas!.height = this.clientHeight * dpr();
610    this.canvas!.getContext('2d')!.scale(dpr(), dpr());
611    window.publish(window.SmartEvent.UI.RefreshCanvas, {});
612  }
613
614  private getHtmlCss(): string {
615    return `<style>
616    :host{
617        display: none;
618        width: 100%;
619        height: auto;
620        overflow-anchor: none;
621        z-index: 3;
622        box-shadow: 0 10px 10px #00000044;
623        position: relative;
624        overflow: auto;
625        overflow-x: hidden;
626        scroll-behavior: smooth;
627    }
628    .root{
629        width: 100%;
630        box-sizing: border-box;
631    }
632    .panel-canvas{
633        position: absolute;
634        top: 0;
635        right: 0;
636        bottom: 0;
637        box-sizing: border-box;
638    }
639    .icon:hover {
640     color:#ecb93f;
641    }
642    .icon {
643        margin-right: 10px;
644        cursor: pointer;
645    }
646    </style>`;
647  }
648
649  initHtml(): string {
650    return `
651 ${this.getHtmlCss()}
652<canvas id="canvas-panel" class="panel-canvas" ondragstart="return false"></canvas>
653<div class="root">
654    <div id="group-1-title" style="background-color: #efefef;padding: 10px;align-items: center">
655        <lit-icon id="group_1_expand" class="icon" name="caret-down" size="19"></lit-icon>
656        <span style="width: 184px;font-size: 10px;color: #898989">G1</span>
657        <lit-icon id="group_1_collect" name="star-fill" style="color: #5291FF;cursor: pointer" size="19"></lit-icon>
658    </div>
659    <div id="collect-group-1"></div>
660    <div id="group-2-title" style="background-color: #efefef;padding: 10px;align-items: center">
661        <lit-icon id="group_2_expand" class="icon" name="caret-down" size="19"></lit-icon>
662        <span style="width: 184px;font-size: 10px;color: #898989">G2</span>
663        <lit-icon id="group_2_collect" name="star-fill" style="color: #f56940;cursor: pointer" size="19"></lit-icon>
664    </div>
665    <div id="collect-group-2"></div>
666</div>
667`;
668  }
669}
670