diff --git a/.github/old_screenshot.png b/.github/old_screenshot.png new file mode 100644 index 0000000..0fa1120 Binary files /dev/null and b/.github/old_screenshot.png differ diff --git a/.github/screenshot.png b/.github/screenshot.png index 0fa1120..9586aa8 100644 Binary files a/.github/screenshot.png and b/.github/screenshot.png differ diff --git a/README.md b/README.md index bc9d7ef..40418c5 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,25 @@ To install Tracy in Visual Studio Code: ## User Guide -### Log format +### Input File Format Guidelines -Tracy assumes that a log is represented in JSON format. The log must be a list of JSON objects, with each object representing an event. Every event is assumed to have the same fields, with the first field being the timestamp of the event. Thus, the log can be viewed as a table where each row is an event and each column an event field, with the first column containing the timestamps. +Tracy is designed to process log files represented in JSON format. The input log file must adhere to specific criteria outlined below to ensure optimal utilization of Tracy's capabilities. -Files with extension `*.tracy.json` will be automatically opened in Tracy. +1. **File Format:** + - The input log file must be formatted in JSON. Specifically, it should be structured as a list of JSON objects, where each object represents a distinct event. + - All events within the log file must possess identical fields, rendering the log as a tabular structure with rows representing events and columns representing event fields. + - It is recommended that the input file contains only UTF-8 encoded characters. Non-UTF characters may lead to unexpected behavior or errors during processing by Tracy. + +2. **Automated File Recognition:** + - Files with the extension `*.tracy.json` will be automatically recognized and opened by Tracy without any additional configuration. + +3. **Transformation for Non-JSON Files:** + - If the input log file is not in JSON format, users are required to transform it before using Tracy. + - To facilitate this transformation, an open source converter has been developed. This converter can be accessed [here](https://github.com/TNO/vscode-tracy-csv-converter), and it streamlines the process of converting non-JSON log files into the required JSON format. + +4. **Column Configuration:** + - If the input log file contains a column that indicates the timestamp of each event, then it should be explicitly named "Timestamp". Furthermore, for ease of use we suggest that such a column corresponds to the first column of each event. + - Users are advised against including two specific column names, namely "Line" and "Structure," as these columns are utilized internally by Tracy. ### The minimap diff --git a/media/style.css b/media/style.css index b6a20df..56c8daf 100644 --- a/media/style.css +++ b/media/style.css @@ -14,6 +14,19 @@ box-shadow: 0 2px 8px var(--vscode-widget-shadow); } +.structure-settings-dropdown { + background-color: var(--vscode-menu-background); +} + +.structure-settings-dropdown-item { + color: var(--vscode-menu-foreground); + background-color: var(--vscode-menu-background); +} + +.structure-dialog-icon { + color: var(--vscode-menu-foreground); +} + ::selection { color: var(--vscode-menu-background); background-color: var(--vscode-menu-foreground); diff --git a/package-lock.json b/package-lock.json index 36cdf4b..6a2aa66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "@mui/icons-material": "^5.15.0", "@mui/material": "^5.14.0", "framer-motion": "^10.15.1", "lodash": "^4.17.21", @@ -109,9 +110,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.15.tgz", - "integrity": "sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.6.tgz", + "integrity": "sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -547,6 +548,31 @@ "url": "https://opencollective.com/mui" } }, + "node_modules/@mui/icons-material": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.0.tgz", + "integrity": "sha512-zHY6fOkaK7VfhWeyxO8MjO3IAjEYpYMXuqUhX7TkUZJ9+TSH/9dn4ClG4K2j6hdgBU5Yrq2Z/89Bo6BHHp7AdQ==", + "dependencies": { + "@babel/runtime": "^7.23.5" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^5.0.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/material": { "version": "5.14.9", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.14.9.tgz", @@ -4207,9 +4233,9 @@ "dev": true }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true, "funding": [ { @@ -4657,9 +4683,9 @@ } }, "node_modules/postcss": { - "version": "8.4.29", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz", - "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==", + "version": "8.4.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", + "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", "dev": true, "funding": [ { @@ -4676,7 +4702,7 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, diff --git a/package.json b/package.json index ff26d39..2d237be 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "@mui/icons-material": "^5.15.0", "@mui/material": "^5.14.0", "framer-motion": "^10.15.1", "lodash": "^4.17.21", diff --git a/src/extension/extension.ts b/src/extension/extension.ts index 7b41d57..7bf3bd9 100644 --- a/src/extension/extension.ts +++ b/src/extension/extension.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode'; import fs from 'fs'; export function activate(context: vscode.ExtensionContext) { - context.subscriptions.push(EditorProvider.register(context)); + context.subscriptions.push(EditorProvider.register(context)); } export class EditorProvider implements vscode.CustomTextEditorProvider { @@ -18,11 +18,12 @@ export class EditorProvider implements vscode.CustomTextEditorProvider { ) { } public async resolveCustomTextEditor( - document: vscode.TextDocument, + document: vscode.TextDocument, webviewPanel: vscode.WebviewPanel, _token: vscode.CancellationToken ): Promise { const rulesFile = `${document.fileName}.rules`; + const structureDefinitionFile = `${document.fileName}.structure`; // Setup initial content for the webview webviewPanel.webview.options = { @@ -34,7 +35,7 @@ export class EditorProvider implements vscode.CustomTextEditorProvider { webviewPanel.webview.postMessage({ type: message_type, text: document.getText(), - rules: fs.existsSync(rulesFile) ? JSON.parse(fs.readFileSync(rulesFile, {encoding: 'utf8'})) : [], + rules: fs.existsSync(rulesFile) ? JSON.parse(fs.readFileSync(rulesFile, { encoding: 'utf8' })) : [], }); } @@ -59,17 +60,74 @@ export class EditorProvider implements vscode.CustomTextEditorProvider { // Receive message from the webview. webviewPanel.webview.onDidReceiveMessage(e => { + if (e.type === 'readFile') { updateWebview('readFile'); } else if (e.type === 'saveRules') { fs.writeFileSync(rulesFile, JSON.stringify(e.rules)); } + else if (e.type === 'saveStructureDefinition') { + + const options: vscode.SaveDialogOptions = { + title: 'Save Structure Definition', + defaultUri: vscode.Uri.joinPath(document.uri, structureDefinitionFile), + filters: { + 'Stucture files': ['structure'] + } + }; + vscode.window.showSaveDialog(options).then(fileUri => { + if (fileUri) { + fs.writeFileSync(fileUri.fsPath, e.structureDefinition); + } + }); + } + else if (e.type === 'loadStructureDefinition') { + const options: vscode.OpenDialogOptions = { + title: 'Load Structure Definition', + canSelectMany: false, + openLabel: 'Load', + filters: { + 'Stucture files': ['structure'] + } + }; + vscode.window.showOpenDialog(options).then(fileUri => { + + if (fileUri && fileUri[0]) { + const filePath = fileUri[0].fsPath; + + webviewPanel.webview.postMessage({ + type: 'loadedStructureDefinition', + structureDefinition: fs.existsSync(filePath) ? JSON.parse(fs.readFileSync(filePath, {encoding: 'utf8'})) : [] + }); + } + + }); + } 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, ""); + let filename = document.fileName; + const splitItems = [".tracy", ".json", ".txt", ".csv", "_Tracy_export_"]; + splitItems.forEach(item => { filename = filename.split(item)[0]; }); + const tzoffset = (new Date()).getTimezoneOffset() * 60000; //offset in milliseconds + const _date = new Date(Date.now() - tzoffset).toISOString().slice(0, 10).replace(/-/g, ""); + const _time = new Date(Date.now() - tzoffset).toISOString().slice(11, 19).replace(/:/g, ""); const exportFile = `${filename}_Tracy_export_${_date}_${_time}.tracy.json`; - fs.writeFileSync(exportFile, JSON.stringify(e.data)); + const options: vscode.SaveDialogOptions = { + title: 'Export Data', + defaultUri: vscode.Uri.joinPath(document.uri, exportFile), + filters: { + 'Tracy files': ['json'] + } + }; + vscode.window.showSaveDialog(options).then(fileUri => { + if (fileUri) { + fs.writeFileSync(fileUri.fsPath, JSON.stringify(e.data)); + } + }); + // fs.writeFileSync(exportFile, JSON.stringify(e.data)); + // webviewPanel.webview.postMessage({ + // type: "readExportPath", + // text: fileUri.fsPath, + // }); } }); } diff --git a/src/viewer/App.tsx b/src/viewer/App.tsx index 734b22d..2ad23ee 100644 --- a/src/viewer/App.tsx +++ b/src/viewer/App.tsx @@ -1,9 +1,10 @@ import React from "react"; +import Rule from "./rules/Rule"; import LogFile from "./LogFile"; import LogView from "./log/LogView"; import MinimapView from "./minimap/MinimapView"; import Tooltip from "@mui/material/Tooltip"; -import { LogViewState, StructureMatchId, RowProperty, Segment, LogEntryCharMaps } from "./types"; +import { LogViewState, StructureMatchId, RowProperty, Segment, LogEntryCharMaps, StructureDefinition } from "./types"; import { COLUMN_0_HEADER_STYLE, COLUMN_2_HEADER_STYLE, @@ -21,14 +22,13 @@ import { import { useGetCharIndicesForLogEntries, useStructureRegularExpressionSearch, - useStructureRegularExpressionNestedSearch } from "./hooks/useStructureRegularExpressionManager"; -import { getRegularExpressionMatches, returnSearchIndices } from "./hooks/useLogSearchManager"; -import { constructNewRowProperty, constructNewSegment } from "./hooks/useRowProperty"; +import { returnSearchIndices } from "./hooks/useLogSearchManager"; +import { constructNewRowProperty } from "./hooks/useRowProperty"; import StructureDialog from "./structures/StructureDialog"; import StatesDialog from "./rules/Dialogs/StatesDialog"; import FlagsDialog from "./rules/Dialogs/FlagsDialog"; -import Rule from "./rules/Rule"; +import ExportDialog from "./rules/Dialogs/ExportDialog"; import MinimapHeader from "./minimap/MinimapHeader"; import SelectColDialog from "./log/SelectColDialog"; @@ -37,6 +37,7 @@ interface State { logFile: LogFile; logViewState: LogViewState | undefined; rules: Rule[]; + showExportDialog: boolean; showStatesDialog: boolean; showStructureDialog: boolean; showFlagsDialog: boolean; @@ -57,6 +58,7 @@ interface State { // Structure related logFileAsString: string; + loadedStructureDefinition: StructureDefinition | null; logEntryCharIndexMaps: LogEntryCharMaps | null; selectedLogRows: string[][]; rowProperties: RowProperty[]; @@ -73,6 +75,7 @@ interface State { let searchTimeoutId; let searchText: string = ""; let searchColumn: string = "All"; +let exportPath: string = ""; let logHeaderColumnTypes: StructureHeaderColumnType[] = []; export default class App extends React.Component { @@ -119,9 +122,10 @@ export default class App extends React.Component { const lines = JSON.parse(message.text); const logFileText = JSON.stringify(lines, null, 2); const logEntryCharIndexMaps = useGetCharIndicesForLogEntries(logFileText); - const logFile = LogFile.create(lines, rules); - logFile.setSelectedColumns(this.state.selectedColumns, this.state.selectedColumnsMini); - this.extractHeaderColumnTypes(logFile, rules); + let logFile = LogFile.create(lines, rules); + // if (this.previousSession) + // logFile.setSelectedColumns(this.previousSession.selectedColumns, this.previousSession.selectedColumnsMini); + this.extractHeaderColumnTypes(logFile); this.setState({ rules, logFile, @@ -136,31 +140,36 @@ export default class App extends React.Component { this.setState({ rowProperties: newRowsProps }); } else { - const showFlagsDialog = this.previousSession.showFlagsDialog; - const showStatesDialog = this.previousSession.showStatesDialog; - const showStructureDialog = this.previousSession.showStructureDialog; - this.setState({ showFlagsDialog, showStatesDialog, showStructureDialog }); + this.setState({ + showFlagsDialog: this.previousSession.showFlagsDialog, + showStatesDialog: this.previousSession.showStatesDialog, + showStructureDialog: this.previousSession.showStructureDialog, + }); } + } else if (message.type === "loadedStructureDefinition") { + const loadedStructureDefinition = message.structureDefinition; + console.log("loadedStructureDefinition - in App", loadedStructureDefinition); + this.setState({loadedStructureDefinition}); + } + else if (message.type === "readExportPath") { + exportPath = message.text; + this.setState({ showExportDialog: true }); } } - extractHeaderColumnTypes(logFile: LogFile, rules: Rule[]) { + extractHeaderColumnTypes(logFile: LogFile) { logHeaderColumnTypes = []; for (let h = 0; h < logFile.headers.length; h++) { let headerType = StructureHeaderColumnType.Selected; - if (logFile.headers[h].name.toLowerCase() === "timestamp") { + const cleanHeader = logFile.headers[h].name.toLowerCase().replace(/[\u200B-\u200D\uFEFF]/g, ''); + if (cleanHeader === "timestamp") { headerType = StructureHeaderColumnType.Unselected; - } else if (logFile.headers[h].name === "Line") { + } + if (!logFile.contentHeaders.includes(logFile.headers[h].name)) { headerType = StructureHeaderColumnType.Custom; } - rules.forEach((rule) => { - if (rule.column === logFile.headers[h].name) { - headerType = StructureHeaderColumnType.Custom; - } - }); - logHeaderColumnTypes.push(headerType); } } @@ -236,12 +245,12 @@ export default class App extends React.Component { else this.setState({ rules: newRules }); } - handleSelectDialog(selectedCols: boolean[], selectedColsMini: boolean[], isClose: boolean) { + handleSelectDialog(selectedColumns: boolean[], selectedColumnsMini: boolean[], isClose: boolean) { if (isClose === true) { this.setState({ - selectedColumns: selectedCols, - selectedColumnsMini: selectedColsMini, - logFile: this.state.logFile.setSelectedColumns(selectedCols, selectedColsMini), + selectedColumns: selectedColumns, + selectedColumnsMini: selectedColumnsMini, + logFile: this.state.logFile.setSelectedColumns(selectedColumns, selectedColumnsMini), showSelectDialog: false, }); } @@ -250,8 +259,9 @@ export default class App extends React.Component { handleStructureDialog(isClosing: boolean) { if (isClosing === true) { this.handleStructureUpdate(isClosing); + } else { - const { logFile, rowProperties, rules, showStructureDialog } = this.state; + const { logFile, rowProperties, showStructureDialog } = this.state; const selectedLogRows = logFile.rows.filter( (v, i) => rowProperties[i].rowType === SelectedRowType.UserSelect, @@ -262,7 +272,7 @@ export default class App extends React.Component { } if (!showStructureDialog) { - this.extractHeaderColumnTypes(logFile, rules); + this.extractHeaderColumnTypes(logFile); this.setState({ showStructureDialog: true }); } @@ -270,6 +280,14 @@ export default class App extends React.Component { } } + handleSavingStuctureDefinition(structureDefinition: string) { + this.vscode.postMessage({ type: "saveStructureDefinition", structureDefinition: structureDefinition}); + } + + handleLoadingStructureDefinition() { + this.vscode.postMessage({ type: "loadStructureDefinition"}); + } + handleRowCollapse(rowIndex: number, isRendered: boolean) { const newRowProps = this.state.rowProperties; newRowProps[rowIndex].isRendered = isRendered; @@ -328,6 +346,7 @@ export default class App extends React.Component { this.setState({ showStructureDialog: !isClosing, rowProperties: clearedSelectedRows, + loadedStructureDefinition: null, structureMatches: [], currentStructureMatchIndex: null, currentStructureMatch: [], @@ -402,52 +421,10 @@ export default class App extends React.Component { } } - handleSegmentation(expression: string) { - const { logFileAsString, logEntryCharIndexMaps } = this.state; - const { collapsibleRows } = this.state; - - 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()!; - let nextExit = exitMatches.shift()!; - - while (nextEntry !== undefined && nextExit !== undefined) { - if (nextEntry < nextExit) { - stack.push(nextEntry); - nextEntry = entryMatches.shift()!; - } else { - const entry = stack.pop()!; - if (stack.length <= maximumLevel - 1) - collapsibleRows[entry] = constructNewSegment(entry, nextExit, stack.length); - else console.log(`Maximum segment level reached: Discarding (${entry}, ${nextExit})`); - nextExit = exitMatches.shift()!; - } - } - if (nextExit !== undefined) { - const entry = stack.pop()!; - collapsibleRows[entry] = constructNewSegment(entry, nextExit, 0); - } - + updateSegmentation(collapsibleRows: { [key: number]: Segment }) { this.setState({ collapsibleRows }); } - clearSegmentation() { - this.setState({ collapsibleRows: {} }); - } - switchBooleanState(name: string) { switch (name) { case "coloredTable": @@ -455,12 +432,15 @@ export default class App extends React.Component { break; case "reSearch": this.setState(({ reSearch }) => ({ reSearch: !reSearch })); + this.updateSearchField(); break; case "wholeSearch": this.setState(({ wholeSearch }) => ({ wholeSearch: !wholeSearch })); + this.updateSearchField(); break; case "caseSearch": this.setState(({ caseSearch }) => ({ caseSearch: !caseSearch })); + this.updateSearchField(); break; case "filterSearch": this.setState(({ filterSearch }) => ({ filterSearch: !filterSearch })); @@ -468,15 +448,16 @@ export default class App extends React.Component { } } - exportData(exportIndices: number[]) { + exportData(exportIndices: number[], structureExport: boolean) { var exportObjects: Object[] = [] const originalColumns = this.state.logFile.headers; - if (exportIndices.length === 0) - exportIndices = Array.from(Array(this.state.logFile.amountOfRows()).keys()) + if (!structureExport) + if (exportIndices.length === 0 || this.state.filterSearch === false) + 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++) + for (var columnIndex = 0; columnIndex <= originalColumns.length - 1; columnIndex++) rowObject[originalColumns[columnIndex].name] = row[columnIndex]; exportObjects.push(rowObject) } @@ -485,7 +466,7 @@ export default class App extends React.Component { render() { const minimapCounter = this.state.logFile.selectedColumnsMini.filter(Boolean).length; - const minimapWidth = minimapCounter * MINIMAP_COLUMN_WIDTH; + const minimapWidth = Math.max(6, minimapCounter) * MINIMAP_COLUMN_WIDTH; const minimapHeight = this.state.showMinimapHeader ? "12%" : "5%"; const allColumns = [ @@ -521,7 +502,7 @@ export default class App extends React.Component { this.exportData(this.state.searchMatches)} + onClick={() => this.exportData(this.state.searchMatches, false)} > Export @@ -690,7 +671,7 @@ export default class App extends React.Component { onSelectedRowsChanged={(index, e) => this.handleSelectedLogRow(index, e)} onRowPropsChanged={(index, isRendered) => this.handleRowCollapse(index, isRendered)} collapsibleRows={this.state.collapsibleRows} - clearSegmentation={() => this.clearSegmentation()} + clearSegmentation={() => this.updateSegmentation({})} />
{ /> )}
+ {this.state.showExportDialog && ( + this.setState({ showExportDialog: false })} + /> + )} {this.state.showStatesDialog && ( { logHeaderColumns={this.state.logFile.headers} logHeaderColumnsTypes={logHeaderColumnTypes} logSelectedRows={this.state.selectedLogRows} + logFileAsString={this.state.logFileAsString} + logEntryCharIndexMaps={this.state.logEntryCharIndexMaps} + collapsibleRows={this.state.collapsibleRows} + loadedStructureDefinition={this.state.loadedStructureDefinition} currentStructureMatchIndex={this.state.currentStructureMatchIndex} numberOfMatches={this.state.structureMatches.length} onClose={() => this.handleStructureDialog(true)} onStructureUpdate={() => this.handleStructureUpdate(false)} onMatchStructure={(expression) => this.handleStructureMatching(expression)} - onDefineSegment={(expression) => this.handleSegmentation(expression)} - onNavigateStructureMatches={(isGoingForward) => - this.handleNavigation(isGoingForward, true) - } - onExportStructureMatches={() => this.exportData(this.state.structureMatches.flat(1))} + onNavigateStructureMatches={(isGoingForward) => this.handleNavigation(isGoingForward, true)} + onStructureDefinitionSave={(structureDefinitionString) => this.handleSavingStuctureDefinition(structureDefinitionString)} + onStructureDefinitionLoad={() => {this.handleLoadingStructureDefinition()}} + onDefineSegment={(collapsibleRows) => this.updateSegmentation(collapsibleRows)} + onExportStructureMatches={() => this.exportData(this.state.structureMatches.flat(1), true)} /> )} diff --git a/src/viewer/LogFile.tsx b/src/viewer/LogFile.tsx index aa2ef63..541912c 100644 --- a/src/viewer/LogFile.tsx +++ b/src/viewer/LogFile.tsx @@ -30,13 +30,11 @@ export default class LogFile { } static create(content: { [s: string]: string }[], rules: Rule[]) { - 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 contentHeaders = this.getContentHeaders(content); const headers = this.getHeaders(contentHeaders, rules); + if (!contentHeaders.includes("Line")) + for (let i = 0; i < content.length; i++) + content[i]["Line"] = (i + 1).toString(); const rows = content.map((l) => headers.map((h) => l[h.name])); const logFile = new LogFile(contentHeaders, headers, rows); logFile.computeDefaultColumnColors(); @@ -49,18 +47,32 @@ export default class LogFile { const headers = LogFile.getHeaders(this.contentHeaders, rules); 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 num = 1; + while (true) { + const name = "Structure" + num + const type = DEFAULT_HEADER_TYPE; + if (!headers.map(h => h.name).includes(name)) { + headers.push({ name, type }); + break; + } + num++; + } + // Previous structure match already exists in logfile + if (headers.length === rows[0].length) { + for (let i = 0; i < rows.length; i++) + rows[i].pop(); + } + else { + updatedSelected.push(true); + updatedSelectedMini.push(true); + } + for (let i = 0; i < rows.length; i++) + rows[i].push("0"); + 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++; @@ -72,7 +84,7 @@ export default class LogFile { rows[i].pop(); rows[i].push((currentStructureIndex + 1).toString()); } - } + } } } @@ -80,12 +92,6 @@ export default class LogFile { logFile.copyDefaultColumnColors(this.columnsColors); logFile.computeRulesValuesAndColors(rules); return logFile.setSelectedColumns(updatedSelected, updatedSelectedMini); - - // Old solution - // this.updateSelectedColumns(rules); - // this.updateHeaders(rules); - // this.computeRulesValuesAndColors(rules); - // return this; } updateSelectedColumns(rules: Rule[]) { @@ -140,36 +146,34 @@ export default class LogFile { private static getHeaders(contentHeaders: string[], rules: Rule[]) { const allHeaders = [...contentHeaders, ...rules.map((r) => r.column)]; - return allHeaders.map((name) => { + let headers = allHeaders.map((name) => { const type = HEADER_TYPE_LOOKUP[name] ?? DEFAULT_HEADER_TYPE; return { name, type }; }); + if (!contentHeaders.includes("Line")) { + const lineHeader = [{ name: "Line", type: DEFAULT_HEADER_TYPE }]; + headers = lineHeader.concat(headers); + } + return headers; } - private updateHeaders(rules: Rule[]) { - const allHeaders = [...this.contentHeaders, ...rules.map((r) => r.column)]; - this.headers = allHeaders.map((name) => { - const type = HEADER_TYPE_LOOKUP[name] ?? DEFAULT_HEADER_TYPE; - return { name, type }; - }); - } private computeDefaultColumnColors() { - for (let i = 0; i < this.contentHeaders.length; i++) { + for (let i = 0; i < this.getStaticHeadersSize(); i++) { const values = this.rows.map((r) => r[i]); this.columnsColors[i] = LogFile.computeColors(this.headers[i], values); } } private copyDefaultColumnColors(colours: string[][]) { - for (let i = 0; i < this.contentHeaders.length; i++) { + for (let i = 0; i < this.getStaticHeadersSize(); i++) { this.columnsColors[i] = colours[i]; } } private computeRulesValuesAndColors(rules: Rule[]) { // Compute rules values - const firstRuleIndex = this.contentHeaders.length; + const firstRuleIndex = this.getStaticHeadersSize(); 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++) { @@ -186,19 +190,33 @@ export default class LogFile { private static computeColors(header: Header, values: string[]) { let colorizer: (s: string) => string; - - if (header.name === "Line" || header.name === "Structure") { - colorizer = (v) => interpolateTurbo(values.indexOf(v) / values.length); + if (this.containsOnlyNumbers(values)) { + let uniqueValues = [...new Set(values)]; + const sortedNumbers = uniqueValues.map(Number).sort(function (a, b) { return a - b; }); + colorizer = scaleSequential().domain(extent(sortedNumbers)).interpolator(interpolateTurbo); } 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)); } + private static containsOnlyNumbers(items: string[]) { + for (const i of items) { + if (!Number(+i) && (+i !== 0)) + return false; + } + return true; + } + + private getStaticHeadersSize() { + let size = this.contentHeaders.length; + if (!this.contentHeaders.includes("Line")) + size++; + return size; + } + amountOfRows = () => this.rows.length; amountOfColumns = () => this.headers.length; amountOfColorColumns = () => this.columnsColors.length; diff --git a/src/viewer/constants.ts b/src/viewer/constants.ts index d4b0c07..7b0bbf7 100644 --- a/src/viewer/constants.ts +++ b/src/viewer/constants.ts @@ -7,6 +7,7 @@ export const defaultAppState = { logViewState: undefined, coloredTable: false, showMinimapHeader: true, + showExportDialog: false, showStatesDialog: false, showFlagsDialog: false, showSelectDialog: false, @@ -23,6 +24,7 @@ export const defaultAppState = { rowProperties: [], logEntryCharIndexMaps: null, showStructureDialog: false, + loadedStructureDefinition: null, structureMatches: [], currentStructureMatchIndex: null, currentStructureMatch: [], diff --git a/src/viewer/hooks/useLogSearchManager.ts b/src/viewer/hooks/useLogSearchManager.ts index bf0b958..d770536 100644 --- a/src/viewer/hooks/useLogSearchManager.ts +++ b/src/viewer/hooks/useLogSearchManager.ts @@ -102,13 +102,13 @@ export const returnSearchIndices = ( if (columnIndex === -1) { for (let i = 0; i < rows.length; i++) { loglineText = rows[i].join(" "); - if (useRegularExpressionSearch(flags, searchText, loglineText) === false) + if (useRegularExpressionSearch(flags, searchText, loglineText)) indices.push(i); } } else { for (let i = 0; i < rows.length; i++) { - loglineText = rows[i][columnIndex]; //Lowercase? - if (useRegularExpressionSearch(flags, searchText, loglineText) === false) + loglineText = rows[i][columnIndex]; + if (useRegularExpressionSearch(flags, searchText, loglineText)) indices.push(i); } } diff --git a/src/viewer/hooks/useRowProperty.ts b/src/viewer/hooks/useRowProperty.ts index 9b3b9e3..f71af9b 100644 --- a/src/viewer/hooks/useRowProperty.ts +++ b/src/viewer/hooks/useRowProperty.ts @@ -1,6 +1,8 @@ import { RowProperty, Segment } from "../types"; import { SelectedRowType } from "../constants"; +export const maximumSegmentation = 4; + export const constructNewRowProperty = ( isRendered: boolean, rowType: SelectedRowType, @@ -17,7 +19,7 @@ export const constructNewSegment = (start: number, end: number, level: number) = export const getSegmentMaxLevel = (segments: { [key: number]: Segment }) => { const levels = Object.values(segments).map((segment) => segment.level); if (levels !== undefined && levels.length > 0) { - return Math.min(4, Math.max(...levels)); + return Math.min(maximumSegmentation, Math.max(...levels)); } else { return -1; } diff --git a/src/viewer/hooks/useStructureEntryManager.ts b/src/viewer/hooks/useStructureEntryManager.ts index 1993e7c..7586b32 100644 --- a/src/viewer/hooks/useStructureEntryManager.ts +++ b/src/viewer/hooks/useStructureEntryManager.ts @@ -62,7 +62,7 @@ export const appendNewStructureEntries = ( }); modifiedStructureEntries.sort((a, b) => - a.row[0][0].textValue.localeCompare(b.row[0][0].textValue), + a.row[0][0].textValue.localeCompare(b.row[0][0].textValue, undefined, {'numeric': true}), ); modifiedStructureEntries = removeLastStructureLink(modifiedStructureEntries); diff --git a/src/viewer/hooks/useStructureRegularExpressionManager.ts b/src/viewer/hooks/useStructureRegularExpressionManager.ts index 1864444..a525e47 100644 --- a/src/viewer/hooks/useStructureRegularExpressionManager.ts +++ b/src/viewer/hooks/useStructureRegularExpressionManager.ts @@ -37,12 +37,8 @@ const getLineEndString = (amountOfWhiteSpace: number): string => { function getHeaderValue(text: string) { let headerValue = text; - const regExpCarriageReturnAtEnd = /\r$/; - - if (regExpCarriageReturnAtEnd.test(headerValue)) - headerValue = headerValue.replace(regExpCarriageReturnAtEnd, "\\\\r"); - headerValue = `"${headerValue}"`; + headerValue = `"${escapeSpecialChars(headerValue)}"`; return headerValue; } @@ -106,7 +102,7 @@ const getRegExpForLogEntry = ( let rowString = ""; let hasProcessedLastUsableColumn = false; - for (let c = row.length - 1; c >= 0; c--) { + for (let c = logHeaders.length - 1; c >= 0; c--) { const headerString = getHeaderValue(logHeaders[c].name); @@ -237,8 +233,6 @@ export const useStructureRegularExpressionSearch = ( const perfEnd = performance.now(); console.log(`Execution time (regular expression run): ${perfEnd - perfStart} ms`); - // console.log(textRanges); - const transStart = performance.now(); const resultingMatches = extractMatches(textRanges, logEntryCharIndexMaps); @@ -250,31 +244,46 @@ export const useStructureRegularExpressionSearch = ( }; export const useStructureRegularExpressionNestedSearch = ( - expression: string, + minExpression: string, + maxExpression: string, logFileAsString: string, logEntryCharIndexMaps: LogEntryCharMaps, ): number[][] => { const textRanges: number[][] = []; - const structureQuery = new RegExp(expression, "s"); + const minQuery = new RegExp(minExpression, "s"); + const maxQuery = new RegExp(maxExpression, "s"); - let finished = false; - let previousStartIndex = 0; + let previousIndex = 0; let remainingText = logFileAsString; - while (!finished) { - let match = remainingText.match(structureQuery); - if ((match == undefined) || (match.index == undefined)) { - finished = true; - } + while (true) { + let match = remainingText.match(minQuery); + if ((match == undefined) || (match.index == undefined)) + break; else { - let startIndex = previousStartIndex + match.index; + let startIndex = previousIndex + match.index; let lastIndex = startIndex + match[0].length; textRanges.push([startIndex, lastIndex]); - previousStartIndex = startIndex + 1; + previousIndex = startIndex + 1; remainingText = remainingText.substring(match.index + 1); } } + previousIndex = logFileAsString.length; + remainingText = logFileAsString; + + while (true) { + let match = remainingText.match(maxQuery); + if ((match == undefined) || (match.index == undefined)) + break; + else { + let startIndex = match.index; + let lastIndex = startIndex + match[0].length; + textRanges.push([startIndex, lastIndex]); + remainingText = remainingText.substring(0, lastIndex - 1); + } + } + const resultingMatches = extractMatches(textRanges, logEntryCharIndexMaps); return resultingMatches; @@ -287,6 +296,7 @@ function extractMatches(textRanges: number[][], logEntryCharIndexMaps: LogEntryC const indexOfFirstObjectInMatch = logEntryCharIndexMaps.firstCharIndexMap.get(matchRanges[0]); const indexOfLastObjectInMatch = logEntryCharIndexMaps.lastCharIndexMap.get(matchRanges[1]); + if ((indexOfFirstObjectInMatch !== undefined) && (indexOfLastObjectInMatch !== undefined)) { for (let i = indexOfFirstObjectInMatch; i <= indexOfLastObjectInMatch; i++) { @@ -294,6 +304,7 @@ function extractMatches(textRanges: number[][], logEntryCharIndexMaps: LogEntryC } resultingMatches.push(indexesOfEntriesInMatch); } + }); return resultingMatches; }; \ No newline at end of file diff --git a/src/viewer/log/LogView.tsx b/src/viewer/log/LogView.tsx index af80421..8de6020 100644 --- a/src/viewer/log/LogView.tsx +++ b/src/viewer/log/LogView.tsx @@ -173,7 +173,7 @@ export default class LogView extends React.Component { if (rowProperties[r].isRendered) { let rowStyle; - if (structureMatchesLogRows.includes(r)) { + if (structureMatchesLogRows.includes(r) && !this.props.filterSearch) { rowStyle = getLogViewStructureMatchStyle( currentStructureMatch, structureMatches, diff --git a/src/viewer/rules/Dialogs/ExportDialog.tsx b/src/viewer/rules/Dialogs/ExportDialog.tsx new file mode 100644 index 0000000..0349695 --- /dev/null +++ b/src/viewer/rules/Dialogs/ExportDialog.tsx @@ -0,0 +1,75 @@ +import React from "react"; +import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; + +interface Props { + filepath: string; + onClose: () => void; +} + +interface State { +} + +const BACKDROP_STYLE: React.CSSProperties = { + height: "100vh", + width: "100vw", + backgroundColor: "#00000030", + position: "absolute", + display: "flex", + justifyContent: "center", + alignItems: "center", +}; + +const DIALOG_STYLE: React.CSSProperties = { + height: "100px", + width: "600px", + padding: "5px", + display: "flex", + overflow: "auto", + zIndex: 100 +}; + +export default class FlagsDialog extends React.Component { + constructor(props: Props) { + super(props); + } + + + render() { + return ( +
+
+
+
+

Data has been successfully exported to filepath:

+

{this.props.filepath}

+
+ + this.props.onClose()} + > + Ok + +
+
+
+ ); + } +} diff --git a/src/viewer/rules/FlagRule.tsx b/src/viewer/rules/FlagRule.tsx index 84c8e79..b18d480 100644 --- a/src/viewer/rules/FlagRule.tsx +++ b/src/viewer/rules/FlagRule.tsx @@ -285,7 +285,7 @@ export default class FlagRule extends Rule { editSubcondition(columnIndex, s_i, "Column", e.target.value)} > {allColumns.map((col, col_i) => ( @@ -299,7 +299,7 @@ export default class FlagRule extends Rule { editSubcondition(columnIndex, s_i, "Operation", e.target.value)} > @@ -325,25 +325,32 @@ export default class FlagRule extends Rule { ); + + let isDropdown = false; let dropdownOptions: string[] = []; if (user_columns.includes(sub.Column)) { const dropdownRule = rules.filter(r => r.column === sub.Column)[0]; - if (dropdownRule.ruleType === 'Flag rule') { + if (dropdownRule.ruleType === "Flag rule") { let dropdownFlagRule = dropdownRule as FlagRule; dropdownOptions = dropdownFlagRule.flags.map(f => f.name); + dropdownOptions.push(dropdownFlagRule.defaultValue); + if (dropdownFlagRule.flagType === "User Defined" && dropdownOptions.length > 0) + isDropdown = true; } - else if (dropdownRule.ruleType === 'State based rule') { + else if (dropdownRule.ruleType === "State based rule") { let dropdownStateRule = dropdownRule as StateBasedRule; - dropdownOptions = dropdownStateRule.ruleStates.map(s => s.name) + dropdownOptions = dropdownStateRule.ruleStates.map(s => s.name); + if (dropdownOptions.length > 0) + isDropdown = true; } } - console.log(dropdownOptions) - if (dropdownOptions.length === 0 || dropdownOptions[0] === '') { + dropdownOptions = dropdownOptions.filter(opt => opt != "") + if (!isDropdown) { setMap.push( editSubcondition(columnIndex, s_i, "Text", e.target.value)} />, ); @@ -353,7 +360,7 @@ export default class FlagRule extends Rule { editSubcondition(columnIndex, s_i, "Text", e.target.value)} > {dropdownOptions.map((option, optionIndex) => ( diff --git a/src/viewer/rules/StateBasedRule.tsx b/src/viewer/rules/StateBasedRule.tsx index 2f1bdbd..0ebca2a 100644 --- a/src/viewer/rules/StateBasedRule.tsx +++ b/src/viewer/rules/StateBasedRule.tsx @@ -245,7 +245,7 @@ export default class StateBasedRule extends Rule { editTransition(transitionIndex, c_i, "Column", e.target.value)} > {allColumns.map((col, col_i) => ( @@ -259,7 +259,7 @@ export default class StateBasedRule extends Rule { editTransition(transitionIndex, c_i, "Operation", e.target.value)} > @@ -285,25 +285,33 @@ export default class StateBasedRule extends Rule { ); + + let isDropdown = false; let dropdownOptions: string[] = []; if (user_columns.includes(r.Column)) { const dropdownRule = rules.filter(rule => rule.column === r.Column)[0]; - if (dropdownRule.ruleType === 'Flag rule') { + if (dropdownRule.ruleType === "Flag rule") { let dropdownFlagRule = dropdownRule as FlagRule; dropdownOptions = dropdownFlagRule.flags.map(f => f.name); + dropdownOptions.push(dropdownFlagRule.defaultValue); + if (dropdownFlagRule.flagType === "User Defined" && dropdownOptions.length > 0) + isDropdown = true; } - else if (dropdownRule.ruleType === 'State based rule') { + else if (dropdownRule.ruleType === "State based rule") { let dropdownStateRule = dropdownRule as StateBasedRule; - dropdownOptions = dropdownStateRule.ruleStates.map(s => s.name) + dropdownOptions = dropdownStateRule.ruleStates.map(s => s.name); + if (dropdownOptions.length > 0) + isDropdown = true; } } - if (dropdownOptions.length === 0 || dropdownOptions[0] === '') { + dropdownOptions = dropdownOptions.filter(opt => opt != "") + if (!isDropdown) { setMap.push( editTransition(transitionIndex, c_i, "Text", e.target.value)} - key="Text" + key="editTransitionText" /> ); } @@ -312,7 +320,7 @@ export default class StateBasedRule extends Rule { editTransition(transitionIndex, c_i, "Text", e.target.value)} > {dropdownOptions.map((option, optionIndex) => ( diff --git a/src/viewer/structures/StructureDialog.tsx b/src/viewer/structures/StructureDialog.tsx index 58d3892..6027715 100644 --- a/src/viewer/structures/StructureDialog.tsx +++ b/src/viewer/structures/StructureDialog.tsx @@ -1,10 +1,12 @@ import React from "react"; import Tooltip, { TooltipProps, tooltipClasses } from "@mui/material/Tooltip"; +import CloseIcon from "@mui/icons-material/Close"; +import IconButton from "@mui/material/IconButton"; import StructureTable from "./StructureTable"; -import { ContextMenuItem, Header, StructureEntry, Wildcard } from "../types"; -import { StructureHeaderColumnType } from "../constants"; +import { ContextMenuItem, Header, LogEntryCharMaps, Segment, StructureDefinition, StructureEntry, Wildcard } from "../types"; +import { StructureHeaderColumnType, StructureLinkDistance } from "../constants"; import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; -import { useStructureQueryConstructor } from "../hooks/useStructureRegularExpressionManager"; +import { useStructureQueryConstructor, useStructureRegularExpressionNestedSearch } from "../hooks/useStructureRegularExpressionManager"; import { constructStructureEntriesArray, appendNewStructureEntries, @@ -30,52 +32,66 @@ import isEqual from "react-fast-compare"; import cloneDeep from "lodash/cloneDeep"; import ContextMenu from "../contextMenu/contextMenu"; import { styled } from "@mui/material/styles"; +import { constructNewSegment, maximumSegmentation } from "../hooks/useRowProperty"; +import { StructureSettingsDropdown } from "./StructureSettingsDropdown"; interface Props { + logFileAsString: string; + logEntryCharIndexMaps: LogEntryCharMaps | null; logHeaderColumns: Header[]; logHeaderColumnsTypes: StructureHeaderColumnType[]; logSelectedRows: string[][]; + loadedStructureDefinition: StructureDefinition | null; currentStructureMatchIndex: number | null; numberOfMatches: number; + collapsibleRows: { [key: number]: Segment }; onClose: () => void; onStructureUpdate: () => void; onNavigateStructureMatches: (isGoingForward: boolean) => void; onMatchStructure: (expression: string) => void; + onStructureDefinitionSave: (structureDefinition: string) => void; + onStructureDefinitionLoad: () => void; onExportStructureMatches: () => void; - onDefineSegment: (expression: string) => void; + onDefineSegment: (expression: { [key: number]: Segment }) => void; } interface State { wildcards: Wildcard[]; structureEntries: StructureEntry[]; isRemovingStructureEntries: boolean; + isLoadingStructureDefintion: boolean; isStructureMatching: boolean; + structureHeaderColumns: Header[]; structureHeaderColumnsTypes: StructureHeaderColumnType[]; } export default class StructureDialog extends React.Component { constructor(props: Props) { super(props); - const { logHeaderColumnsTypes, logSelectedRows } = this.props; + const { logHeaderColumns, logHeaderColumnsTypes, logSelectedRows } = this.props; let structureEntries = constructStructureEntriesArray(logHeaderColumnsTypes, logSelectedRows); structureEntries = removeLastStructureLink(structureEntries); this.state = { isRemovingStructureEntries: false, + isLoadingStructureDefintion: false, isStructureMatching: this.props.numberOfMatches > 0 ? true : false, + structureHeaderColumns: logHeaderColumns, structureHeaderColumnsTypes: logHeaderColumnsTypes, structureEntries: structureEntries, wildcards: [], }; - //bind context for all functions used by the context menu: + //bind context for all functions used by the context and dropdown menus: this.createWildcard = this.createWildcard.bind(this); + this.saveStructureDefinition = this.saveStructureDefinition.bind(this); + this.loadStructureDefinition = this.loadStructureDefinition.bind(this); } componentDidMount(): void { // trigger manually, as update function isn't called for initial render. // removing the trigger to keep persistence - // this.props.onStructureUpdate(); + //this.props.onStructureUpdate(); } shouldComponentUpdate( @@ -83,6 +99,7 @@ export default class StructureDialog extends React.Component { nextState: Readonly, _nextContext: any, ): boolean { + const isLoadingStructureDefinition = nextProps.loadedStructureDefinition !== null; const arelogHeaderColumnsUpdating = !isEqual( this.props.logHeaderColumns, nextProps.logHeaderColumns, @@ -123,6 +140,7 @@ export default class StructureDialog extends React.Component { ); if ( + isLoadingStructureDefinition || arelogHeaderColumnsUpdating || arelogHeaderColumnTypesUpdating || arelogSelectedRowsUpdating || @@ -141,7 +159,20 @@ export default class StructureDialog extends React.Component { } componentDidUpdate(prevProps: Readonly, _prevState: Readonly): void { - if (this.props.logSelectedRows !== prevProps.logSelectedRows) { + const { loadedStructureDefinition, logSelectedRows } = this.props; + + if (this.state.isLoadingStructureDefintion && loadedStructureDefinition !== null) { + this.setState({ + isLoadingStructureDefintion: false, + isStructureMatching: false, + structureHeaderColumns: loadedStructureDefinition.headerColumns, + structureHeaderColumnsTypes: loadedStructureDefinition.headerColumnsTypes, + structureEntries: loadedStructureDefinition.entries, + wildcards: loadedStructureDefinition.wildcards, + }); + + this.props.onStructureUpdate(); + } else if (logSelectedRows !== prevProps.logSelectedRows) { this.updateStructure(); } } @@ -271,7 +302,7 @@ export default class StructureDialog extends React.Component { matchStructure() { // pass list of wildcards and use those in regular expression construction const structureRegExp = useStructureQueryConstructor( - this.props.logHeaderColumns, + this.state.structureHeaderColumns, this.state.structureHeaderColumnsTypes, this.state.structureEntries, this.state.wildcards, @@ -282,14 +313,65 @@ export default class StructureDialog extends React.Component { } defineSegment() { - const segmentRegExp = useStructureQueryConstructor( - this.props.logHeaderColumns, + let collapsibleRows = this.props.collapsibleRows; + + const minSegmentRegExp = useStructureQueryConstructor( + this.state.structureHeaderColumns, this.state.structureHeaderColumnsTypes, this.state.structureEntries, this.state.wildcards, ); - this.props.onDefineSegment(segmentRegExp); + let maxSegmentEntries = this.state.structureEntries; + maxSegmentEntries[maxSegmentEntries.length-2].structureLink = StructureLinkDistance.Max; + + const maxSegmentRegExp = useStructureQueryConstructor( + this.state.structureHeaderColumns, + this.state.structureHeaderColumnsTypes, + maxSegmentEntries, + this.state.wildcards, + ); + + const segmentMatches = useStructureRegularExpressionNestedSearch( + minSegmentRegExp, + maxSegmentRegExp, + this.props.logFileAsString, + this.props.logEntryCharIndexMaps!, + ); + + let entryMatches: number[] = []; + let exitMatches: number[] = []; + segmentMatches.forEach((match) => { + entryMatches.push(match[0]) + exitMatches.push(match[match.length - 1]) + }); + entryMatches = [...new Set(entryMatches)].sort(function (a, b) { return a - b; }); + exitMatches = [...new Set(exitMatches)].sort(function (a, b) { return a - b; }); + + const stack: number[] = []; + let nextEntry = entryMatches.shift()!; + let nextExit = exitMatches.shift()!; + + while (nextEntry !== undefined && nextExit !== undefined) { + if (nextEntry < nextExit) { + stack.push(nextEntry); + nextEntry = entryMatches.shift()!; + } else { + const entry = stack.pop()!; + if (stack.length < maximumSegmentation) + collapsibleRows[entry] = constructNewSegment(entry, nextExit, stack.length); + // else console.log(`Maximum segment level reached: Discarding (${entry}, ${nextExit})`); + nextExit = exitMatches.shift()!; + } + } + while (nextExit !== undefined) { + const entry = stack.pop()!; + if (stack.length < maximumSegmentation) + collapsibleRows[entry] = constructNewSegment(entry, nextExit, stack.length); + nextExit = exitMatches.shift()!; + } + + this.props.onDefineSegment(collapsibleRows); } createWildcard() { @@ -461,6 +543,27 @@ export default class StructureDialog extends React.Component { } } + saveStructureDefinition() { + const { structureHeaderColumns, structureHeaderColumnsTypes, structureEntries, wildcards } = + this.state; + const { onStructureDefinitionSave } = this.props; + + const structureDefiniton: StructureDefinition = { + headerColumns: structureHeaderColumns, + headerColumnsTypes: structureHeaderColumnsTypes, + entries: structureEntries, + wildcards: wildcards, + }; + const structureDefinitonJSON = JSON.stringify(structureDefiniton); + + onStructureDefinitionSave(structureDefinitonJSON); + } + + loadStructureDefinition() { + this.props.onStructureDefinitionLoad(); + this.setState({ isLoadingStructureDefintion: true }); + } + render() { const { structureEntries, wildcards, isRemovingStructureEntries, isStructureMatching } = this.state; @@ -500,6 +603,7 @@ export default class StructureDialog extends React.Component { display: "flex", flexDirection: "row", alignItems: "center", + justifyContent: "space-between", }} > { > - this.props.onClose()}> - - + + this.props.onClose()} + > + + void; + onStructureDefinitionLoad: () => void; +} + +export const StructureSettingsDropdown: React.FunctionComponent = ({ + onStructureDefinitionSave, + onStructureDefinitionLoad, +}) => { + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = (action: string) => { + if (action === "save") onStructureDefinitionSave(); + else if (action === "load") onStructureDefinitionLoad(); + + setAnchorEl(null); + }; + + return ( +
+ + + + handleClose("")} + MenuListProps={{ + "aria-labelledby": "basic-button", + sx: { py: 0 }, + }} + > + + handleClose("save")}> + + + + + Save Structure Definition + + + handleClose("load")}> + + + + + Load Structure Definition + + + + +
+ ); +}; diff --git a/src/viewer/structures/StructureTable.tsx b/src/viewer/structures/StructureTable.tsx index d9faba7..d19e9c4 100644 --- a/src/viewer/structures/StructureTable.tsx +++ b/src/viewer/structures/StructureTable.tsx @@ -97,7 +97,7 @@ export default class StructureTable extends React.Component { return (
- {this.props.headerColumns.map((h, i) => + {this.props.headerColumns.filter(h => !h.name.startsWith("Structure")).map((h, i) => this.renderHeaderColumn(h.name, i, this.getInitialColumnWidth(h.name)), )}
@@ -162,8 +162,9 @@ export default class StructureTable extends React.Component { renderRows(containerWidth: number, containerHeight: number) { const newContainerWidth = containerWidth + STRUCTURE_WIDTH; const result: ReactNode[] = []; - const { structureEntries, isRemovingStructureEntries, headerColumns, onStructureEntryRemoved } = + const { structureEntries, isRemovingStructureEntries, onStructureEntryRemoved } = this.props; + const headerColumns = this.props.headerColumns.filter(h => !h.name.startsWith("Structure")); const structureEntryIconStyle = getStructureTableEntryIconStyle(isRemovingStructureEntries); let structureLinkIndex = 0; @@ -247,8 +248,8 @@ export default class StructureTable extends React.Component { } render() { - const { headerColumns, structureEntries } = this.props; - const numberOfRows = structureEntries.length; + const numberOfRows = this.props.structureEntries.length; + const headerColumns = this.props.headerColumns.filter(h => !h.name.startsWith("Structure")); const containerHeight = numberOfRows * LOG_ROW_HEIGHT + (numberOfRows - 1) * STRUCTURE_LINK_HEIGHT; const containerWidth = diff --git a/src/viewer/types.d.ts b/src/viewer/types.d.ts index c046810..e4988b8 100644 --- a/src/viewer/types.d.ts +++ b/src/viewer/types.d.ts @@ -1,4 +1,4 @@ -import { SelectedRowType, StructureLinkDistance } from "./constants"; +import { SelectedRowType, StructureHeaderColumnType, StructureLinkDistance } from "./constants"; export interface LogViewState { height: number; @@ -16,6 +16,13 @@ export interface Header { type: "string" | "number"; } +export interface StructureDefinition { + headerColumns: Header[], + headerColumnsTypes: StructureHeaderColumnType[], + entries: StructureEntry[], + wildcards: Wildcard[] +} + export interface StructureEntry { row: CellContents[][]; cellSelection: boolean[];