diff --git a/src/extension/extension.ts b/src/extension/extension.ts index 06a4165..7b41d57 100644 --- a/src/extension/extension.ts +++ b/src/extension/extension.ts @@ -64,6 +64,13 @@ export class EditorProvider implements vscode.CustomTextEditorProvider { } else if (e.type === 'saveRules') { fs.writeFileSync(rulesFile, JSON.stringify(e.rules)); } + else if (e.type === 'exportData') { + const filename = document.fileName.split(".tracy")[0].split("_Tracy_export_")[0] + const _date = new Date().toISOString().slice(0,10).replace(/-/g, ""); + const _time = new Date().toISOString().slice(11,19).replace(/:/g, ""); + const exportFile = `${filename}_Tracy_export_${_date}_${_time}.tracy.json`; + fs.writeFileSync(exportFile, JSON.stringify(e.data)); + } }); } diff --git a/src/viewer/App.tsx b/src/viewer/App.tsx index 0424772..734b22d 100644 --- a/src/viewer/App.tsx +++ b/src/viewer/App.tsx @@ -5,11 +5,12 @@ import MinimapView from "./minimap/MinimapView"; import Tooltip from "@mui/material/Tooltip"; import { LogViewState, StructureMatchId, RowProperty, Segment, LogEntryCharMaps } from "./types"; import { - LOG_HEADER_HEIGHT, + COLUMN_0_HEADER_STYLE, + COLUMN_2_HEADER_STYLE, MINIMAP_COLUMN_WIDTH, - BORDER, SelectedRowType, StructureHeaderColumnType, + defaultAppState, } from "./constants"; import { VSCodeButton, @@ -20,6 +21,7 @@ import { import { useGetCharIndicesForLogEntries, useStructureRegularExpressionSearch, + useStructureRegularExpressionNestedSearch } from "./hooks/useStructureRegularExpressionManager"; import { getRegularExpressionMatches, returnSearchIndices } from "./hooks/useLogSearchManager"; import { constructNewRowProperty, constructNewSegment } from "./hooks/useRowProperty"; @@ -38,24 +40,28 @@ interface State { showStatesDialog: boolean; showStructureDialog: boolean; showFlagsDialog: boolean; - showMinimapHeader: boolean; showSelectDialog: boolean; + showMinimapHeader: boolean; selectedColumns: boolean[]; selectedColumnsMini: boolean[]; coloredTable: boolean; + + // Search related reSearch: boolean; wholeSearch: boolean; caseSearch: boolean; + filterSearch: boolean; + searchMatches: number[]; + currentSearchMatch: number | null; + currentSearchMatchIndex: number | null; // Structure related logFileAsString: string; logEntryCharIndexMaps: LogEntryCharMaps | null; selectedLogRows: string[][]; - // selectedRowsTypes: RowType[]; rowProperties: RowProperty[]; lastSelectedRow: number | undefined; structureMatches: number[][]; - structureMatchesLogRows: number[]; currentStructureMatch: number[]; currentStructureMatchIndex: StructureMatchId; @@ -63,57 +69,29 @@ interface State { collapsibleRows: { [key: number]: Segment }; } -const COLUMN_0_HEADER_STYLE = { - height: LOG_HEADER_HEIGHT, - display: "flex", - justifyContent: "center", - alignItems: "center", - borderLeft: BORDER, - borderBottom: BORDER, -}; - -const COLUMN_2_HEADER_STYLE = { - height: "100%", - display: "flex", - borderLeft: BORDER, -}; - -let searchText = ""; -let searchColumn = "All"; + +let searchTimeoutId; +let searchText: string = ""; +let searchColumn: string = "All"; let logHeaderColumnTypes: StructureHeaderColumnType[] = []; export default class App extends React.Component { // @ts-ignore vscode = acquireVsCodeApi(); + previousSession = this.vscode.getState(); child = React.createRef(); constructor(props: Props) { super(props); - this.state = { - logFile: LogFile.create([], []), - logFileAsString: "", - logViewState: undefined, - coloredTable: false, - showMinimapHeader: true, - rules: [], - showStatesDialog: false, - showFlagsDialog: false, - showSelectDialog: false, - selectedColumns: [], - selectedColumnsMini: [], - reSearch: false, - wholeSearch: false, - caseSearch: false, - selectedLogRows: [], - rowProperties: [], - logEntryCharIndexMaps: null, - showStructureDialog: false, - structureMatches: [], - structureMatchesLogRows: [], - currentStructureMatchIndex: null, - currentStructureMatch: [], - lastSelectedRow: undefined, - collapsibleRows: {}, - }; + this.updateSearchMatches = this.updateSearchMatches.bind(this); + + this.state = defaultAppState; + if (this.previousSession !== undefined) { + searchText = this.previousSession.searchText; + searchColumn = this.previousSession.searchColumn; + ["searchColumn", "searchText"].forEach(e => delete this.previousSession[e]); + const { showFlagsDialog, showStatesDialog, showStructureDialog, ...updatedState } = this.previousSession; + this.state = { ...this.state, ...updatedState } + } this.onMessage = this.onMessage.bind(this); window.addEventListener("message", this.onMessage); @@ -124,6 +102,10 @@ export default class App extends React.Component { } componentDidUpdate(prevProps: Props, prevState: State) { + if (this.state !== prevState) { + const { rules, logFile, logFileAsString, logEntryCharIndexMaps, ...updatedState } = this.state; + this.vscode.setState({ ...updatedState, searchText, searchColumn }) + } if (this.state.logFile !== prevState.logFile || this.state.collapsibleRows !== prevState.collapsibleRows) { this.render(); @@ -138,70 +120,116 @@ export default class App extends React.Component { const logFileText = JSON.stringify(lines, null, 2); const logEntryCharIndexMaps = useGetCharIndicesForLogEntries(logFileText); const logFile = LogFile.create(lines, rules); - const newRowsProps = logFile.rows.map(() => - constructNewRowProperty(true, true, SelectedRowType.None), - ); + logFile.setSelectedColumns(this.state.selectedColumns, this.state.selectedColumnsMini); + this.extractHeaderColumnTypes(logFile, rules); this.setState({ + rules, logFile, logFileAsString: logFileText, logEntryCharIndexMaps: logEntryCharIndexMaps, - rules, - rowProperties: newRowsProps, }); - } - } - filterLog(action: any) { - let newRowsProps; - if (action === "Clear") { - searchText = ""; - newRowsProps = this.state.logFile.rows.map(() => - constructNewRowProperty(true, true, SelectedRowType.None), - ); - this.setState({ rowProperties: newRowsProps }); - } else if (action === "Enter") { - if (searchText === "") { - newRowsProps = this.state.logFile.rows.map(() => - constructNewRowProperty(true, true, SelectedRowType.None), + if (!this.previousSession) { + const newRowsProps = logFile.rows.map(() => + constructNewRowProperty(true, SelectedRowType.None), ); this.setState({ rowProperties: newRowsProps }); - } else { - const rules = this.state.rules; - const logFile = this.state.logFile; - const logFileText = this.state.logFileAsString; - const colIndex = this.state.logFile.headers.findIndex((h) => h.name === searchColumn); - const filteredIndices = returnSearchIndices( - logFile.rows, - colIndex, - searchText, - this.state.reSearch, - this.state.wholeSearch, - this.state.caseSearch, - ); + } + else { + const showFlagsDialog = this.previousSession.showFlagsDialog; + const showStatesDialog = this.previousSession.showStatesDialog; + const showStructureDialog = this.previousSession.showStructureDialog; + this.setState({ showFlagsDialog, showStatesDialog, showStructureDialog }); + } + } + } - newRowsProps = this.state.logFile.rows.map((row, index) => { - if (filteredIndices.includes(index)) - return constructNewRowProperty(true, true, SelectedRowType.None); - else return constructNewRowProperty(false, false, SelectedRowType.None); - }); - const logEntryCharIndexMaps = useGetCharIndicesForLogEntries(logFileText); - this.setState({ - logFile, - logFileAsString: logFileText, - logEntryCharIndexMaps: logEntryCharIndexMaps, - rules, - rowProperties: newRowsProps, - }); + extractHeaderColumnTypes(logFile: LogFile, rules: Rule[]) { + logHeaderColumnTypes = []; + for (let h = 0; h < logFile.headers.length; h++) { + let headerType = StructureHeaderColumnType.Selected; + + if (logFile.headers[h].name.toLowerCase() === "timestamp") { + headerType = StructureHeaderColumnType.Unselected; + } else if (logFile.headers[h].name === "Line") { + headerType = StructureHeaderColumnType.Custom; } + + rules.forEach((rule) => { + if (rule.column === logFile.headers[h].name) { + headerType = StructureHeaderColumnType.Custom; + } + }); + + logHeaderColumnTypes.push(headerType); } } + updateSearchField() { + clearTimeout(searchTimeoutId); + searchTimeoutId = setTimeout(this.updateSearchMatches, 1000); + } + + clearSearchField() { + searchText = ""; + const newRowsProps = this.clearRowsTypes(); + this.setState({ + filterSearch: false, + rowProperties: newRowsProps, + searchMatches: [], + currentSearchMatch: null, + currentSearchMatchIndex: null + }); + } + + updateSearchMatches() { + if (searchText === "") + this.clearSearchField(); + else { + const colIndex = this.state.logFile.headers.findIndex((h) => h.name === searchColumn); + const searchMatches = returnSearchIndices( + this.state.logFile.rows, + colIndex, + searchText, + this.state.reSearch, + this.state.wholeSearch, + this.state.caseSearch, + ); + + const currentSearchMatch = searchMatches[0]; + const currentSearchMatchIndex = 0; + const [rowProperties, filterSearch] = this.updateVisibleSearchMatches(searchMatches, this.state.filterSearch); + + this.setState({ searchMatches, currentSearchMatch, currentSearchMatchIndex, rowProperties, filterSearch }); + } + } + + updateVisibleSearchMatches(searchMatches: number[], filterSearch: boolean) { + let rowProperties; + if (!filterSearch) { + rowProperties = this.state.logFile.rows.map((row, index) => { + if (searchMatches.includes(index)) + return constructNewRowProperty(true, SelectedRowType.SearchResult); + else return constructNewRowProperty(true, SelectedRowType.None); + }); + } + else { + rowProperties = this.state.logFile.rows.map((row, index) => { + if (searchMatches.includes(index)) + return constructNewRowProperty(true, SelectedRowType.SearchResult); + else return constructNewRowProperty(false, SelectedRowType.None); + }); + } + this.setState({ rowProperties, filterSearch }); + return [rowProperties, filterSearch]; + } + handleAnnotationDialog(newRules: Rule[], isClose: boolean) { this.vscode.postMessage({ type: "saveRules", rules: newRules.map((r) => r.toJSON()) }); if (isClose === true) this.setState({ rules: newRules, - logFile: this.state.logFile.updateRules(newRules), + logFile: this.state.logFile.updateLogFile(newRules, []), showStatesDialog: false, showFlagsDialog: false, }); @@ -221,7 +249,6 @@ export default class App extends React.Component { handleStructureDialog(isClosing: boolean) { if (isClosing === true) { - logHeaderColumnTypes = []; this.handleStructureUpdate(isClosing); } else { const { logFile, rowProperties, rules, showStructureDialog } = this.state; @@ -235,22 +262,7 @@ export default class App extends React.Component { } if (!showStructureDialog) { - for (let h = 0; h < logFile.headers.length; h++) { - let headerType = StructureHeaderColumnType.Selected; - - if (logFile.headers[h].name.toLowerCase() === "timestamp") { - headerType = StructureHeaderColumnType.Unselected; - } - - rules.forEach((rule) => { - if (rule.column === logFile.headers[h].name) { - headerType = StructureHeaderColumnType.Custom; - } - }); - - logHeaderColumnTypes.push(headerType); - } - + this.extractHeaderColumnTypes(logFile, rules); this.setState({ showStructureDialog: true }); } @@ -264,36 +276,38 @@ export default class App extends React.Component { this.setState({ rowProperties: newRowProps }); } + handleRowSelect(rowProperties: RowProperty[], rowIndex: number) { + if (rowProperties[rowIndex].rowType !== SelectedRowType.UserSelect) + return SelectedRowType.UserSelect; + else if (this.state.searchMatches.includes(rowIndex)) + return SelectedRowType.SearchResult; + else + return SelectedRowType.None; + } + handleSelectedLogRow(rowIndex: number, event: React.MouseEvent) { if (event.ctrlKey) { const newRowProps = this.state.rowProperties; - const { structureMatchesLogRows, lastSelectedRow } = this.state; + const { structureMatches, lastSelectedRow } = this.state; + + const structureMatchesLogRows = structureMatches.flat(1); if (!structureMatchesLogRows.includes(rowIndex)) { if (event.shiftKey && rowIndex !== this.state.lastSelectedRow) { // Shift click higher in the event log if (lastSelectedRow !== undefined && lastSelectedRow < rowIndex) { for (let i = lastSelectedRow + 1; i < rowIndex + 1; i++) { - newRowProps[i].rowType = - newRowProps[i].rowType === SelectedRowType.None - ? SelectedRowType.UserSelect - : SelectedRowType.None; + newRowProps[i].rowType = this.handleRowSelect(newRowProps, rowIndex); } } // Shift click lower in the event log else if (lastSelectedRow !== undefined && lastSelectedRow > rowIndex) { for (let i = rowIndex; i < lastSelectedRow + 1; i++) { - newRowProps[i].rowType = - newRowProps[i].rowType === SelectedRowType.None - ? SelectedRowType.UserSelect - : SelectedRowType.None; + newRowProps[i].rowType = this.handleRowSelect(newRowProps, rowIndex); } } } else { - newRowProps[rowIndex].rowType = - newRowProps[rowIndex].rowType === SelectedRowType.None - ? SelectedRowType.UserSelect - : SelectedRowType.None; + newRowProps[rowIndex].rowType = this.handleRowSelect(newRowProps, rowIndex); } this.setState({ rowProperties: newRowProps, lastSelectedRow: rowIndex }); @@ -301,28 +315,28 @@ export default class App extends React.Component { } } - clearSelectedRowsTypes(): RowProperty[] { + clearRowsTypes(): RowProperty[] { const clearedSelectedRows = this.state.rowProperties.map(() => - constructNewRowProperty(true, true, SelectedRowType.None), + constructNewRowProperty(true, SelectedRowType.None), ); return clearedSelectedRows; } handleStructureUpdate(isClosing: boolean) { - const clearedSelectedRows = this.clearSelectedRowsTypes(); + const clearedSelectedRows = this.clearRowsTypes(); this.setState({ showStructureDialog: !isClosing, rowProperties: clearedSelectedRows, structureMatches: [], - structureMatchesLogRows: [], currentStructureMatchIndex: null, currentStructureMatch: [], + logFile: this.state.logFile.updateLogFile(this.state.rules, []) }); } handleStructureMatching(expression: string) { - const rowProperties = this.clearSelectedRowsTypes(); + const rowProperties = this.clearRowsTypes(); const { logFileAsString, logEntryCharIndexMaps } = this.state; let { currentStructureMatch, currentStructureMatchIndex } = this.state; @@ -331,11 +345,6 @@ export default class App extends React.Component { logFileAsString, logEntryCharIndexMaps!, ); - let structureMatchesLogRows: number[] = []; - - structureMatches.forEach((matchArray) => { - structureMatchesLogRows = structureMatchesLogRows.concat(matchArray); - }); if (structureMatches.length >= 1) { currentStructureMatchIndex = 0; @@ -348,54 +357,68 @@ export default class App extends React.Component { this.setState({ rowProperties, structureMatches, - structureMatchesLogRows, currentStructureMatch, currentStructureMatchIndex, + logFile: this.state.logFile.updateLogFile(this.state.rules, structureMatches) }); } - handleNavigateStructureMatches(isGoingForward: boolean) { - const { currentStructureMatch, currentStructureMatchIndex, structureMatches } = this.state; - let newCurrentStructureMatch = [...currentStructureMatch]; - let newCurrentStructureMatchIndex; + handleNavigation(isGoingForward: boolean, isStructureMatching: boolean) { + let matches, currentMatchIndex; + if (!isStructureMatching) { + matches = this.state.searchMatches; + currentMatchIndex = this.state.currentSearchMatchIndex; + } + else { + matches = this.state.structureMatches; + currentMatchIndex = this.state.currentStructureMatchIndex; + } - if (currentStructureMatchIndex !== null) { + if (currentMatchIndex !== null) { if (isGoingForward) { - newCurrentStructureMatchIndex = - currentStructureMatchIndex < structureMatches.length - 1 - ? currentStructureMatchIndex + 1 + currentMatchIndex = + currentMatchIndex < matches.length - 1 + ? currentMatchIndex + 1 : 0; } else { - newCurrentStructureMatchIndex = - currentStructureMatchIndex > 0 - ? currentStructureMatchIndex - 1 - : structureMatches.length - 1; + currentMatchIndex = + currentMatchIndex > 0 + ? currentMatchIndex - 1 + : matches.length - 1; } - newCurrentStructureMatch = structureMatches[newCurrentStructureMatchIndex]; - - this.setState({ - currentStructureMatch: newCurrentStructureMatch, - currentStructureMatchIndex: newCurrentStructureMatchIndex, - }); + if (!isStructureMatching) { + this.setState({ + currentSearchMatchIndex: currentMatchIndex, + currentSearchMatch: matches[currentMatchIndex] + }); + } + else { + this.setState({ + currentStructureMatch: matches[currentMatchIndex], + currentStructureMatchIndex: currentMatchIndex, + }); + } } } - handleSegmentation(entryExpression: string, exitExpression: string) { + handleSegmentation(expression: string) { const { logFileAsString, logEntryCharIndexMaps } = this.state; const { collapsibleRows } = this.state; - const entryMatches = getRegularExpressionMatches( - entryExpression, - logFileAsString, - logEntryCharIndexMaps!, - ); - const exitMatches = getRegularExpressionMatches( - exitExpression, + const segmentMatches = useStructureRegularExpressionNestedSearch( + expression, logFileAsString, logEntryCharIndexMaps!, ); + let entryMatches: number[] = []; + let exitMatches: number[] = []; + segmentMatches.forEach((match) => { + entryMatches.push(match[0]) + exitMatches.push(match[match.length - 1]) + }) + const stack: number[] = []; const maximumLevel = 5; let nextEntry = entryMatches.shift()!; @@ -422,21 +445,47 @@ export default class App extends React.Component { } clearSegmentation() { - this.setState({ collapsibleRows : {} }); + this.setState({ collapsibleRows: {} }); } switchBooleanState(name: string) { - if (name === "coloredTable") - this.setState(({ coloredTable }) => ({ coloredTable: !coloredTable })); - else if (name === "reSearch") this.setState(({ reSearch }) => ({ reSearch: !reSearch })); - else if (name === "wholeSearch") - this.setState(({ wholeSearch }) => ({ wholeSearch: !wholeSearch })); - else if (name === "caseSearch") - this.setState(({ caseSearch }) => ({ caseSearch: !caseSearch })); + switch (name) { + case "coloredTable": + this.setState(({ coloredTable }) => ({ coloredTable: !coloredTable })); + break; + case "reSearch": + this.setState(({ reSearch }) => ({ reSearch: !reSearch })); + break; + case "wholeSearch": + this.setState(({ wholeSearch }) => ({ wholeSearch: !wholeSearch })); + break; + case "caseSearch": + this.setState(({ caseSearch }) => ({ caseSearch: !caseSearch })); + break; + case "filterSearch": + this.setState(({ filterSearch }) => ({ filterSearch: !filterSearch })); + break; + } + } + + exportData(exportIndices: number[]) { + var exportObjects: Object[] = [] + const originalColumns = this.state.logFile.headers; + if (exportIndices.length === 0) + exportIndices = Array.from(Array(this.state.logFile.amountOfRows()).keys()) + for (var index of exportIndices) { + var rowObject = {}; + const row = this.state.logFile.rows[index] + for (var columnIndex = 0; columnIndex <= originalColumns.length-1; columnIndex++) + rowObject[originalColumns[columnIndex].name] = row[columnIndex]; + exportObjects.push(rowObject) + } + this.vscode.postMessage({ type: "exportData", data: exportObjects }); } render() { - const minimapWidth = this.state.logFile.amountOfColorColumns() * MINIMAP_COLUMN_WIDTH; + const minimapCounter = this.state.logFile.selectedColumnsMini.filter(Boolean).length; + const minimapWidth = minimapCounter * MINIMAP_COLUMN_WIDTH; const minimapHeight = this.state.showMinimapHeader ? "12%" : "5%"; const allColumns = [ @@ -465,20 +514,24 @@ export default class App extends React.Component { >
this.setState({ showSelectDialog: true })} > Choose Columns - {/* */} + this.exportData(this.state.searchMatches)} + > + Export +
(searchColumn = e.target.value)} + onChange={(e) => { searchColumn = e.target.value; this.updateSearchField(); }} > {allColumns.map((col, col_i) => ( @@ -487,11 +540,12 @@ export default class App extends React.Component { ))} (searchText = e.target.value)} - onKeyUp={(e) => this.filterLog(e.key)} + onInput={(e) => { searchText = e.target.value; this.updateSearchField(); }} + autofocus > Match Case} placement="bottom" arrow> { slot="end" style={{ cursor: "pointer" }} className="codicon codicon-close" - onClick={() => this.filterLog("Clear")} + onClick={() => this.clearSearchField()} > + {" "} + {this.state.searchMatches.length === 0 + ? "No Results" + : `${this.state.currentSearchMatchIndex! + 1} of ${this.state.searchMatches.length}` + } + this.handleNavigation(false, false)} + > + + + this.handleNavigation(true, false)} + > + + + {this.state.filterSearch && ( + { this.updateVisibleSearchMatches(this.state.searchMatches, false); }} + > + + + )} + {!this.state.filterSearch && ( + { this.updateVisibleSearchMatches(this.state.searchMatches, true); }} + > + + + )} {this.state.showMinimapHeader && ( {
this.setState({ logViewState })} forwardRef={this.child} + filterSearch={this.state.filterSearch} coloredTable={this.state.coloredTable} rowProperties={this.state.rowProperties} structureMatches={this.state.structureMatches} - structureMatchesLogRows={this.state.structureMatchesLogRows} currentStructureMatch={this.state.currentStructureMatch} + currentSearchMatch={this.state.currentSearchMatch} onSelectedRowsChanged={(index, e) => this.handleSelectedLogRow(index, e)} onRowPropsChanged={(index, isRendered) => this.handleRowCollapse(index, isRendered)} collapsibleRows={this.state.collapsibleRows} @@ -697,12 +794,11 @@ export default class App extends React.Component { onClose={() => this.handleStructureDialog(true)} onStructureUpdate={() => this.handleStructureUpdate(false)} onMatchStructure={(expression) => this.handleStructureMatching(expression)} - onDefineSegment={(entryExpression, exitExpression) => - this.handleSegmentation(entryExpression, exitExpression) - } + onDefineSegment={(expression) => this.handleSegmentation(expression)} onNavigateStructureMatches={(isGoingForward) => - this.handleNavigateStructureMatches(isGoingForward) + this.handleNavigation(isGoingForward, true) } + onExportStructureMatches={() => this.exportData(this.state.structureMatches.flat(1))} /> )}
diff --git a/src/viewer/LogFile.tsx b/src/viewer/LogFile.tsx index ab9dfde..aa2ef63 100644 --- a/src/viewer/LogFile.tsx +++ b/src/viewer/LogFile.tsx @@ -4,7 +4,6 @@ import { Header } from "./types"; import { scaleSequential } from "d3-scale"; import { interpolateTurbo } from "d3-scale-chromatic"; -// TODO: determine column type automatically, not hardcoded const DEFAULT_HEADER_TYPE = "string"; const HEADER_TYPE_LOOKUP = { threadID: "number", @@ -27,10 +26,16 @@ export default class LogFile { this.rows = rows; this.selectedColumns = new Array(headers.length).fill(true); this.selectedColumnsMini = new Array(headers.length).fill(true); + this.selectedColumnsMini[0] = false; } static create(content: { [s: string]: string }[], rules: Rule[]) { - const contentHeaders = this.getContentHeaders(content); + let contentHeaders = this.getContentHeaders(content); + if (!contentHeaders.includes("Line")) { + contentHeaders = ["Line"].concat(contentHeaders); + for (let i = 0; i < content.length; i++) + content[i]["Line"] = (i+1).toString(); + } const headers = this.getHeaders(contentHeaders, rules); const rows = content.map((l) => headers.map((h) => l[h.name])); const logFile = new LogFile(contentHeaders, headers, rows); @@ -39,11 +44,39 @@ export default class LogFile { return logFile; } - updateRules(rules: Rule[]): LogFile { - // Slower solution + updateLogFile(rules: Rule[], structureMatches: number[][]): LogFile { const [updatedSelected, updatedSelectedMini] = this.updateSelectedColumns(rules) const headers = LogFile.getHeaders(this.contentHeaders, rules); - const logFile = new LogFile(this.contentHeaders, headers, this.rows); + + let rows = this.rows; + if (this.rows[0].length === headers.length + 1) + rows = this.rows.map(r => r.slice(0,-1)) + + if (structureMatches.length > 0) { + updatedSelected.push(false); + updatedSelectedMini.push(true); + const name = "Structure" + const type = DEFAULT_HEADER_TYPE; + headers.push({name, type}); + let currentStructureIndex = 0; + for (let i = 0; i < rows.length; i++) { + rows[i].push(""); + if (currentStructureIndex < structureMatches.length) { + if (i > structureMatches[currentStructureIndex].at(-1)!) { + currentStructureIndex++; + if (currentStructureIndex === structureMatches.length) + break; + } + + if (structureMatches[currentStructureIndex].includes(i)) { + rows[i].pop(); + rows[i].push((currentStructureIndex + 1).toString()); + } + } + } + } + + const logFile = new LogFile(this.contentHeaders, headers, rows); logFile.copyDefaultColumnColors(this.columnsColors); logFile.computeRulesValuesAndColors(rules); return logFile.setSelectedColumns(updatedSelected, updatedSelectedMini); @@ -100,8 +133,9 @@ export default class LogFile { private static getContentHeaders(content: { [s: string]: string }[]) { // Headers are all keys that are present in the first object (row) - const first = content[0] ?? {}; - return Object.keys(first); + const firstRow = content[0] ?? {}; + const contentHeaders = Object.keys(firstRow); + return contentHeaders; } private static getHeaders(contentHeaders: string[], rules: Rule[]) { @@ -135,16 +169,16 @@ export default class LogFile { private computeRulesValuesAndColors(rules: Rule[]) { // Compute rules values - const startIndex = this.headers.length - rules.length; + const firstRuleIndex = this.contentHeaders.length; const rulesValues = rules.map((r) => r.computeValues(this)); for (let row = 0; row < this.rows.length; row++) { for (let column = 0; column < rulesValues.length; column++) { - this.rows[row][column + startIndex] = rulesValues[column][row]; + this.rows[row][column + firstRuleIndex] = rulesValues[column][row]; } } // Compute colors - for (let i = startIndex; i < this.headers.length; i++) { + for (let i = firstRuleIndex; i < this.headers.length; i++) { const values = this.rows.map((r) => r[i]); this.columnsColors[i] = LogFile.computeColors(this.headers[i], values); } @@ -153,12 +187,14 @@ export default class LogFile { private static computeColors(header: Header, values: string[]) { let colorizer: (s: string) => string; - if (header.type === "number") { - colorizer = scaleSequential().domain(extent(values)).interpolator(interpolateTurbo); - } else { + if (header.name === "Line" || header.name === "Structure") { + colorizer = (v) => interpolateTurbo(values.indexOf(v) / values.length); + } else if (header.type === "string") { const uniqueValues = [...new Set(values)].sort(); colorizer = (v) => interpolateTurbo(uniqueValues.indexOf(v) / uniqueValues.length); - } + } else if (header.type === "number") { + colorizer = scaleSequential().domain(extent(values)).interpolator(interpolateTurbo); + } return values.map((l) => colorizer(l)); } diff --git a/src/viewer/constants.ts b/src/viewer/constants.ts index c787f17..d4b0c07 100644 --- a/src/viewer/constants.ts +++ b/src/viewer/constants.ts @@ -1,9 +1,43 @@ +import LogFile from "./LogFile"; + +export const defaultAppState = { + rules: [], + logFile: LogFile.create([], []), + logFileAsString: "", + logViewState: undefined, + coloredTable: false, + showMinimapHeader: true, + showStatesDialog: false, + showFlagsDialog: false, + showSelectDialog: false, + selectedColumns: [], + selectedColumnsMini: [], + reSearch: false, + wholeSearch: false, + caseSearch: false, + filterSearch: false, + searchMatches: [], + currentSearchMatch: null, + currentSearchMatchIndex: null, + selectedLogRows: [], + rowProperties: [], + logEntryCharIndexMaps: null, + showStructureDialog: false, + structureMatches: [], + currentStructureMatchIndex: null, + currentStructureMatch: [], + lastSelectedRow: undefined, + collapsibleRows: {}, +} + const RGB_LIME_GREEN = "0, 208, 0"; const RGB_TURQUOISE = "69, 205, 191"; const RGB_OFFICE_GREEN = "0, 100, 0"; +const RGB_OFF_ORANGE = "244, 157, 71"; + export const MINIMAP_COLUMN_WIDTH = 15; export const LOG_HEADER_HEIGHT = 40; @@ -24,6 +58,8 @@ export const BORDER_SELECTED_ROW = `${SELECTED_ROW_BORDER_SIZE}px solid rgb(${RG export const BACKGROUND_COLOR_SELECTED_ROW = `rgba(${RGB_TURQUOISE}, 0.5)`; +export const BACKGROUND_COLOR_SEARCH_ROW = `rgba(${RGB_OFF_ORANGE}, 0.5)`; + export const BORDER_STRUCTURE_MATCH_CURRENT = `${STRUCUTURE_MATCH_BORDER_SIZE}px solid rgb(${RGB_LIME_GREEN})`; export const BACKGROUND_COLOR_MATCHED_ROW_CURRENT = `rgba(${RGB_LIME_GREEN}, 0.5)`; @@ -44,6 +80,21 @@ export const RGB_ANNOTATION3 = `rgb(255, 165, 0)`; export const RGB_ANNOTATION4 = `rgb(128, 0, 128)`; +export const COLUMN_0_HEADER_STYLE = { + height: LOG_HEADER_HEIGHT, + display: "flex", + justifyContent: "center", + alignItems: "center", + borderLeft: BORDER, + borderBottom: BORDER, +}; + +export const COLUMN_2_HEADER_STYLE = { + height: "100%", + display: "flex", + borderLeft: BORDER, +}; + // TODO: determine column width automatically, not hardcoded export const LOG_COLUMN_WIDTH_LOOKUP = { timestamp: 180, @@ -51,6 +102,7 @@ export const LOG_COLUMN_WIDTH_LOOKUP = { threadID: 80, location: 200, message: 400, + Line: 50 }; export const STRUCTURE_WIDTH = 28; @@ -73,4 +125,5 @@ export enum SelectedRowType { None = "NONE", UserSelect = "SELECTED", QueryResult = "QUERY_RESULT", + SearchResult = "SEARCH_RESULT" } diff --git a/src/viewer/hooks/useLogSearchManager.ts b/src/viewer/hooks/useLogSearchManager.ts index 198634f..bf0b958 100644 --- a/src/viewer/hooks/useLogSearchManager.ts +++ b/src/viewer/hooks/useLogSearchManager.ts @@ -18,17 +18,6 @@ export const escapeSpecialChars = (text: string): string => { return safeText; }; -export const useRegularExpressionSearch = ( - flags: string, - expression: string, - text: string, -): boolean => { - const structureQuery = new RegExp(expression, flags); - const result = structureQuery.exec(escapeSpecialChars(text)); - if (result === null) return false; - else return true; -}; - // Long function to reduce number of checks export const returnSearchIndices = ( rows: string[][], @@ -39,63 +28,68 @@ export const returnSearchIndices = ( caseSearchBool: boolean, ): number[] => { let loglineText: string; - let searchTerms: string[]; const indices: number[] = []; if (!caseSearchBool && !reSearchBool) searchText = searchText.toLowerCase(); - if (searchText.charAt(0) === '"' && searchText.slice(-1) === '"') - searchTerms = [searchText.slice(1, -1)]; - else searchTerms = searchText.split(" "); - if (!reSearchBool && !wholeSearchBool) { - if (columnIndex === -1) { - if (!caseSearchBool) { - for (let i = 0; i < rows.length; i++) { - loglineText = rows[i].join(" ").toLowerCase(); - let found = true; - for (const term of searchTerms) { - if (loglineText.indexOf(term) == -1) { - found = false; - break; - } + if (!reSearchBool) { + if (!wholeSearchBool) { + if (columnIndex === -1) { + if (!caseSearchBool) { + for (let i = 0; i < rows.length; i++) { + loglineText = rows[i].join(" ").toLowerCase(); + if (loglineText.indexOf(searchText) != -1) + indices.push(i); + } + } else { + for (let i = 0; i < rows.length; i++) { + loglineText = rows[i].join(" "); + if (loglineText.indexOf(searchText) != -1) + indices.push(i); } - if (found) indices.push(i); } } else { - for (let i = 0; i < rows.length; i++) { - loglineText = rows[i].join(" "); - let found = true; - for (const term of searchTerms) { - if (loglineText.indexOf(term) == -1) { - found = false; - break; - } + if (!caseSearchBool) { + for (let i = 0; i < rows.length; i++) { + loglineText = rows[i][columnIndex].toLowerCase(); + if (loglineText.indexOf(searchText) != -1) + indices.push(i); + } + } else { + for (let i = 0; i < rows.length; i++) { + loglineText = rows[i][columnIndex]; + if (loglineText.indexOf(searchText) != -1) + indices.push(i); } - if (found) indices.push(i); } } - } else { - if (!caseSearchBool) { - for (let i = 0; i < rows.length; i++) { - loglineText = rows[i][columnIndex].toLowerCase(); - let found = true; - for (const term of searchTerms) { - if (loglineText.indexOf(term) == -1) { - found = false; - break; - } + } + else { + if (columnIndex === -1) { + if (!caseSearchBool) { + for (let i = 0; i < rows.length; i++) { + loglineText = rows[i].join(" ").toLowerCase(); + if (matchWholeString(loglineText, searchText)) + indices.push(i); + } + } else { + for (let i = 0; i < rows.length; i++) { + loglineText = rows[i].join(" "); + if (matchWholeString(loglineText, searchText)) + indices.push(i); } - if (found) indices.push(i); } } else { - for (let i = 0; i < rows.length; i++) { - loglineText = rows[i][columnIndex]; - let found = true; - for (const term of searchTerms) { - if (loglineText.indexOf(term) == -1) { - found = false; - break; - } + if (!caseSearchBool) { + for (let i = 0; i < rows.length; i++) { + loglineText = rows[i][columnIndex].toLowerCase(); + if (matchWholeString(loglineText, searchText)) + indices.push(i); + } + } else { + for (let i = 0; i < rows.length; i++) { + loglineText = rows[i][columnIndex]; + if (matchWholeString(loglineText, searchText)) + indices.push(i); } - if (found) indices.push(i); } } } @@ -104,36 +98,35 @@ export const returnSearchIndices = ( if (!caseSearchBool) flags = "gsi"; else flags = "gs"; if (wholeSearchBool) - for (let i = 0; i < searchTerms.length; i++) searchTerms[i] = "\\b" + searchTerms[i] + "\\b"; + searchText = "\\b" + searchText + "\\b"; if (columnIndex === -1) { for (let i = 0; i < rows.length; i++) { loglineText = rows[i].join(" "); - let found = true; - for (const term of searchTerms) { - if (useRegularExpressionSearch(flags, term, loglineText) === false) { - found = false; - break; - } - } - if (found) indices.push(i); + if (useRegularExpressionSearch(flags, searchText, loglineText) === false) + indices.push(i); } } else { for (let i = 0; i < rows.length; i++) { loglineText = rows[i][columnIndex]; //Lowercase? - let found = true; - for (const term of searchTerms) { - if (useRegularExpressionSearch(flags, term, loglineText) === false) { - found = false; - break; - } - } - if (found) indices.push(i); + if (useRegularExpressionSearch(flags, searchText, loglineText) === false) + indices.push(i); } } } return indices; }; +export const useRegularExpressionSearch = ( + flags: string, + expression: string, + text: string, +): boolean => { + const structureQuery = new RegExp(expression, flags); + const result = structureQuery.exec(escapeSpecialChars(text)); + if (result === null) return false; + else return true; +}; + export const getRegularExpressionMatches = ( expression: string, logFileAsString: string, @@ -161,3 +154,17 @@ export const getRegularExpressionMatches = ( return resultingMatches; }; + + + +function matchWholeString(text: any, searchText: any) { + if (text.indexOf(' ' + searchText + ' ') != -1) + return true + else if (text.indexOf(searchText + ' ') == 0) + return true + else if (text.indexOf(' ' + searchText) == (text.length - (searchText.length + 1))) + return true + else if (text === searchText) + return true + return false +} diff --git a/src/viewer/hooks/useRowProperty.ts b/src/viewer/hooks/useRowProperty.ts index 8a8961e..9b3b9e3 100644 --- a/src/viewer/hooks/useRowProperty.ts +++ b/src/viewer/hooks/useRowProperty.ts @@ -2,11 +2,10 @@ import { RowProperty, Segment } from "../types"; import { SelectedRowType } from "../constants"; export const constructNewRowProperty = ( - isSearchResult: boolean, isRendered: boolean, rowType: SelectedRowType, ): RowProperty => { - const newRowProperty: RowProperty = { isSearchResult, isRendered, rowType }; + const newRowProperty: RowProperty = { isRendered, rowType }; return newRowProperty; }; diff --git a/src/viewer/hooks/useStructureEntryManager.ts b/src/viewer/hooks/useStructureEntryManager.ts index e9fbb5f..1993e7c 100644 --- a/src/viewer/hooks/useStructureEntryManager.ts +++ b/src/viewer/hooks/useStructureEntryManager.ts @@ -192,8 +192,6 @@ export const removeWildcardFromStructureEntry = ( wildcardIndex: number, ): StructureEntry[] => { const modifiedStructureEntries = structureEntries; - // console.log('entryIndex:',structureEntryIndex); - // console.log('cellIndex:',cellIndex); let wildcardsInCell = modifiedStructureEntries[structureEntryIndex].wildcardsIndices[cellIndex]; wildcardsInCell = wildcardsInCell.filter((value) => value !== wildcardIndex); diff --git a/src/viewer/hooks/useStructureRegularExpressionManager.ts b/src/viewer/hooks/useStructureRegularExpressionManager.ts index f6c1049..1864444 100644 --- a/src/viewer/hooks/useStructureRegularExpressionManager.ts +++ b/src/viewer/hooks/useStructureRegularExpressionManager.ts @@ -227,7 +227,6 @@ export const useStructureRegularExpressionSearch = ( console.log("Starting Structure Matching"); const perfStart = performance.now(); const textRanges: number[][] = []; - const resultingMatches: number[][] = []; const structureQuery = new RegExp(expression, flags); let result; @@ -238,28 +237,63 @@ export const useStructureRegularExpressionSearch = ( const perfEnd = performance.now(); console.log(`Execution time (regular expression run): ${perfEnd - perfStart} ms`); - console.log(textRanges); + // console.log(textRanges); const transStart = performance.now(); + const resultingMatches = extractMatches(textRanges, logEntryCharIndexMaps); + + const transEnd = performance.now(); + console.log(`Execution time (translation from char indices to logFile.rows indices): ${transEnd - transStart} ms`); + + return resultingMatches; +}; + +export const useStructureRegularExpressionNestedSearch = ( + expression: string, + logFileAsString: string, + logEntryCharIndexMaps: LogEntryCharMaps, +): number[][] => { + const textRanges: number[][] = []; + const structureQuery = new RegExp(expression, "s"); + + let finished = false; + let previousStartIndex = 0; + let remainingText = logFileAsString; + + while (!finished) { + let match = remainingText.match(structureQuery); + if ((match == undefined) || (match.index == undefined)) { + finished = true; + } + else { + let startIndex = previousStartIndex + match.index; + let lastIndex = startIndex + match[0].length; + textRanges.push([startIndex, lastIndex]); + previousStartIndex = startIndex + 1; + remainingText = remainingText.substring(match.index + 1); + } + } + + const resultingMatches = extractMatches(textRanges, logEntryCharIndexMaps); + + return resultingMatches; +}; + +function extractMatches(textRanges: number[][], logEntryCharIndexMaps: LogEntryCharMaps) { + let resultingMatches: number[][] = []; textRanges.forEach((matchRanges) => { const indexesOfEntriesInMatch: number[] = []; const indexOfFirstObjectInMatch = logEntryCharIndexMaps.firstCharIndexMap.get(matchRanges[0]); const indexOfLastObjectInMatch = logEntryCharIndexMaps.lastCharIndexMap.get(matchRanges[1]); - - if (indexOfFirstObjectInMatch && indexOfLastObjectInMatch) { + if ((indexOfFirstObjectInMatch !== undefined) && (indexOfLastObjectInMatch !== undefined)) { for (let i = indexOfFirstObjectInMatch; i <= indexOfLastObjectInMatch; i++) { indexesOfEntriesInMatch.push(i); } - resultingMatches.push(indexesOfEntriesInMatch); } }); - - const transEnd = performance.now(); - console.log(`Execution time (translation from char indices to logFile.rows indices): ${transEnd - transStart} ms`); - return resultingMatches; }; \ No newline at end of file diff --git a/src/viewer/hooks/useStyleManager.ts b/src/viewer/hooks/useStyleManager.ts index c4275da..bb6a2fb 100644 --- a/src/viewer/hooks/useStyleManager.ts +++ b/src/viewer/hooks/useStyleManager.ts @@ -11,6 +11,7 @@ import { BACKGROUND_COLOR_MATCHED_ROW_CURRENT, BACKGROUND_COLOR_MATCHED_ROW_OTHER, BACKGROUND_COLOR_SELECTED_ROW, + BACKGROUND_COLOR_SEARCH_ROW, } from "../constants"; import { RowProperty, StructureEntry } from "../types"; @@ -236,6 +237,11 @@ export const getLogViewRowSelectionStyle = ( backgroundColor: BACKGROUND_COLOR_SELECTED_ROW, }; break; + case SelectedRowType.SearchResult: + rowSelectionStyle = { + backgroundColor: BACKGROUND_COLOR_SEARCH_ROW, + }; + break; case SelectedRowType.None: rowSelectionStyle = { borderBottom: BORDER, diff --git a/src/viewer/hooks/useWildcardManager.tsx b/src/viewer/hooks/useWildcardManager.tsx index 6464985..3e66f5d 100644 --- a/src/viewer/hooks/useWildcardManager.tsx +++ b/src/viewer/hooks/useWildcardManager.tsx @@ -249,7 +249,7 @@ export const removeWildcardFromCellContent = ( contentsBeforeCurrent[contentsBeforeCurrent.length - 1].wildcardIndex !== null && contentsAfterCurrent[0].wildcardIndex !== null ) { - //console.log("both are wildcards"); + // Both are wildcards contentsToBeRemoved.wildcardIndex = null; newCellContents.push(...contentsBeforeCurrent); @@ -261,7 +261,7 @@ export const removeWildcardFromCellContent = ( contentsBeforeCurrent[contentsBeforeCurrent.length - 1].wildcardIndex === null && contentsAfterCurrent[0].wildcardIndex === null ) { - //console.log("both are text"); + // Both are text const newContentsTextValue = contentsBeforeCurrent[contentsBeforeCurrent.length - 1].textValue + contentsToBeRemoved.textValue + @@ -288,7 +288,7 @@ export const removeWildcardFromCellContent = ( contentsBeforeCurrent[contentsBeforeCurrent.length - 1].wildcardIndex === null && contentsAfterCurrent[0].wildcardIndex !== null ) { - //console.log("before is text, after is wildcard"); + // Before is text, after is wildcard const newContentsTextValue = contentsBeforeCurrent[contentsBeforeCurrent.length - 1] + contentsToBeRemoved.textValue; const newContents: CellContents = { @@ -305,7 +305,7 @@ export const removeWildcardFromCellContent = ( newCellContents.push(newContents); newCellContents = newCellContents.concat(contentsAfterCurrent); } else { - //console.log("before is wildcard, after is text"); + // Before is wildcard, after is text const newContentsTextValue = contentsToBeRemoved.textValue + contentsAfterCurrent[0].textValue; const newContents: CellContents = { diff --git a/src/viewer/log/LogView.tsx b/src/viewer/log/LogView.tsx index 207f41b..af80421 100644 --- a/src/viewer/log/LogView.tsx +++ b/src/viewer/log/LogView.tsx @@ -29,23 +29,26 @@ import Tooltip from "@mui/material/Tooltip"; interface Props { logFile: LogFile; + previousSessionLogView: LogViewState | undefined; onLogViewStateChanged: (value: LogViewState) => void; onSelectedRowsChanged: (index: number, event: React.MouseEvent) => void; onRowPropsChanged: (index: number, isRendered: boolean) => void; forwardRef: React.RefObject; + filterSearch: boolean; coloredTable: boolean; rowProperties: RowProperty[]; + currentSearchMatch: number | null; currentStructureMatch: number[]; structureMatches: number[][]; - structureMatchesLogRows: number[]; collapsibleRows: { [key: number]: Segment }; clearSegmentation: () => void; } interface State { state: LogViewState | undefined; - columnWidth: { [id: string]: number }; logFile: LogFile; + columnWidth: { [id: string]: number }; collapsed: { [key: number]: boolean }; + isLoadingSavedState: boolean; } const HEADER_STYLE: React.CSSProperties = { @@ -70,6 +73,7 @@ export default class LogView extends React.Component { columnWidth: LOG_COLUMN_WIDTH_LOOKUP, logFile: this.props.logFile, collapsed: [], + isLoadingSavedState: false }; } @@ -80,11 +84,25 @@ export default class LogView extends React.Component { componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { if (prevProps.logFile !== this.props.logFile) { - this.updateState(); + if ( this.props.previousSessionLogView === undefined) + this.updateState(); + else + this.loadState(); } if (prevProps.currentStructureMatch[0] !== this.props.currentStructureMatch[0]) { this.updateState(this.props.currentStructureMatch[0]); } + if (prevProps.currentSearchMatch !== this.props.currentSearchMatch) { + this.updateState(this.props.currentSearchMatch); + } + if (this.viewport.current && this.props.previousSessionLogView && this.state.isLoadingSavedState) { + this.viewport.current.scrollTop = this.props.previousSessionLogView.scrollTop; + this.setState({isLoadingSavedState:false}); + } + if (prevProps.filterSearch !== this.props.filterSearch) { + const firstRow = this.state.state?.startFloor; + this.updateState(firstRow); + } } renderColumn( @@ -128,7 +146,6 @@ export default class LogView extends React.Component { rowProperties, structureMatches, currentStructureMatch, - structureMatchesLogRows, collapsibleRows, } = this.props; let firstRender = this.state.state.startFloor; @@ -150,8 +167,10 @@ export default class LogView extends React.Component { let counter = firstRender; const maxLevel = Math.min(4, getSegmentMaxLevel(collapsibleRows)); const segmentWidth: number = (getSegmentMaxLevel(this.props.collapsibleRows) + 1) * 30 + BORDER_SIZE; + const structureMatchesLogRows = structureMatches.flat(1); + for (let r = firstRender; counter <= lastRender; r++) { - if (rowProperties[r].isSearchResult && rowProperties[r].isRendered) { + if (rowProperties[r].isRendered) { let rowStyle; if (structureMatchesLogRows.includes(r)) { @@ -295,16 +314,22 @@ export default class LogView extends React.Component { } } - updateState(currentStructureMatchFirstRow: StructureMatchId = null) { + loadState() { + if (!this.props.previousSessionLogView) return; + this.setState({ state: this.props.previousSessionLogView, isLoadingSavedState: true }); + this.props.onLogViewStateChanged( this.props.previousSessionLogView ); + } + + updateState(currentMatchFirstRow: StructureMatchId = null) { if (!this.viewport.current) return; const height = this.viewport.current.clientHeight; const maxVisibleItems = height / LOG_ROW_HEIGHT; const visibleItems = Math.min(this.props.logFile.amountOfRows(), maxVisibleItems); let scrollTop; - if (currentStructureMatchFirstRow !== null) { - if (currentStructureMatchFirstRow + visibleItems < this.props.logFile.amountOfRows()) { - scrollTop = currentStructureMatchFirstRow * LOG_ROW_HEIGHT; + if (currentMatchFirstRow !== null) { + if (currentMatchFirstRow + visibleItems < this.props.logFile.amountOfRows()) { + scrollTop = currentMatchFirstRow * LOG_ROW_HEIGHT; } else { scrollTop = visibleItems < this.props.logFile.amountOfRows() @@ -317,9 +342,9 @@ export default class LogView extends React.Component { const scrollLeft = this.viewport.current.scrollLeft; const start = - currentStructureMatchFirstRow !== null && - currentStructureMatchFirstRow + maxVisibleItems < this.props.logFile.amountOfRows() - ? currentStructureMatchFirstRow + currentMatchFirstRow !== null && + currentMatchFirstRow + maxVisibleItems < this.props.logFile.amountOfRows() + ? currentMatchFirstRow : scrollTop / LOG_ROW_HEIGHT; const startFloor = Math.floor(start); const endCeil = Math.min( @@ -337,7 +362,7 @@ export default class LogView extends React.Component { rowHeight: LOG_ROW_HEIGHT, }; - if (currentStructureMatchFirstRow !== null) { + if (currentMatchFirstRow !== null) { this.viewport.current.scrollTop = scrollTop; } @@ -385,8 +410,8 @@ export default class LogView extends React.Component { return visibleRows.length; } - deleteSegmentAnnotations(){ - this.setState({collapsed : []}); + deleteSegmentAnnotations() { + this.setState({ collapsed: [] }); this.props.clearSegmentation(); } @@ -412,12 +437,12 @@ export default class LogView extends React.Component { {this.deleteSegmentAnnotations()}} + onClick={() => { this.deleteSegmentAnnotations() }} > - -
} + + } {this.props.logFile .getSelectedHeader() @@ -448,7 +473,7 @@ export default class LogView extends React.Component { render() { const selection = getSelection(); - if(selection !== null){ + if (selection !== null) { // empty unwanted text selection resulting from Shift-click selection.empty(); } @@ -460,7 +485,7 @@ export default class LogView extends React.Component { logFile.headers.reduce( (partialSum: number, h) => partialSum + this.state.columnWidth[h.name], 0, - ) + (getSegmentMaxLevel(this.props.collapsibleRows) + 1) * 30 + BORDER_SIZE : + ) + (getSegmentMaxLevel(this.props.collapsibleRows) + 1) * 30 + BORDER_SIZE : logFile.amountOfColumns() * BORDER_SIZE + logFile.headers.reduce( (partialSum: number, h) => partialSum + this.state.columnWidth[h.name], diff --git a/src/viewer/minimap/MinimapView.tsx b/src/viewer/minimap/MinimapView.tsx index b2b2187..fb28159 100644 --- a/src/viewer/minimap/MinimapView.tsx +++ b/src/viewer/minimap/MinimapView.tsx @@ -100,7 +100,6 @@ export default class MinimapView extends React.Component { let counter = 0; //increase only when row is rendered for (let i = 0; i < colors.length; i++) { if ( - this.props.rowProperties[i].isSearchResult && this.props.rowProperties[i].isRendered ) { ctx.beginPath(); diff --git a/src/viewer/rules/Dialogs/FlagsDialog.tsx b/src/viewer/rules/Dialogs/FlagsDialog.tsx index 8342163..093722b 100644 --- a/src/viewer/rules/Dialogs/FlagsDialog.tsx +++ b/src/viewer/rules/Dialogs/FlagsDialog.tsx @@ -242,6 +242,7 @@ export default class FlagsDialog extends React.Component { textFieldWidth, userColumns, this.props.logFile, + this.state.rules )} ); diff --git a/src/viewer/rules/Dialogs/StatesDialog.tsx b/src/viewer/rules/Dialogs/StatesDialog.tsx index b802a6b..8b485b6 100644 --- a/src/viewer/rules/Dialogs/StatesDialog.tsx +++ b/src/viewer/rules/Dialogs/StatesDialog.tsx @@ -195,6 +195,7 @@ export default class StatesDialog extends React.Component { textFieldWidth, userColumns, this.props.logFile, + this.state.rules )} ); diff --git a/src/viewer/rules/FlagRule.tsx b/src/viewer/rules/FlagRule.tsx index 08581fa..84c8e79 100644 --- a/src/viewer/rules/FlagRule.tsx +++ b/src/viewer/rules/FlagRule.tsx @@ -1,6 +1,7 @@ // When adding new rules, don't forget to update the lookup in Rule.fromJSON import React from "react"; import Rule from "./Rule"; +import StateBasedRule from "./StateBasedRule"; import LogFile from "../LogFile"; import Table from "./Tables/Table"; import FlagTable from "./Tables/FlagTable"; @@ -126,7 +127,7 @@ export default class FlagRule extends Rule { const flagRule = rule as FlagRule; let newFlags = flagRule.flags; for (let i = 0; i < newFlags.length; i++) { - for (let j = 0; j < newFlags[i].conditions.length; j++) + for (let j = 0; j < newFlags[i].conditions.length; j++) newFlags[i].conditions[j] = newFlags[i].conditions[j].filter(subCondition => ((subCondition.Column !== "") && (subCondition.Text !== ""))) newFlags[i].conditions = newFlags[i].conditions.filter(li => li.length !== 0) } @@ -141,6 +142,7 @@ export default class FlagRule extends Rule { textFieldWidth: string, user_columns: string[], logFile: LogFile, + rules: Rule[] ) { const allColumns = ["", ...logFile.contentHeaders, ...user_columns]; @@ -278,7 +280,8 @@ export default class FlagRule extends Rule { const conditionSet = this.flags[this.selectedFlag].conditions[columnIndex]; conditionRows.push( conditionSet.map((sub, s_i) => { - return [ + let setMap: any[] = []; + setMap.push( ))} - , + + ); + setMap.push( equals - + + regex search + + startsWith - + endsWith - - regex search + + less than - , - editSubcondition(columnIndex, s_i, "Text", e.target.value)} - />, - ]; + + more than + + + ); + let dropdownOptions: string[] = []; + if (user_columns.includes(sub.Column)) { + const dropdownRule = rules.filter(r => r.column === sub.Column)[0]; + if (dropdownRule.ruleType === 'Flag rule') { + let dropdownFlagRule = dropdownRule as FlagRule; + dropdownOptions = dropdownFlagRule.flags.map(f => f.name); + } + else if (dropdownRule.ruleType === 'State based rule') { + let dropdownStateRule = dropdownRule as StateBasedRule; + dropdownOptions = dropdownStateRule.ruleStates.map(s => s.name) + } + } + console.log(dropdownOptions) + if (dropdownOptions.length === 0 || dropdownOptions[0] === '') { + setMap.push( + editSubcondition(columnIndex, s_i, "Text", e.target.value)} + />, + ); + } + else { + setMap.push( + editSubcondition(columnIndex, s_i, "Text", e.target.value)} + > + {dropdownOptions.map((option, optionIndex) => ( + + {option} + + ))} + + ); + } + return setMap; }), ); } @@ -397,7 +442,7 @@ export default class FlagRule extends Rule { = parseFloat(condition.Text)) { + allConditionsSatisfied = false; + break; + } + } else if (condition.Operation === "moreThan") { + if (parseFloat(logValue) <= parseFloat(condition.Text)) { + allConditionsSatisfied = false; + break; + } } } if (allConditionsSatisfied === true) { diff --git a/src/viewer/rules/Rule.tsx b/src/viewer/rules/Rule.tsx index 1d12952..1b162f4 100644 --- a/src/viewer/rules/Rule.tsx +++ b/src/viewer/rules/Rule.tsx @@ -20,6 +20,7 @@ export default abstract class Rule { textFieldWidth: string, user_columns: string[], logFile: LogFile, + rules: Rule[] ): JSX.Element; abstract computeValues(logFile: LogFile): string[]; abstract toJSON(): { [s: string]: any }; diff --git a/src/viewer/rules/StateBasedRule.tsx b/src/viewer/rules/StateBasedRule.tsx index c754375..2f1bdbd 100644 --- a/src/viewer/rules/StateBasedRule.tsx +++ b/src/viewer/rules/StateBasedRule.tsx @@ -1,6 +1,7 @@ // When adding new rules, don't forget to update the lookup in Rule.fromJSON import React from "react"; import Rule from "./Rule"; +import FlagRule from "./FlagRule"; import LogFile from "../LogFile"; import Table from "./Tables/Table"; import StateTable from "./Tables/StateTable"; @@ -130,7 +131,7 @@ export default class StateBasedRule extends Rule { let newStates = stateRule.ruleStates; for (let i = 0; i < newStates.length; i++) { for (let j = 0; j < newStates[i].transitions.length; j++) { - for (let k = 0; k < newStates[i].transitions[j].conditions.length; k++) + for (let k = 0; k < newStates[i].transitions[j].conditions.length; k++) newStates[i].transitions[j].conditions[k] = newStates[i].transitions[j].conditions[k].filter(subCondition => ((subCondition.Column !== "") && (subCondition.Text !== ""))) newStates[i].transitions[j].conditions = newStates[i].transitions[j].conditions.filter(li => li.length !== 0) } @@ -144,6 +145,7 @@ export default class StateBasedRule extends Rule { textFieldWidth: string, user_columns: string[], logFile: LogFile, + rules: Rule[] ) { const editStateName = (stateIndex: number, value: string) => { const states = [...this.ruleStates]; @@ -234,11 +236,12 @@ export default class StateBasedRule extends Rule { ) { const conditionSet = this.ruleStates[this.originIndex].transitions[this.destinationIndex].conditions[ - transitionIndex + transitionIndex ]; transitionRows.push( conditionSet.map((r, c_i) => { - return [ + let setMap: any[] = []; + setMap.push( ))} - , + + ); + setMap.push( equals - + + regex search + + startsWith - + endsWith - - regex search + + less than - , - editTransition(transitionIndex, c_i, "Text", e.target.value)} - key="Text" - />, - ]; + + more than + + + ); + let dropdownOptions: string[] = []; + if (user_columns.includes(r.Column)) { + const dropdownRule = rules.filter(rule => rule.column === r.Column)[0]; + if (dropdownRule.ruleType === 'Flag rule') { + let dropdownFlagRule = dropdownRule as FlagRule; + dropdownOptions = dropdownFlagRule.flags.map(f => f.name); + } + else if (dropdownRule.ruleType === 'State based rule') { + let dropdownStateRule = dropdownRule as StateBasedRule; + dropdownOptions = dropdownStateRule.ruleStates.map(s => s.name) + } + } + if (dropdownOptions.length === 0 || dropdownOptions[0] === '') { + setMap.push( + editTransition(transitionIndex, c_i, "Text", e.target.value)} + key="Text" + /> + ); + } + else { + setMap.push( + editTransition(transitionIndex, c_i, "Text", e.target.value)} + > + {dropdownOptions.map((option, optionIndex) => ( + + {option} + + ))} + + ); + } + return setMap; }), ); } @@ -500,6 +544,16 @@ export default class StateBasedRule extends Rule { allConditionsSatisfied = false; break; } + } else if (condition.Operation === "lessThan") { + if (parseFloat(logValue) >= parseFloat(condition.Text)) { + allConditionsSatisfied = false; + break; + } + } else if (condition.Operation === "moreThan") { + if (parseFloat(logValue) <= parseFloat(condition.Text)) { + allConditionsSatisfied = false; + break; + } } } if (allConditionsSatisfied === true) { diff --git a/src/viewer/structures/StructureDialog.tsx b/src/viewer/structures/StructureDialog.tsx index e6b9295..58d3892 100644 --- a/src/viewer/structures/StructureDialog.tsx +++ b/src/viewer/structures/StructureDialog.tsx @@ -41,7 +41,8 @@ interface Props { onStructureUpdate: () => void; onNavigateStructureMatches: (isGoingForward: boolean) => void; onMatchStructure: (expression: string) => void; - onDefineSegment: (entryExpression: string, exitExpression: string) => void; + onExportStructureMatches: () => void; + onDefineSegment: (expression: string) => void; } interface State { @@ -61,7 +62,7 @@ export default class StructureDialog extends React.Component { this.state = { isRemovingStructureEntries: false, - isStructureMatching: false, + isStructureMatching: this.props.numberOfMatches > 0 ? true : false, structureHeaderColumnsTypes: logHeaderColumnsTypes, structureEntries: structureEntries, wildcards: [], @@ -72,7 +73,9 @@ export default class StructureDialog extends React.Component { } componentDidMount(): void { - this.props.onStructureUpdate(); //trigger manually, as update function isn't called for initial render. + // trigger manually, as update function isn't called for initial render. + // removing the trigger to keep persistence + // this.props.onStructureUpdate(); } shouldComponentUpdate( @@ -279,20 +282,14 @@ export default class StructureDialog extends React.Component { } defineSegment() { - // TODO: Add functionality with wildcard - const entryRegExp = useStructureQueryConstructor( + const segmentRegExp = useStructureQueryConstructor( this.props.logHeaderColumns, this.state.structureHeaderColumnsTypes, - this.state.structureEntries.slice(0, 1), - [], - ); - const exitRegExp = useStructureQueryConstructor( - this.props.logHeaderColumns, - this.state.structureHeaderColumnsTypes, - this.state.structureEntries.slice(-1), - [], + this.state.structureEntries, + this.state.wildcards, ); - this.props.onDefineSegment(entryRegExp, exitRegExp); + + this.props.onDefineSegment(segmentRegExp); } createWildcard() { @@ -479,9 +476,9 @@ export default class StructureDialog extends React.Component { }, }); - const selection = getSelection(); + const selection = getSelection(); - if(selection !== null){ + if (selection !== null) { // empty unwanted text selection resulting from Shift-click selection.empty(); } @@ -609,6 +606,15 @@ export default class StructureDialog extends React.Component { > Search for Structure + { + this.props.onExportStructureMatches(); + }} + disabled={this.props.numberOfMatches == 0} + > + Export Structures + {isStructureMatching && ( <>