From 61a66fca6c9c9eab18fd5a06fcc7e0b138009a2f Mon Sep 17 00:00:00 2001 From: Luke Cotter <4013877+lukecotter@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:13:58 +0000 Subject: [PATCH 1/4] fix: ensure row ids are unique To avoid cases where a row would be filtered out when is has the same id as the parent. They have the same id because the timestamps could be the same --- log-viewer/modules/components/calltree-view/CalltreeView.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/log-viewer/modules/components/calltree-view/CalltreeView.ts b/log-viewer/modules/components/calltree-view/CalltreeView.ts index 813d5c2e..6966691b 100644 --- a/log-viewer/modules/components/calltree-view/CalltreeView.ts +++ b/log-viewer/modules/components/calltree-view/CalltreeView.ts @@ -759,7 +759,7 @@ export class CalltreeView extends LitElement { const isTimedNode = node instanceof TimedNode; const children = isTimedNode ? this._toCallTree(node.children) : null; const data: CalltreeRow = { - id: node.timestamp, + id: node.timestamp + '-' + i, text: node.text, namespace: node.namespace, duration: node.duration.total, @@ -819,7 +819,7 @@ export class CalltreeView extends LitElement { } interface CalltreeRow { - id: number; + id: string; originalData: LogLine; text: string; duration: number; From 79c72c5f0427f3cf193f5d2303759fccc4bfc877 Mon Sep 17 00:00:00 2001 From: Luke Cotter <4013877+lukecotter@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:14:57 +0000 Subject: [PATCH 2/4] fix: clear search results when filters change. --- log-viewer/modules/components/AnalysisView.ts | 397 +++++----- .../components/calltree-view/CalltreeView.ts | 131 ++-- .../components/database-view/DMLView.ts | 500 ++++++------ .../components/database-view/SOQLView.ts | 711 +++++++++--------- 4 files changed, 927 insertions(+), 812 deletions(-) diff --git a/log-viewer/modules/components/AnalysisView.ts b/log-viewer/modules/components/AnalysisView.ts index 659e19b1..d0b05814 100644 --- a/log-viewer/modules/components/AnalysisView.ts +++ b/log-viewer/modules/components/AnalysisView.ts @@ -26,30 +26,31 @@ import { RowNavigation } from '../datagrid/module/RowNavigation.js'; provideVSCodeDesignSystem().register(vsCodeCheckbox(), vsCodeDropdown(), vsCodeOption()); -let analysisTable: Tabulator; -let tableContainer: HTMLDivElement | null; -let findMap: { [key: number]: RowComponent } = {}; -let findArgs: { text: string; count: number; options: { matchCase: boolean } } = { - text: '', - count: 0, - options: { matchCase: false }, -}; -let totalMatches = 0; @customElement('analysis-view') export class AnalysisView extends LitElement { @property() timelineRoot: ApexLog | null = null; + analysisTable: Tabulator | null = null; + tableContainer: HTMLDivElement | null = null; + findMap: { [key: number]: RowComponent } = {}; + findArgs: { text: string; count: number; options: { matchCase: boolean } } = { + text: '', + count: 0, + options: { matchCase: false }, + }; + totalMatches = 0; + get _tableWrapper(): HTMLDivElement | null { - return (tableContainer = this.renderRoot?.querySelector('#analysis-table') ?? null); + return (this.tableContainer = this.renderRoot?.querySelector('#analysis-table') ?? null); } constructor() { super(); - document.addEventListener('lv-find', this._find as EventListener); - document.addEventListener('lv-find-match', this._find as EventListener); - document.addEventListener('lv-find-close', this._find as EventListener); + document.addEventListener('lv-find', this._findEvt); + document.addEventListener('lv-find-match', this._findEvt); + document.addEventListener('lv-find-close', this._findEvt); } updated(changedProperties: PropertyValues): void { @@ -116,11 +117,13 @@ export class AnalysisView extends LitElement { `; } + _findEvt = ((event: FindEvt) => this._find(event)) as EventListener; + _groupBy(event: Event) { const target = event.target as HTMLInputElement; const fieldName = target.value.toLowerCase(); - analysisTable.setGroupBy(fieldName !== 'none' ? fieldName : ''); + this.analysisTable?.setGroupBy(fieldName !== 'none' ? fieldName : ''); } _appendTableWhenVisible() { @@ -130,7 +133,7 @@ export class AnalysisView extends LitElement { const analysisObserver = new IntersectionObserver((entries, observer) => { const visible = entries[0]?.isIntersecting; if (visible) { - renderAnalysis(rootMethod); + this._renderAnalysis(rootMethod); observer.disconnect(); } }); @@ -138,17 +141,17 @@ export class AnalysisView extends LitElement { } } - _find = (e: CustomEvent<{ text: string; count: number; options: { matchCase: boolean } }>) => { - const isTableVisible = !!analysisTable?.element?.clientHeight; - if (!isTableVisible && !totalMatches) { + _find(e: CustomEvent<{ text: string; count: number; options: { matchCase: boolean } }>) { + const isTableVisible = !!this.analysisTable?.element?.clientHeight; + if (!isTableVisible && !this.totalMatches) { return; } const newFindArgs = JSON.parse(JSON.stringify(e.detail)); const newSearch = - newFindArgs.text !== findArgs.text || - newFindArgs.options.matchCase !== findArgs.options?.matchCase; - findArgs = newFindArgs; + newFindArgs.text !== this.findArgs.text || + newFindArgs.options.matchCase !== this.findArgs.options?.matchCase; + this.findArgs = newFindArgs; const clearHighlights = e.type === 'lv-find-close' || (!isTableVisible && newFindArgs.count === 0); @@ -157,9 +160,9 @@ export class AnalysisView extends LitElement { } if (newSearch || clearHighlights) { //@ts-expect-error This is a custom function added in by Find custom module - const result = analysisTable.find(findArgs); - totalMatches = result.totalMatches; - findMap = result.matchIndexes; + const result = this.analysisTable.find(this.findArgs); + this.totalMatches = result.totalMatches; + this.findMap = result.matchIndexes; if (!clearHighlights) { document.dispatchEvent( @@ -168,185 +171,205 @@ export class AnalysisView extends LitElement { } } - const currentRow = findMap[findArgs.count]; - const rows = [currentRow, findMap[findArgs.count + 1], findMap[findArgs.count - 1]]; + const currentRow = this.findMap[this.findArgs.count]; + const rows = [ + currentRow, + this.findMap[this.findArgs.count + 1], + this.findMap[this.findArgs.count - 1], + ]; rows.forEach((row) => { row?.reformat(); }); //@ts-expect-error This is a custom function added in by RowNavigation custom module - analysisTable.goToRow(currentRow, { scrollIfVisible: false, focusRow: false }); - }; -} - -async function renderAnalysis(rootMethod: ApexLog) { - if (!tableContainer) { - return; + this.analysisTable.goToRow(currentRow, { scrollIfVisible: false, focusRow: false }); } - const methodMap: Map = new Map(); - addNodeToMap(methodMap, rootMethod); - const metricList = [...methodMap.values()]; + async _renderAnalysis(rootMethod: ApexLog) { + if (!this.tableContainer) { + return; + } + const methodMap: Map = new Map(); - const headerMenu = [ - { - label: 'Export to CSV', - action: function (_e: PointerEvent, column: ColumnComponent) { - column.getTable().download('csv', 'analysis.csv', { bom: true, delimiter: ',' }); - }, - }, - ]; + addNodeToMap(methodMap, rootMethod); + const metricList = [...methodMap.values()]; - Tabulator.registerModule(Object.values(CommonModules)); - Tabulator.registerModule([RowKeyboardNavigation, RowNavigation, Find]); - analysisTable = new Tabulator(tableContainer, { - rowKeyboardNavigation: true, - selectableRows: 1, - data: metricList, - layout: 'fitColumns', - placeholder: 'No Analysis Available', - columnCalcs: 'both', - clipboard: true, - downloadEncoder: function (fileContents: string, mimeType) { - const vscodeHost = vscodeMessenger.getVsCodeAPI(); - if (vscodeHost) { - vscodeMessenger.send('saveFile', { - fileContent: fileContents, - options: { - defaultFileName: 'analysis.csv', - }, - }); - return false; - } - - return new Blob([fileContents], { type: mimeType }); - }, - downloadRowRange: 'all', - downloadConfig: { - columnHeaders: true, - columnGroups: true, - rowGroups: true, - columnCalcs: false, - dataTree: true, - }, - //@ts-expect-error types need update array is valid - keybindings: { copyToClipboard: ['ctrl + 67', 'meta + 67'] }, - clipboardCopyRowRange: 'all', - height: '100%', - groupClosedShowCalcs: true, - groupStartOpen: false, - groupToggleElement: 'header', - rowFormatter: (row: RowComponent) => { - formatter(row, findArgs); - }, - columnDefaults: { - title: 'default', - resizable: true, - headerSortStartingDir: 'desc', - headerTooltip: true, - headerMenu: headerMenu, - headerWordWrap: true, - }, - initialSort: [{ column: 'selfTime', dir: 'desc' }], - headerSortElement: function (column, dir) { - switch (dir) { - case 'asc': - return "
"; - break; - case 'desc': - return "
"; - break; - default: - return "
"; - } - }, - columns: [ + const headerMenu = [ { - title: 'Name', - field: 'name', - formatter: 'textarea', - headerSortStartingDir: 'asc', - sorter: 'string', - cssClass: 'datagrid-code-text', - bottomCalc: () => { - return 'Total'; + label: 'Export to CSV', + action: function (_e: PointerEvent, column: ColumnComponent) { + column.getTable().download('csv', 'analysis.csv', { bom: true, delimiter: ',' }); }, - widthGrow: 5, }, - { - title: 'Namespace', - field: 'namespace', - headerSortStartingDir: 'desc', - width: 150, - sorter: 'string', - cssClass: 'datagrid-code-text', - tooltip: true, - headerFilter: 'list', - headerFilterFunc: 'in', - headerFilterParams: { - valuesLookup: 'all', - clearable: true, - multiselect: true, - }, - headerFilterLiveFilter: false, + ]; + + Tabulator.registerModule(Object.values(CommonModules)); + Tabulator.registerModule([RowKeyboardNavigation, RowNavigation, Find]); + this.analysisTable = new Tabulator(this.tableContainer, { + rowKeyboardNavigation: true, + selectableRows: 1, + data: metricList, + layout: 'fitColumns', + placeholder: 'No Analysis Available', + columnCalcs: 'both', + clipboard: true, + downloadEncoder: function (fileContents: string, mimeType) { + const vscodeHost = vscodeMessenger.getVsCodeAPI(); + if (vscodeHost) { + vscodeMessenger.send('saveFile', { + fileContent: fileContents, + options: { + defaultFileName: 'analysis.csv', + }, + }); + return false; + } + + return new Blob([fileContents], { type: mimeType }); }, - { - title: 'Type', - field: 'type', - headerSortStartingDir: 'asc', - width: 150, - sorter: 'string', - tooltip: true, - cssClass: 'datagrid-code-text', + downloadRowRange: 'all', + downloadConfig: { + columnHeaders: true, + columnGroups: true, + rowGroups: true, + columnCalcs: false, + dataTree: true, }, - { - title: 'Count', - field: 'count', - sorter: 'number', - width: 65, - hozAlign: 'right', - headerHozAlign: 'right', - bottomCalc: 'sum', + //@ts-expect-error types need update array is valid + keybindings: { copyToClipboard: ['ctrl + 67', 'meta + 67'] }, + clipboardCopyRowRange: 'all', + height: '100%', + groupClosedShowCalcs: true, + groupStartOpen: false, + groupToggleElement: 'header', + rowFormatter: (row: RowComponent) => { + formatter(row, this.findArgs); }, - { - title: 'Total Time (ms)', - field: 'totalTime', - sorter: 'number', - width: 165, - hozAlign: 'right', - headerHozAlign: 'right', - formatter: progressFormatter, - formatterParams: { - thousand: false, - precision: 3, - totalValue: rootMethod.duration.total, - }, - accessorDownload: NumberAccessor, - bottomCalcFormatter: progressFormatter, - bottomCalc: 'max', - bottomCalcFormatterParams: { precision: 3, totalValue: rootMethod.duration.total }, + columnDefaults: { + title: 'default', + resizable: true, + headerSortStartingDir: 'desc', + headerTooltip: true, + headerMenu: headerMenu, + headerWordWrap: true, }, - { - title: 'Self Time (ms)', - field: 'selfTime', - sorter: 'number', - width: 165, - hozAlign: 'right', - headerHozAlign: 'right', - bottomCalc: 'sum', - bottomCalcFormatterParams: { precision: 3, totalValue: rootMethod.duration.total }, - formatter: progressFormatter, - formatterParams: { - thousand: false, - precision: 3, - totalValue: rootMethod.duration.total, - }, - accessorDownload: NumberAccessor, - bottomCalcFormatter: progressFormatter, + initialSort: [{ column: 'selfTime', dir: 'desc' }], + headerSortElement: function (column, dir) { + switch (dir) { + case 'asc': + return "
"; + break; + case 'desc': + return "
"; + break; + default: + return "
"; + } }, - ], - }); -} + columns: [ + { + title: 'Name', + field: 'name', + formatter: 'textarea', + headerSortStartingDir: 'asc', + sorter: 'string', + cssClass: 'datagrid-code-text', + bottomCalc: () => { + return 'Total'; + }, + widthGrow: 5, + }, + { + title: 'Namespace', + field: 'namespace', + headerSortStartingDir: 'desc', + width: 150, + sorter: 'string', + cssClass: 'datagrid-code-text', + tooltip: true, + headerFilter: 'list', + headerFilterFunc: 'in', + headerFilterParams: { + valuesLookup: 'all', + clearable: true, + multiselect: true, + }, + headerFilterLiveFilter: false, + }, + { + title: 'Type', + field: 'type', + headerSortStartingDir: 'asc', + width: 150, + sorter: 'string', + tooltip: true, + cssClass: 'datagrid-code-text', + }, + { + title: 'Count', + field: 'count', + sorter: 'number', + width: 65, + hozAlign: 'right', + headerHozAlign: 'right', + bottomCalc: 'sum', + }, + { + title: 'Total Time (ms)', + field: 'totalTime', + sorter: 'number', + width: 165, + hozAlign: 'right', + headerHozAlign: 'right', + formatter: progressFormatter, + formatterParams: { + thousand: false, + precision: 3, + totalValue: rootMethod.duration.total, + }, + accessorDownload: NumberAccessor, + bottomCalcFormatter: progressFormatter, + bottomCalc: 'max', + bottomCalcFormatterParams: { precision: 3, totalValue: rootMethod.duration.total }, + }, + { + title: 'Self Time (ms)', + field: 'selfTime', + sorter: 'number', + width: 165, + hozAlign: 'right', + headerHozAlign: 'right', + bottomCalc: 'sum', + bottomCalcFormatterParams: { precision: 3, totalValue: rootMethod.duration.total }, + formatter: progressFormatter, + formatterParams: { + thousand: false, + precision: 3, + totalValue: rootMethod.duration.total, + }, + accessorDownload: NumberAccessor, + bottomCalcFormatter: progressFormatter, + }, + ], + }); + + this.analysisTable.on('dataFiltering', () => { + this._resetFindWidget(); + this._clearSearchHighlights(); + }); + } + + _resetFindWidget() { + document.dispatchEvent(new CustomEvent('lv-find-results', { detail: { totalMatches: 0 } })); + } + _clearSearchHighlights() { + this._find( + new CustomEvent('lv-find', { + detail: { text: '', count: 0, options: { matchCase: false } }, + }), + ); + } +} export class Metric { name: string; type; @@ -390,3 +413,5 @@ type VSCodeSaveFile = { defaultFileName: string; }; }; + +type FindEvt = CustomEvent<{ text: string; count: number; options: { matchCase: boolean } }>; diff --git a/log-viewer/modules/components/calltree-view/CalltreeView.ts b/log-viewer/modules/components/calltree-view/CalltreeView.ts index 6966691b..3f1f7901 100644 --- a/log-viewer/modules/components/calltree-view/CalltreeView.ts +++ b/log-viewer/modules/components/calltree-view/CalltreeView.ts @@ -36,10 +36,6 @@ import { MiddleRowFocus } from './module/MiddleRowFocus.js'; provideVSCodeDesignSystem().register(vsCodeCheckbox(), vsCodeDropdown(), vsCodeOption()); -let calltreeTable: Tabulator; -let tableContainer: HTMLDivElement | null; -let rootMethod: ApexLog | null; - @customElement('call-tree-view') export class CalltreeView extends LitElement { @property() @@ -57,8 +53,20 @@ export class CalltreeView extends LitElement { findMap: { [key: number]: RowComponent } = {}; totalMatches = 0; + canClearSearchHighlights = false; + searchString = ''; + findArgs: { text: string; count: number; options: { matchCase: boolean } } = { + text: '', + count: 0, + options: { matchCase: false }, + }; + + calltreeTable: Tabulator | null = null; + tableContainer: HTMLDivElement | null = null; + rootMethod: ApexLog | null = null; + get _callTreeTableWrapper(): HTMLDivElement | null { - return (tableContainer = this.renderRoot?.querySelector('#call-tree-table') ?? null); + return (this.tableContainer = this.renderRoot?.querySelector('#call-tree-table') ?? null); } constructor() { @@ -68,9 +76,9 @@ export class CalltreeView extends LitElement { this._goToRow(e.detail.timestamp); }) as EventListener); - document.addEventListener('lv-find', this._find as EventListener); - document.addEventListener('lv-find-match', this._find as EventListener); - document.addEventListener('lv-find-close', this._find as EventListener); + document.addEventListener('lv-find', this._findEvt); + document.addEventListener('lv-find-match', this._findEvt); + document.addEventListener('lv-find-close', this._findEvt); } updated(changedProperties: PropertyValues): void { @@ -199,6 +207,8 @@ export class CalltreeView extends LitElement { `; } + _findEvt = ((event: FindEvt) => this._find(event)) as EventListener; + _getAllTypes(data: LogLine[]): string[] { const flatten = (line: LogLine): LogLine[] => [line, ...line.children.flatMap(flatten)]; const flattened = data.flatMap(flatten); @@ -227,6 +237,9 @@ export class CalltreeView extends LitElement { } _updateFiltering() { + if (!this.calltreeTable) { + return; + } const filtersToAdd = []; // if debug only we want to show everything and apply the debug only filter. @@ -246,36 +259,42 @@ export class CalltreeView extends LitElement { } } - calltreeTable.blockRedraw(); - calltreeTable.clearFilter(false); + this.calltreeTable.blockRedraw(); + this.calltreeTable.clearFilter(false); filtersToAdd.forEach((filter) => { // @ts-expect-error valid - calltreeTable.addFilter(filter); + this.calltreeTable.addFilter(filter); }); - calltreeTable.restoreRedraw(); + this.calltreeTable.restoreRedraw(); } _expandButtonClick() { - calltreeTable.blockRedraw(); - this._expandCollapseAll(calltreeTable.getRows(), true); - calltreeTable.restoreRedraw(); + if (!this.calltreeTable) { + return; + } + this.calltreeTable.blockRedraw(); + this._expandCollapseAll(this.calltreeTable.getRows(), true); + this.calltreeTable.restoreRedraw(); } _collapseButtonClick() { - calltreeTable.blockRedraw(); - this._expandCollapseAll(calltreeTable.getRows(), false); - calltreeTable.restoreRedraw(); + if (!this.calltreeTable) { + return; + } + this.calltreeTable.blockRedraw(); + this._expandCollapseAll(this.calltreeTable.getRows(), false); + this.calltreeTable.restoreRedraw(); } _appendTableWhenVisible() { const callTreeWrapper = this._callTreeTableWrapper; - rootMethod = this.timelineRoot; - if (callTreeWrapper && rootMethod) { + this.rootMethod = this.timelineRoot; + if (callTreeWrapper && this.rootMethod) { const analysisObserver = new IntersectionObserver( (entries, observer) => { const visible = entries[0]?.isIntersecting; - if (rootMethod && visible) { - this._renderCallTree(callTreeWrapper, rootMethod); + if (this.rootMethod && visible) { + this._renderCallTree(callTreeWrapper, this.rootMethod); observer.disconnect(); } }, @@ -286,29 +305,26 @@ export class CalltreeView extends LitElement { } async _goToRow(timestamp: number) { - if (!tableContainer || !rootMethod) { + if (!this.tableContainer || !this.rootMethod || !this.calltreeTable) { return; } document.dispatchEvent(new CustomEvent('show-tab', { detail: { tabid: 'tree-tab' } })); - await this._renderCallTree(tableContainer, rootMethod); + await this._renderCallTree(this.tableContainer, this.rootMethod); - const treeRow = this._findByTime(calltreeTable.getRows(), timestamp); + const treeRow = this._findByTime(this.calltreeTable.getRows(), timestamp); //@ts-expect-error This is a custom function added in by RowNavigation custom module - calltreeTable.goToRow(treeRow, { scrollIfVisible: true, focusRow: true }); + this.calltreeTable.goToRow(treeRow, { scrollIfVisible: true, focusRow: true }); } - searchString = ''; - findArgs: { text: string; count: number; options: { matchCase: boolean } } = { - text: '', - count: 0, - options: { matchCase: false }, - }; - - _find = (e: CustomEvent<{ text: string; count: number; options: { matchCase: boolean } }>) => { - const isTableVisible = !!calltreeTable?.element?.clientHeight; + _find( + e: CustomEvent<{ text: string; count: number; options: { matchCase: boolean } }>, + canClearOverride = true, + ) { + const isTableVisible = !!this.calltreeTable?.element?.clientHeight; if (!isTableVisible && !this.totalMatches) { return; } + this.canClearSearchHighlights = canClearOverride; const newFindArgs = JSON.parse(JSON.stringify(e.detail)); const newSearch = @@ -323,7 +339,7 @@ export class CalltreeView extends LitElement { } if (newSearch || clearHighlights) { //@ts-expect-error This is a custom function added in by Find custom module - const result = calltreeTable.find(this.findArgs); + const result = this.calltreeTable.find(this.findArgs); this.totalMatches = result.totalMatches; this.findMap = result.matchIndexes; @@ -346,9 +362,9 @@ export class CalltreeView extends LitElement { if (currentRow) { //@ts-expect-error This is a custom function added in by RowNavigation custom module - calltreeTable.goToRow(currentRow, { scrollIfVisible: false, focusRow: false }); + this.calltreeTable.goToRow(currentRow, { scrollIfVisible: false, focusRow: false }); } - }; + } _highlight(inputString: string, substring: string) { const regex = new RegExp(substring, 'gi'); @@ -445,7 +461,7 @@ export class CalltreeView extends LitElement { let childMatch = false; const children = rowData._children || []; let len = children.length; - while (len-- > 0) { + while (--len >= 0) { const childRow = children[len]; if (childRow) { const match = this._deepFilter(childRow, filterFunction, filterParams); @@ -469,7 +485,7 @@ export class CalltreeView extends LitElement { callTreeTableContainer: HTMLDivElement, rootMethod: ApexLog, ): Promise { - if (calltreeTable) { + if (this.calltreeTable) { // Ensure the table is fully visible before attempting to do things e.g go to rows. // Otherwise there are visible rendering issues. await new Promise((resolve, reject) => { @@ -500,7 +516,7 @@ export class CalltreeView extends LitElement { const namespaceFilterCache = new Map(); let childIndent; - calltreeTable = new Tabulator(callTreeTableContainer, { + this.calltreeTable = new Tabulator(callTreeTableContainer, { data: this._toCallTree(rootMethod.children), layout: 'fitColumns', placeholder: 'No Call Tree Available', @@ -713,7 +729,17 @@ export class CalltreeView extends LitElement { ], }); - calltreeTable.on('dataFiltered', () => { + this.calltreeTable.on('dataFiltering', () => { + // With a datatree the dataFiltering event occurs multi times and we only want to call this once. + // We will reset the flag when the user next searches. + if (this.canClearSearchHighlights) { + this.canClearSearchHighlights = false; + this._resetFindWidget(); + this._clearSearchHighlights(); + } + }); + + this.calltreeTable.on('dataFiltered', () => { totalTimeFilterCache.clear(); selfTimeFilterCache.clear(); namespaceFilterCache.clear(); @@ -722,12 +748,25 @@ export class CalltreeView extends LitElement { this.typeFilterCache.clear(); }); - calltreeTable.on('tableBuilt', () => { + this.calltreeTable.on('tableBuilt', () => { resolve(); }); }); } + private _resetFindWidget() { + document.dispatchEvent(new CustomEvent('lv-find-results', { detail: { totalMatches: 0 } })); + } + + private _clearSearchHighlights() { + this._find( + new CustomEvent('lv-find', { + detail: { text: '', count: 0, options: { matchCase: false } }, + }), + false, + ); + } + private _expandCollapseAll(rows: RowComponent[], expand: boolean = true) { const len = rows.length; for (let i = 0; i < len; i++) { @@ -833,10 +872,6 @@ interface CalltreeRow { } export async function goToRow(timestamp: number) { - if (!tableContainer || !rootMethod) { - return; - } - document.dispatchEvent( new CustomEvent('calltree-go-to-row', { detail: { timestamp: timestamp } }), ); @@ -846,3 +881,5 @@ type VSCodeApexSymbol = { typeName: string; text: string; }; + +type FindEvt = CustomEvent<{ text: string; count: number; options: { matchCase: boolean } }>; diff --git a/log-viewer/modules/components/database-view/DMLView.ts b/log-viewer/modules/components/database-view/DMLView.ts index 0af6c4c8..60817d94 100644 --- a/log-viewer/modules/components/database-view/DMLView.ts +++ b/log-viewer/modules/components/database-view/DMLView.ts @@ -28,16 +28,6 @@ import './DatabaseSection.js'; import databaseViewStyles from './DatabaseView.scss'; provideVSCodeDesignSystem().register(vsCodeCheckbox()); -let dmlTable: Tabulator; -let holder: HTMLElement | null = null; -let table: HTMLElement | null = null; -let findArgs: { text: string; count: number; options: { matchCase: boolean } } = { - text: '', - count: 0, - options: { matchCase: false }, -}; -let findMap: { [key: number]: RowComponent } = {}; -let totalMatches = 0; @customElement('dml-view') export class DMLView extends LitElement { @@ -53,15 +43,22 @@ export class DMLView extends LitElement { @state() dmlLines: DMLBeginLine[] = []; - get _dmlTableWrapper(): HTMLDivElement | null { - return this.renderRoot?.querySelector('#db-dml-table') ?? null; - } + dmlTable: Tabulator | null = null; + holder: HTMLElement | null = null; + table: HTMLElement | null = null; + findArgs: { text: string; count: number; options: { matchCase: boolean } } = { + text: '', + count: 0, + options: { matchCase: false }, + }; + findMap: { [key: number]: RowComponent } = {}; + totalMatches = 0; constructor() { super(); - document.addEventListener('lv-find', this._find as EventListener); - document.addEventListener('lv-find-close', this._find as EventListener); + document.addEventListener('lv-find', this._findEvt); + document.addEventListener('lv-find-close', this._findEvt); } updated(changedProperties: PropertyValues): void { @@ -115,9 +112,15 @@ export class DMLView extends LitElement { `; } + _findEvt = ((event: FindEvt) => this._find(event)) as EventListener; + _dmlGroupBy(event: Event) { const target = event.target as HTMLInputElement; - dmlTable.setGroupBy(target.checked ? 'dml' : ''); + this.dmlTable?.setGroupBy(target.checked ? 'dml' : ''); + } + + get _dmlTableWrapper(): HTMLDivElement | null { + return this.renderRoot?.querySelector('#db-dml-table') ?? null; } _appendTableWhenVisible() { @@ -134,7 +137,7 @@ export class DMLView extends LitElement { Tabulator.registerModule(Object.values(CommonModules)); Tabulator.registerModule([RowKeyboardNavigation, RowNavigation, Find]); - renderDMLTable(dmlTableWrapper, this.dmlLines); + this._renderDMLTable(dmlTableWrapper, this.dmlLines); } }); dbObserver.observe(this); @@ -142,26 +145,26 @@ export class DMLView extends LitElement { } _highlightMatches(highlightIndex: number) { - if (!dmlTable?.element?.clientHeight) { + if (!this.dmlTable?.element?.clientHeight) { return; } - findArgs.count = highlightIndex; - const currentRow = findMap[highlightIndex]; - const rows = [currentRow, findMap[this.oldIndex]]; + this.findArgs.count = highlightIndex; + const currentRow = this.findMap[highlightIndex]; + const rows = [currentRow, this.findMap[this.oldIndex]]; rows.forEach((row) => { row?.reformat(); }); if (currentRow) { //@ts-expect-error This is a custom function added in by RowNavigation custom module - dmlTable.goToRow(currentRow, { scrollIfVisible: false, focusRow: false }); + this.dmlTable.goToRow(currentRow, { scrollIfVisible: false, focusRow: false }); } this.oldIndex = highlightIndex; } - _find = (e: CustomEvent<{ text: string; count: number; options: { matchCase: boolean } }>) => { - const isTableVisible = !!dmlTable?.element?.clientHeight; - if (!isTableVisible && !totalMatches) { + _find(e: CustomEvent<{ text: string; count: number; options: { matchCase: boolean } }>) { + const isTableVisible = !!this.dmlTable?.element?.clientHeight; + if (!isTableVisible && !this.totalMatches) { return; } @@ -171,9 +174,9 @@ export class DMLView extends LitElement { } const newSearch = - newFindArgs.text !== findArgs.text || - newFindArgs.options.matchCase !== findArgs.options?.matchCase; - findArgs = newFindArgs; + newFindArgs.text !== this.findArgs.text || + newFindArgs.options.matchCase !== this.findArgs.options?.matchCase; + this.findArgs = newFindArgs; const clearHighlights = e.type === 'lv-find-close' || (!isTableVisible && newFindArgs.count === 0); @@ -182,9 +185,9 @@ export class DMLView extends LitElement { } if (newSearch || clearHighlights) { //@ts-expect-error This is a custom function added in by Find custom module - const result = dmlTable.find(findArgs); - totalMatches = result.totalMatches; - findMap = result.matchIndexes; + const result = this.dmlTable.find(this.findArgs); + this.totalMatches = result.totalMatches; + this.findMap = result.matchIndexes; if (!clearHighlights) { document.dispatchEvent( @@ -194,240 +197,261 @@ export class DMLView extends LitElement { ); } } - }; -} - -function renderDMLTable(dmlTableContainer: HTMLElement, dmlLines: DMLBeginLine[]) { - const dmlData: DMLRow[] = []; - if (dmlLines) { - for (const dml of dmlLines) { - dmlData.push({ - dml: dml.text, - rowCount: dml.rowCount.self, - timeTaken: dml.duration.total, - timestamp: dml.timestamp, - _children: [{ timestamp: dml.timestamp, isDetail: true }], - }); - } } - const dmlText = sortByFrequency(dmlData || [], 'dml'); - - dmlTable = new Tabulator(dmlTableContainer, { - height: '100%', - clipboard: true, - downloadEncoder: downlodEncoder('dml.csv'), - downloadRowRange: 'all', - downloadConfig: { - columnHeaders: true, - columnGroups: true, - rowGroups: true, - columnCalcs: false, - dataTree: true, - }, - //@ts-expect-error types need update array is valid - keybindings: { copyToClipboard: ['ctrl + 67', 'meta + 67'] }, - clipboardCopyRowRange: 'all', - rowKeyboardNavigation: true, - data: dmlData, //set initial table data - layout: 'fitColumns', - placeholder: 'No DML statements found', - columnCalcs: 'both', - groupClosedShowCalcs: true, - groupStartOpen: false, - groupValues: [dmlText], - groupHeader(value, count, data: DMLRow[], _group) { - const hasDetail = data.some((d) => { - return d.isDetail; - }); - const newCount = hasDetail ? count - 1 : count; - return ` + _renderDMLTable(dmlTableContainer: HTMLElement, dmlLines: DMLBeginLine[]) { + const dmlData: DMLRow[] = []; + if (dmlLines) { + for (const dml of dmlLines) { + dmlData.push({ + dml: dml.text, + rowCount: dml.rowCount.self, + timeTaken: dml.duration.total, + timestamp: dml.timestamp, + _children: [{ timestamp: dml.timestamp, isDetail: true }], + }); + } + } + const dmlText = this.sortByFrequency(dmlData || [], 'dml'); + + this.dmlTable = new Tabulator(dmlTableContainer, { + height: '100%', + clipboard: true, + downloadEncoder: this.downlodEncoder('dml.csv'), + downloadRowRange: 'all', + downloadConfig: { + columnHeaders: true, + columnGroups: true, + rowGroups: true, + columnCalcs: false, + dataTree: true, + }, + //@ts-expect-error types need update array is valid + keybindings: { copyToClipboard: ['ctrl + 67', 'meta + 67'] }, + clipboardCopyRowRange: 'all', + rowKeyboardNavigation: true, + data: dmlData, //set initial table data + layout: 'fitColumns', + placeholder: 'No DML statements found', + columnCalcs: 'both', + groupClosedShowCalcs: true, + groupStartOpen: false, + groupValues: [dmlText], + groupHeader(value, count, data: DMLRow[], _group) { + const hasDetail = data.some((d) => { + return d.isDetail; + }); + + const newCount = hasDetail ? count - 1 : count; + return `
${value}
(${newCount} DML)
`; - }, - - groupToggleElement: 'header', - selectableRowsCheck: function (row: RowComponent) { - return !row.getData().isDetail; - }, - dataTree: true, - dataTreeBranchElement: false, - columnDefaults: { - title: 'default', - resizable: true, - headerSortStartingDir: 'desc', - headerTooltip: true, - headerMenu: csvheaderMenu('dml.csv'), - headerWordWrap: true, - }, - initialSort: [{ column: 'rowCount', dir: 'desc' }], - headerSortElement: function (column, dir) { - switch (dir) { - case 'asc': - return "
"; - break; - case 'desc': - return "
"; - break; - default: - return "
"; - } - }, - columns: [ - { - title: 'DML', - field: 'dml', - sorter: 'string', - bottomCalc: () => { - return 'Total'; - }, - cssClass: 'datagrid-textarea datagrid-code-text', - variableHeight: true, - formatter: (cell, _formatterParams, _onRendered) => { - const data = cell.getData() as DMLRow; - return `"; + break; + case 'desc': + return "
"; + break; + default: + return "
"; + } + }, + columns: [ + { + title: 'DML', + field: 'dml', + sorter: 'string', + bottomCalc: () => { + return 'Total'; + }, + cssClass: 'datagrid-textarea datagrid-code-text', + variableHeight: true, + formatter: (cell, _formatterParams, _onRendered) => { + const data = cell.getData() as DMLRow; + return ``; + }, }, - }, - { - title: 'Row Count', - field: 'rowCount', - sorter: 'number', - width: 90, - bottomCalc: 'sum', - hozAlign: 'right', - headerHozAlign: 'right', - }, - { - title: 'Time Taken (ms)', - field: 'timeTaken', - sorter: 'number', - width: 110, - hozAlign: 'right', - headerHozAlign: 'right', - formatter: Number, - formatterParams: { - thousand: false, - precision: 3, + { + title: 'Row Count', + field: 'rowCount', + sorter: 'number', + width: 90, + bottomCalc: 'sum', + hozAlign: 'right', + headerHozAlign: 'right', }, - accessorDownload: NumberAccessor, - bottomCalcFormatter: Number, - bottomCalc: 'sum', - bottomCalcFormatterParams: { precision: 3 }, + { + title: 'Time Taken (ms)', + field: 'timeTaken', + sorter: 'number', + width: 110, + hozAlign: 'right', + headerHozAlign: 'right', + formatter: Number, + formatterParams: { + thousand: false, + precision: 3, + }, + accessorDownload: NumberAccessor, + bottomCalcFormatter: Number, + bottomCalc: 'sum', + bottomCalcFormatterParams: { precision: 3 }, + }, + ], + rowFormatter: (row) => { + const data = row.getData(); + if (data.isDetail && data.timestamp) { + const detailContainer = this.createDetailPanel(data.timestamp); + row.getElement().replaceChildren(detailContainer); + row.normalizeHeight(); + } + + requestAnimationFrame(() => { + formatter(row, this.findArgs); + }); }, - ], - rowFormatter: function (row) { + }); + + this.dmlTable.on('dataFiltering', () => { + this._resetFindWidget(); + this._clearSearchHighlights(); + }); + + this.dmlTable.on('tableBuilt', () => { + this.dmlTable?.setGroupBy('dml'); + }); + + this.dmlTable.on('groupClick', (e: UIEvent, group: GroupComponent) => { + if (!group.isVisible()) { + this.dmlTable?.blockRedraw(); + this.dmlTable?.getRows().forEach((row) => { + !row.isTreeExpanded() && row.treeExpand(); + }); + this.dmlTable?.restoreRedraw(); + } + }); + + this.dmlTable.on('rowClick', function (e, row) { const data = row.getData(); - if (data.isDetail && data.timestamp) { - const detailContainer = createDetailPanel(data.timestamp); - row.getElement().replaceChildren(detailContainer); - row.normalizeHeight(); + if (!(data.timestamp && data.dml)) { + return; } - requestAnimationFrame(() => { - formatter(row, findArgs); - }); - }, - }); - - dmlTable.on('tableBuilt', () => { - dmlTable.setGroupBy('dml'); - }); - - dmlTable.on('groupClick', (e: UIEvent, group: GroupComponent) => { - if (!group.isVisible()) { - dmlTable.blockRedraw(); - dmlTable.getRows().forEach((row) => { - !row.isTreeExpanded() && row.treeExpand(); - }); - dmlTable.restoreRedraw(); - } - }); + const origRowHeight = row.getElement().offsetHeight; + row.treeToggle(); + row.getCell('dml').getElement().style.height = origRowHeight + 'px'; + }); - dmlTable.on('rowClick', function (e, row) { - const data = row.getData(); - if (!(data.timestamp && data.dml)) { - return; - } + this.dmlTable.on('renderStarted', () => { + const holder = this._getTableHolder(); + holder.style.minHeight = holder.clientHeight + 'px'; + holder.style.overflowAnchor = 'none'; + }); - const origRowHeight = row.getElement().offsetHeight; - row.treeToggle(); - row.getCell('dml').getElement().style.height = origRowHeight + 'px'; - }); - - dmlTable.on('renderStarted', () => { - const holder = _getTableHolder(); - holder.style.minHeight = holder.clientHeight + 'px'; - holder.style.overflowAnchor = 'none'; - }); - - dmlTable.on('renderComplete', () => { - const holder = _getTableHolder(); - const table = _getTable(); - holder.style.minHeight = Math.min(holder.clientHeight, table.clientHeight) + 'px'; - }); -} + this.dmlTable.on('renderComplete', () => { + const holder = this._getTableHolder(); + const table = this._getTable(); + holder.style.minHeight = Math.min(holder.clientHeight, table.clientHeight) + 'px'; + }); + } -function _getTable() { - table ??= dmlTable.element.querySelector('.tabulator-table')! as HTMLElement; - return table; -} + _resetFindWidget() { + document.dispatchEvent( + new CustomEvent('db-find-results', { + detail: { totalMatches: 0, type: 'dml' }, + }), + ); + } -function _getTableHolder() { - holder ??= dmlTable.element.querySelector('.tabulator-tableholder')! as HTMLElement; - return holder; -} + private _clearSearchHighlights() { + this._find( + new CustomEvent('lv-find', { + detail: { text: '', count: 0, options: { matchCase: false } }, + }), + ); + } + + _getTable() { + this.table ??= this.dmlTable?.element.querySelector('.tabulator-table') as HTMLElement; + return this.table; + } -function createDetailPanel(timestamp: number) { - const detailContainer = document.createElement('div'); - detailContainer.className = 'row__details-container'; - render(html``, detailContainer); + _getTableHolder() { + this.holder = this.dmlTable?.element.querySelector('.tabulator-tableholder') as HTMLElement; + return this.holder; + } - return detailContainer; -} + createDetailPanel(timestamp: number) { + const detailContainer = document.createElement('div'); + detailContainer.className = 'row__details-container'; + render(html``, detailContainer); -function sortByFrequency(dataArray: DMLRow[], field: keyof DMLRow) { - const map = new Map(); - dataArray.forEach((row) => { - const val = row[field]; - map.set(val, (map.get(val) || 0) + 1); - }); - const newMap = new Map([...map.entries()].sort((a, b) => b[1] - a[1])); + return detailContainer; + } - return [...newMap.keys()]; -} + sortByFrequency(dataArray: DMLRow[], field: keyof DMLRow) { + const map = new Map(); + dataArray.forEach((row) => { + const val = row[field]; + map.set(val, (map.get(val) || 0) + 1); + }); + const newMap = new Map([...map.entries()].sort((a, b) => b[1] - a[1])); -function csvheaderMenu(csvFileName: string) { - return [ - { - label: 'Export to CSV', - action: function (_e: PointerEvent, column: ColumnComponent) { - column.getTable().download('csv', csvFileName, { bom: true, delimiter: ',' }); - }, - }, - ]; -} + return [...newMap.keys()]; + } -function downlodEncoder(defaultFileName: string) { - return function (fileContents: string, mimeType: string) { - const vscode = vscodeMessenger.getVsCodeAPI(); - if (vscode) { - vscodeMessenger.send('saveFile', { - fileContent: fileContents, - options: { - defaultFileName: defaultFileName, + csvheaderMenu(csvFileName: string) { + return [ + { + label: 'Export to CSV', + action: function (_e: PointerEvent, column: ColumnComponent) { + column.getTable().download('csv', csvFileName, { bom: true, delimiter: ',' }); }, - }); - return false; - } + }, + ]; + } - return new Blob([fileContents], { type: mimeType }); - }; + downlodEncoder(defaultFileName: string) { + return function (fileContents: string, mimeType: string) { + const vscode = vscodeMessenger.getVsCodeAPI(); + if (vscode) { + vscodeMessenger.send('saveFile', { + fileContent: fileContents, + options: { + defaultFileName: defaultFileName, + }, + }); + return false; + } + + return new Blob([fileContents], { type: mimeType }); + }; + } } type VSCodeSaveFile = { @@ -445,3 +469,5 @@ interface DMLRow { isDetail?: boolean; _children?: DMLRow[]; } + +type FindEvt = CustomEvent<{ text: string; count: number; options: { matchCase: boolean } }>; diff --git a/log-viewer/modules/components/database-view/SOQLView.ts b/log-viewer/modules/components/database-view/SOQLView.ts index acb3eb06..fb334c9d 100644 --- a/log-viewer/modules/components/database-view/SOQLView.ts +++ b/log-viewer/modules/components/database-view/SOQLView.ts @@ -38,17 +38,6 @@ import databaseViewStyles from './DatabaseView.scss'; provideVSCodeDesignSystem().register(vsCodeDropdown(), vsCodeOption()); -let soqlTable: Tabulator; -let holder: HTMLElement | null = null; -let table: HTMLElement | null = null; -let findArgs: { text: string; count: number; options: { matchCase: boolean } } = { - text: '', - count: 0, - options: { matchCase: false }, -}; -let findMap: { [key: number]: RowComponent } = {}; -let totalMatches = 0; - @customElement('soql-view') export class SOQLView extends LitElement { @property() @@ -63,6 +52,17 @@ export class SOQLView extends LitElement { @state() soqlLines: SOQLExecuteBeginLine[] = []; + soqlTable: Tabulator | null = null; + holder: HTMLElement | null = null; + table: HTMLElement | null = null; + findArgs: { text: string; count: number; options: { matchCase: boolean } } = { + text: '', + count: 0, + options: { matchCase: false }, + }; + findMap: { [key: number]: RowComponent } = {}; + totalMatches = 0; + get _soqlTableWrapper(): HTMLDivElement | null { return this.renderRoot?.querySelector('#db-soql-table') ?? null; } @@ -70,8 +70,8 @@ export class SOQLView extends LitElement { constructor() { super(); - document.addEventListener('lv-find', this._find as EventListener); - document.addEventListener('lv-find-close', this._find as EventListener); + document.addEventListener('lv-find', this._findEvt); + document.addEventListener('lv-find-close', this._findEvt); } updated(changedProperties: PropertyValues): void { @@ -148,15 +148,22 @@ export class SOQLView extends LitElement { `; } + _findEvt = ((event: FindEvt) => this._find(event)) as EventListener; + _soqlGroupBy(event: Event) { const target = event.target as HTMLInputElement; const fieldName = target.value.toLowerCase(); const groupValue = fieldName !== 'none' ? fieldName : ''; + if (!this.soqlTable) { + return; + } - soqlTable.setGroupValues([ - groupValue ? sortByFrequency(soqlTable.getData(), groupValue as keyof GridSOQLData) : [''], + this.soqlTable.setGroupValues([ + groupValue + ? this.sortByFrequency(this.soqlTable.getData(), groupValue as keyof GridSOQLData) + : [''], ]); - soqlTable.setGroupBy(groupValue); + this.soqlTable.setGroupBy(groupValue); } _appendTableWhenVisible() { @@ -171,7 +178,7 @@ export class SOQLView extends LitElement { Tabulator.registerModule(Object.values(CommonModules)); Tabulator.registerModule([RowKeyboardNavigation, RowNavigation, Find]); - renderSOQLTable(soqlTableWrapper, this.soqlLines); + this._renderSOQLTable(soqlTableWrapper, this.soqlLines); } }); dbObserver.observe(this); @@ -179,36 +186,36 @@ export class SOQLView extends LitElement { } _highlightMatches(highlightIndex: number) { - if (!soqlTable?.element?.clientHeight) { + if (!this.soqlTable?.element?.clientHeight) { return; } - findArgs.count = highlightIndex; - const currentRow = findMap[highlightIndex]; - const rows = [currentRow, findMap[this.oldIndex]]; + this.findArgs.count = highlightIndex; + const currentRow = this.findMap[highlightIndex]; + const rows = [currentRow, this.findMap[this.oldIndex]]; rows.forEach((row) => { row?.reformat(); }); if (currentRow) { //@ts-expect-error This is a custom function added in by RowNavigation custom module - soqlTable.goToRow(currentRow, { scrollIfVisible: false, focusRow: false }); + this.soqlTable.goToRow(currentRow, { scrollIfVisible: false, focusRow: false }); } this.oldIndex = highlightIndex; } - _find = (e: CustomEvent<{ text: string; count: number; options: { matchCase: boolean } }>) => { - const isTableVisible = !!soqlTable?.element?.clientHeight; - if (!isTableVisible && !totalMatches) { + _find(e: CustomEvent<{ text: string; count: number; options: { matchCase: boolean } }>) { + const isTableVisible = !!this.soqlTable?.element?.clientHeight; + if (!isTableVisible && !this.totalMatches) { return; } const newFindArgs = JSON.parse(JSON.stringify(e.detail)); const newSearch = - newFindArgs.text !== findArgs.text || - newFindArgs.options.matchCase !== findArgs.options?.matchCase; - findArgs = newFindArgs; + newFindArgs.text !== this.findArgs.text || + newFindArgs.options.matchCase !== this.findArgs.options?.matchCase; + this.findArgs = newFindArgs; const clearHighlights = e.type === 'lv-find-close' || (!isTableVisible && newFindArgs.count === 0); @@ -217,9 +224,9 @@ export class SOQLView extends LitElement { } if (newSearch || clearHighlights) { //@ts-expect-error This is a custom function added in by Find custom module - const result = soqlTable.find(findArgs); - totalMatches = 0; - findMap = result.matchIndexes; + const result = this.soqlTable.find(this.findArgs); + this.totalMatches = 0; + this.findMap = result.matchIndexes; if (!clearHighlights) { document.dispatchEvent( @@ -229,350 +236,368 @@ export class SOQLView extends LitElement { ); } } - }; -} - -function renderSOQLTable(soqlTableContainer: HTMLElement, soqlLines: SOQLExecuteBeginLine[]) { - const timestampToSOQl = new Map(); - - soqlLines?.forEach((line) => { - timestampToSOQl.set(line.timestamp, line); - }); - - const soqlData: GridSOQLData[] = []; - if (soqlLines) { - for (const soql of soqlLines) { - const explainLine = soql.children[0] as SOQLExecuteExplainLine; - soqlData.push({ - isSelective: explainLine?.relativeCost ? explainLine.relativeCost <= 1 : null, - relativeCost: explainLine?.relativeCost, - soql: soql.text, - namespace: soql.namespace, - rowCount: soql.rowCount.self, - timeTaken: soql.duration.total, - aggregations: soql.aggregations, - timestamp: soql.timestamp, - _children: [{ timestamp: soql.timestamp, isDetail: true }], - }); - } } - const soqlText = sortByFrequency(soqlData || [], 'soql'); - - soqlTable = new Tabulator(soqlTableContainer, { - height: '100%', - rowKeyboardNavigation: true, - data: soqlData, - layout: 'fitColumns', - placeholder: 'No SOQL queries found', - columnCalcs: 'both', - clipboard: true, - downloadEncoder: downlodEncoder('soql.csv'), - downloadRowRange: 'all', - downloadConfig: { - columnHeaders: true, - columnGroups: true, - rowGroups: true, - columnCalcs: false, - dataTree: true, - }, - //@ts-expect-error types need update array is valid - keybindings: { copyToClipboard: ['ctrl + 67', 'meta + 67'] }, - clipboardCopyRowRange: 'all', - groupClosedShowCalcs: true, - groupStartOpen: false, - groupValues: [soqlText], - groupHeader(value, count, data: GridSOQLData[], _group) { - const hasDetail = data.some((d) => { - return d.isDetail; - }); + _renderSOQLTable(soqlTableContainer: HTMLElement, soqlLines: SOQLExecuteBeginLine[]) { + const timestampToSOQl = new Map(); + + soqlLines?.forEach((line) => { + timestampToSOQl.set(line.timestamp, line); + }); - const newCount = hasDetail ? count - 1 : count; - return ` + const soqlData: GridSOQLData[] = []; + if (soqlLines) { + for (const soql of soqlLines) { + const explainLine = soql.children[0] as SOQLExecuteExplainLine; + soqlData.push({ + isSelective: explainLine?.relativeCost ? explainLine.relativeCost <= 1 : null, + relativeCost: explainLine?.relativeCost, + soql: soql.text, + namespace: soql.namespace, + rowCount: soql.rowCount.self, + timeTaken: soql.duration.total, + aggregations: soql.aggregations, + timestamp: soql.timestamp, + _children: [{ timestamp: soql.timestamp, isDetail: true }], + }); + } + } + + const soqlText = this.sortByFrequency(soqlData || [], 'soql'); + + this.soqlTable = new Tabulator(soqlTableContainer, { + height: '100%', + rowKeyboardNavigation: true, + data: soqlData, + layout: 'fitColumns', + placeholder: 'No SOQL queries found', + columnCalcs: 'both', + clipboard: true, + downloadEncoder: this.downlodEncoder('soql.csv'), + downloadRowRange: 'all', + downloadConfig: { + columnHeaders: true, + columnGroups: true, + rowGroups: true, + columnCalcs: false, + dataTree: true, + }, + //@ts-expect-error types need update array is valid + keybindings: { copyToClipboard: ['ctrl + 67', 'meta + 67'] }, + clipboardCopyRowRange: 'all', + groupClosedShowCalcs: true, + groupStartOpen: false, + groupValues: [soqlText], + groupHeader(value, count, data: GridSOQLData[], _group) { + const hasDetail = data.some((d) => { + return d.isDetail; + }); + + const newCount = hasDetail ? count - 1 : count; + return `
${value}
(${newCount} ${ newCount > 1 ? 'Queries' : 'Query' })
`; - }, - groupToggleElement: 'header', - selectableRowsCheck: function (row: RowComponent) { - return !row.getData().isDetail; - }, - dataTree: true, - dataTreeBranchElement: false, - columnDefaults: { - title: 'default', - resizable: true, - headerSortStartingDir: 'desc', - headerTooltip: true, - headerMenu: csvheaderMenu('soql.csv'), - headerWordWrap: true, - }, - initialSort: [{ column: 'rowCount', dir: 'desc' }], - headerSortElement: function (column, dir) { - switch (dir) { - case 'asc': - return "
"; - break; - case 'desc': - return "
"; - break; - default: - return "
"; - } - }, - columns: [ - { - title: 'SOQL', - field: 'soql', - headerSortStartingDir: 'asc', - sorter: 'string', - tooltip: true, - bottomCalc: () => { - return 'Total'; - }, - cssClass: 'datagrid-textarea datagrid-code-text', - variableHeight: true, - formatter: (cell, _formatterParams, _onRendered) => { - const data = cell.getData() as GridSOQLData; - return `"; + break; + case 'desc': + return "
"; + break; + default: + return "
"; + } + }, + columns: [ + { + title: 'SOQL', + field: 'soql', + headerSortStartingDir: 'asc', + sorter: 'string', + tooltip: true, + bottomCalc: () => { + return 'Total'; + }, + cssClass: 'datagrid-textarea datagrid-code-text', + variableHeight: true, + formatter: (cell, _formatterParams, _onRendered) => { + const data = cell.getData() as GridSOQLData; + return ``; + }, }, - }, - { - title: 'Selective', - field: 'isSelective', - formatter: 'tickCross', - formatterParams: { - allowEmpty: true, - }, - width: 40, - hozAlign: 'center', - vertAlign: 'middle', - sorter: function (a, b, aRow, bRow, _column, dir, _sorterParams) { - // Always Sort null values to the bottom (when we do not have selectivity) - if (a === null) { - return dir === 'asc' ? 1 : -1; - } else if (b === null) { - return dir === 'asc' ? -1 : 1; - } - - const aRowData = aRow.getData(); - const bRowData = bRow.getData(); - - return (aRowData.relativeCost || 0) - (bRowData.relativeCost || 0); + { + title: 'Selective', + field: 'isSelective', + formatter: 'tickCross', + formatterParams: { + allowEmpty: true, + }, + width: 40, + hozAlign: 'center', + vertAlign: 'middle', + sorter: function (a, b, aRow, bRow, _column, dir, _sorterParams) { + // Always Sort null values to the bottom (when we do not have selectivity) + if (a === null) { + return dir === 'asc' ? 1 : -1; + } else if (b === null) { + return dir === 'asc' ? -1 : 1; + } + + const aRowData = aRow.getData(); + const bRowData = bRow.getData(); + + return (aRowData.relativeCost || 0) - (bRowData.relativeCost || 0); + }, + tooltip: function (e, cell, _onRendered) { + const { isSelective, relativeCost } = cell.getData() as GridSOQLData; + let title; + if (isSelective === null) { + title = 'Selectivity could not be determined.'; + } else if (isSelective) { + title = 'Query is selective.'; + } else { + title = 'Query is not selective.'; + } + + if (relativeCost) { + title += `
Relative cost: ${relativeCost}`; + } + return title; + }, + accessorDownload: function ( + _value: unknown, + data: GridSOQLData, + _type: 'data' | 'download' | 'clipboard', + _accessorParams: unknown, + _column?: ColumnComponent, + _row?: RowComponent, + ): number | null | undefined { + return data.relativeCost; + }, + accessorClipboard: function ( + _value: unknown, + data: GridSOQLData, + _type: 'data' | 'download' | 'clipboard', + _accessorParams: unknown, + _column?: ColumnComponent, + _row?: RowComponent, + ): number | null | undefined { + return data.relativeCost; + }, }, - tooltip: function (e, cell, _onRendered) { - const { isSelective, relativeCost } = cell.getData() as GridSOQLData; - let title; - if (isSelective === null) { - title = 'Selectivity could not be determined.'; - } else if (isSelective) { - title = 'Query is selective.'; - } else { - title = 'Query is not selective.'; - } - - if (relativeCost) { - title += `
Relative cost: ${relativeCost}`; - } - return title; + { + title: 'Namespace', + field: 'namespace', + sorter: 'string', + cssClass: 'datagrid-code-text', + width: 120, + headerFilter: 'list', + headerFilterFunc: 'in', + headerFilterParams: { + valuesLookup: 'all', + clearable: true, + multiselect: true, + }, + headerFilterLiveFilter: false, }, - accessorDownload: function ( - _value: unknown, - data: GridSOQLData, - _type: 'data' | 'download' | 'clipboard', - _accessorParams: unknown, - _column?: ColumnComponent, - _row?: RowComponent, - ): number | null | undefined { - return data.relativeCost; + { + title: 'Row Count', + field: 'rowCount', + sorter: 'number', + width: 100, + hozAlign: 'right', + headerHozAlign: 'right', + bottomCalc: 'sum', }, - accessorClipboard: function ( - _value: unknown, - data: GridSOQLData, - _type: 'data' | 'download' | 'clipboard', - _accessorParams: unknown, - _column?: ColumnComponent, - _row?: RowComponent, - ): number | null | undefined { - return data.relativeCost; - }, - }, - { - title: 'Namespace', - field: 'namespace', - sorter: 'string', - cssClass: 'datagrid-code-text', - width: 120, - headerFilter: 'list', - headerFilterFunc: 'in', - headerFilterParams: { - valuesLookup: 'all', - clearable: true, - multiselect: true, + { + title: 'Time Taken (ms)', + field: 'timeTaken', + sorter: 'number', + width: 120, + hozAlign: 'right', + headerHozAlign: 'right', + formatter: Number, + formatterParams: { + thousand: false, + precision: 3, + }, + accessorDownload: NumberAccessor, + bottomCalcFormatter: Number, + bottomCalc: 'sum', + bottomCalcFormatterParams: { precision: 3 }, }, - headerFilterLiveFilter: false, - }, - { - title: 'Row Count', - field: 'rowCount', - sorter: 'number', - width: 100, - hozAlign: 'right', - headerHozAlign: 'right', - bottomCalc: 'sum', - }, - { - title: 'Time Taken (ms)', - field: 'timeTaken', - sorter: 'number', - width: 120, - hozAlign: 'right', - headerHozAlign: 'right', - formatter: Number, - formatterParams: { - thousand: false, - precision: 3, + { + title: 'Aggregations', + field: 'aggregations', + sorter: 'number', + width: 100, + hozAlign: 'right', + headerHozAlign: 'right', + bottomCalc: 'sum', }, - accessorDownload: NumberAccessor, - bottomCalcFormatter: Number, - bottomCalc: 'sum', - bottomCalcFormatterParams: { precision: 3 }, - }, - { - title: 'Aggregations', - field: 'aggregations', - sorter: 'number', - width: 100, - hozAlign: 'right', - headerHozAlign: 'right', - bottomCalc: 'sum', + ], + rowFormatter: (row) => { + const data = row.getData(); + if (data.isDetail && data.timestamp) { + const detailContainer = this.createSOQLDetailPanel(data.timestamp, timestampToSOQl); + + row.getElement().replaceChildren(detailContainer); + row.normalizeHeight(); + } + + requestAnimationFrame(() => { + formatter(row, this.findArgs); + }); }, - ], - rowFormatter: function (row) { - const data = row.getData(); - if (data.isDetail && data.timestamp) { - const detailContainer = createSOQLDetailPanel(data.timestamp, timestampToSOQl); + }); - row.getElement().replaceChildren(detailContainer); - row.normalizeHeight(); + this.soqlTable.on('tableBuilt', () => { + this.soqlTable?.setGroupBy('soql'); + }); + + this.soqlTable.on('groupClick', (e: UIEvent, group: GroupComponent) => { + if (!group.isVisible()) { + this.soqlTable?.blockRedraw(); + this.soqlTable?.getRows().forEach((row) => { + !row.isTreeExpanded() && row.treeExpand(); + }); + this.soqlTable?.restoreRedraw(); } + }); - requestAnimationFrame(() => { - formatter(row, findArgs); - }); - }, - }); - - soqlTable.on('tableBuilt', () => { - soqlTable.setGroupBy('soql'); - }); - - soqlTable.on('groupClick', (e: UIEvent, group: GroupComponent) => { - if (!group.isVisible()) { - soqlTable.blockRedraw(); - soqlTable.getRows().forEach((row) => { - !row.isTreeExpanded() && row.treeExpand(); - }); - soqlTable.restoreRedraw(); - } - }); + this.soqlTable.on('rowClick', function (e, row) { + const data = row.getData(); + if (!(data.timestamp && data.soql)) { + return; + } - soqlTable.on('rowClick', function (e, row) { - const data = row.getData(); - if (!(data.timestamp && data.soql)) { - return; - } + const origRowHeight = row.getElement().offsetHeight; + row.treeToggle(); + row.getCell('soql').getElement().style.height = origRowHeight + 'px'; + }); - const origRowHeight = row.getElement().offsetHeight; - row.treeToggle(); - row.getCell('soql').getElement().style.height = origRowHeight + 'px'; - }); - - soqlTable.on('renderStarted', () => { - const holder = _getTableHolder(); - holder.style.minHeight = holder.clientHeight + 'px'; - holder.style.overflowAnchor = 'none'; - }); - - soqlTable.on('renderComplete', () => { - const holder = _getTableHolder(); - const table = _getTable(); - holder.style.minHeight = Math.min(holder.clientHeight, table.clientHeight) + 'px'; - }); -} + this.soqlTable.on('dataFiltering', () => { + this._resetFindWidget(); + this._clearSearchHighlights(); + }); -function _getTable() { - table ??= soqlTable.element.querySelector('.tabulator-table')! as HTMLElement; - return table; -} + this.soqlTable.on('renderStarted', () => { + const holder = this._getTableHolder(); + holder.style.minHeight = holder.clientHeight + 'px'; + holder.style.overflowAnchor = 'none'; + }); -function _getTableHolder() { - holder ??= soqlTable.element.querySelector('.tabulator-tableholder')! as HTMLElement; - return holder; -} + this.soqlTable.on('renderComplete', () => { + const holder = this._getTableHolder(); + const table = this._getTable(); + holder.style.minHeight = Math.min(holder.clientHeight, table.clientHeight) + 'px'; + }); + } -function createSOQLDetailPanel( - timestamp: number, - timestampToSOQl: Map, -) { - const detailContainer = document.createElement('div'); - detailContainer.className = 'row__details-container'; - - const soqlLine = timestampToSOQl.get(timestamp); - render( - html``, - detailContainer, - ); - - return detailContainer; -} + _resetFindWidget() { + document.dispatchEvent( + new CustomEvent('db-find-results', { + detail: { totalMatches: 0, type: 'soql' }, + }), + ); + } -function sortByFrequency(dataArray: GridSOQLData[], field: keyof GridSOQLData) { - const map = new Map(); - dataArray.forEach((row) => { - const val = row[field]; - map.set(val, (map.get(val) || 0) + 1); - }); - const newMap = new Map([...map.entries()].sort((a, b) => b[1] - a[1])); + _clearSearchHighlights() { + this._find( + new CustomEvent('lv-find', { + detail: { text: '', count: 0, options: { matchCase: false } }, + }), + ); + } - return [...newMap.keys()]; -} + _getTable() { + this.table ??= this.soqlTable?.element.querySelector('.tabulator-table') as HTMLElement; + return this.table; + } -function csvheaderMenu(csvFileName: string) { - return [ - { - label: 'Export to CSV', - action: function (_e: PointerEvent, column: ColumnComponent) { - column.getTable().download('csv', csvFileName, { bom: true, delimiter: ',' }); - }, - }, - ]; -} + _getTableHolder() { + this.holder ??= this.soqlTable?.element.querySelector('.tabulator-tableholder') as HTMLElement; + return this.holder; + } + + createSOQLDetailPanel(timestamp: number, timestampToSOQl: Map) { + const detailContainer = document.createElement('div'); + detailContainer.className = 'row__details-container'; + + const soqlLine = timestampToSOQl.get(timestamp); + render( + html``, + detailContainer, + ); + + return detailContainer; + } + + sortByFrequency(dataArray: GridSOQLData[], field: keyof GridSOQLData) { + const map = new Map(); + dataArray.forEach((row) => { + const val = row[field]; + map.set(val, (map.get(val) || 0) + 1); + }); + const newMap = new Map([...map.entries()].sort((a, b) => b[1] - a[1])); + + return [...newMap.keys()]; + } -function downlodEncoder(defaultFileName: string) { - return function (fileContents: string, mimeType: string) { - const vscode = vscodeMessenger.getVsCodeAPI(); - if (vscode) { - vscodeMessenger.send('saveFile', { - fileContent: fileContents, - options: { - defaultFileName: defaultFileName, + csvheaderMenu(csvFileName: string) { + return [ + { + label: 'Export to CSV', + action: function (_e: PointerEvent, column: ColumnComponent) { + column.getTable().download('csv', csvFileName, { bom: true, delimiter: ',' }); }, - }); - return false; - } + }, + ]; + } - return new Blob([fileContents], { type: mimeType }); - }; + downlodEncoder(defaultFileName: string) { + return function (fileContents: string, mimeType: string) { + const vscode = vscodeMessenger.getVsCodeAPI(); + if (vscode) { + vscodeMessenger.send('saveFile', { + fileContent: fileContents, + options: { + defaultFileName: defaultFileName, + }, + }); + return false; + } + + return new Blob([fileContents], { type: mimeType }); + }; + } } type VSCodeSaveFile = { @@ -594,3 +619,5 @@ interface GridSOQLData { isDetail?: boolean; _children?: GridSOQLData[]; } + +type FindEvt = CustomEvent<{ text: string; count: number; options: { matchCase: boolean } }>; From ad366b7b1531e8f1e7fbfbffb330f86c45ba6a74 Mon Sep 17 00:00:00 2001 From: Luke Cotter <4013877+lukecotter@users.noreply.github.com> Date: Thu, 28 Nov 2024 21:54:49 +0000 Subject: [PATCH 3/4] fix: change map key to string id property was changed to string from number --- .../modules/components/calltree-view/CalltreeView.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/log-viewer/modules/components/calltree-view/CalltreeView.ts b/log-viewer/modules/components/calltree-view/CalltreeView.ts index 3f1f7901..afd0d4bb 100644 --- a/log-viewer/modules/components/calltree-view/CalltreeView.ts +++ b/log-viewer/modules/components/calltree-view/CalltreeView.ts @@ -46,9 +46,9 @@ export class CalltreeView extends LitElement { debugOnly: false, selectedTypes: [], }; - debugOnlyFilterCache = new Map(); - showDetailsFilterCache = new Map(); - typeFilterCache = new Map(); + debugOnlyFilterCache = new Map(); + showDetailsFilterCache = new Map(); + typeFilterCache = new Map(); findMap: { [key: number]: RowComponent } = {}; totalMatches = 0; @@ -431,7 +431,7 @@ export class CalltreeView extends LitElement { selectedNamespaces: string[], namespace: string, data: CalltreeRow, - filterParams: { columnName: string; filterCache: Map }, + filterParams: { columnName: string; filterCache: Map }, ) => { if (selectedNamespaces.length === 0) { return true; @@ -451,7 +451,7 @@ export class CalltreeView extends LitElement { private _deepFilter( rowData: CalltreeRow, filterFunction: (rowData: CalltreeRow) => boolean, - filterParams: { filterCache: Map }, + filterParams: { filterCache: Map }, ): boolean { const cachedMatch = filterParams.filterCache.get(rowData.id); if (cachedMatch !== null && cachedMatch !== undefined) { From 8699660f94718d411fc083506f1de12f81535e42 Mon Sep 17 00:00:00 2001 From: Luke Cotter <4013877+lukecotter@users.noreply.github.com> Date: Fri, 29 Nov 2024 10:59:10 +0000 Subject: [PATCH 4/4] fix: gotorow from timeline not working unless calltree had previously been shown --- log-viewer/modules/components/calltree-view/CalltreeView.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/log-viewer/modules/components/calltree-view/CalltreeView.ts b/log-viewer/modules/components/calltree-view/CalltreeView.ts index afd0d4bb..92fece72 100644 --- a/log-viewer/modules/components/calltree-view/CalltreeView.ts +++ b/log-viewer/modules/components/calltree-view/CalltreeView.ts @@ -305,11 +305,14 @@ export class CalltreeView extends LitElement { } async _goToRow(timestamp: number) { - if (!this.tableContainer || !this.rootMethod || !this.calltreeTable) { + if (!this.tableContainer || !this.rootMethod) { return; } document.dispatchEvent(new CustomEvent('show-tab', { detail: { tabid: 'tree-tab' } })); await this._renderCallTree(this.tableContainer, this.rootMethod); + if (!this.calltreeTable) { + return; + } const treeRow = this._findByTime(this.calltreeTable.getRows(), timestamp); //@ts-expect-error This is a custom function added in by RowNavigation custom module