diff --git a/web/src/containers/Studio/internal/components/log/logChunk.tsx b/web/src/containers/Studio/internal/components/log/logChunk.tsx new file mode 100644 index 0000000..e6d1a53 --- /dev/null +++ b/web/src/containers/Studio/internal/components/log/logChunk.tsx @@ -0,0 +1,153 @@ +import { useState, useEffect, SyntheticEvent } from 'react' +import { Typography } from '@mui/material' +import Highlight from 'react-highlight' +import { ErrorOutline, Warning } from '@mui/icons-material' +import ContentCopyIcon from '@mui/icons-material/ContentCopy' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import { makeStyles } from '@mui/styles' +import { + defaultChunkSize, + parseErrorsAndWarnings, + LogInstance, + clearErrorsAndWarningsHtmlWrapping +} from '../../../../../utils' + +const useStyles: any = makeStyles((theme: any) => ({ + expansionDescription: { + backgroundColor: '#fbfbfb', + border: '1px solid #e2e2e2', + borderRadius: '3px', + minHeight: '50px', + padding: '10px', + boxSizing: 'border-box', + whiteSpace: 'pre-wrap', + fontFamily: 'Monaco, Courier, monospace', + position: 'relative', + width: '100%', + [theme.breakpoints.down('sm')]: { + fontSize: theme.typography.pxToRem(12) + }, + [theme.breakpoints.up('md')]: { + fontSize: theme.typography.pxToRem(16) + } + } +})) + +interface LogChunkProps { + id: number + text: string + expanded: boolean + logLineCount: number + onClick: (evt: any, id: number) => void + scrollToLogInstance?: LogInstance +} + +const LogChunk = (props: LogChunkProps) => { + const { id, text, logLineCount, scrollToLogInstance } = props + + const classes = useStyles() + const [expanded, setExpanded] = useState(props.expanded) + + useEffect(() => { + setExpanded(props.expanded) + }, [props.expanded]) + + useEffect(() => { + if (expanded && scrollToLogInstance) { + const { type, id } = scrollToLogInstance + const line = document.getElementById(`${type}_${id}`) + const logWrapper: HTMLDivElement | null = + document.querySelector(`#logWrapper`) + const logContainer: HTMLHeadElement | null = + document.querySelector(`#log_container`) + + if (line && logWrapper && logContainer) { + const initialColor = line.style.color + + line.style.backgroundColor = '#f6e30599' + + line.scrollIntoView({ behavior: 'smooth', block: 'start' }) + + setTimeout(() => { + line.setAttribute('style', `color: ${initialColor};`) + }, 3000) + } + } + }, [expanded, scrollToLogInstance]) + + const { errors, warnings } = parseErrorsAndWarnings(text) + + return ( +
props.onClick(evt, id)}> + +
+
+ + {expanded ? text : ''} + +
+
+
+ ) +} + +export default LogChunk diff --git a/web/src/containers/Studio/internal/components/log/logComponent.tsx b/web/src/containers/Studio/internal/components/log/logComponent.tsx index 112f172..2eb7afc 100644 --- a/web/src/containers/Studio/internal/components/log/logComponent.tsx +++ b/web/src/containers/Studio/internal/components/log/logComponent.tsx @@ -5,8 +5,14 @@ import { Typography } from '@mui/material' import { ListItemText } from '@mui/material' import { makeStyles } from '@mui/styles' import Highlight from 'react-highlight' -import { LogObject } from '../../../../../utils' +import { LogObject, defaultChunkSize } from '../../../../../utils' import { RunTimeType } from '../../../../../context/appContext' +import { splitIntoChunks, LogInstance } from '../../../../../utils' +import LogChunk from './logChunk' +import { useEffect, useState } from 'react' + +// TODO: +// link to download log.log const useStyles: any = makeStyles((theme: any) => ({ expansionDescription: { @@ -37,32 +43,96 @@ interface LogComponentProps { const LogComponent = (props: LogComponentProps) => { const { log, selectedRunTime } = props const logObject = log as LogObject + const logChunks = splitIntoChunks(logObject?.body || '') + const [logChunksState, setLogChunksState] = useState( + new Array(logChunks.length).fill(false) + ) + + const [scrollToLogInstance, setScrollToLogInstance] = useState() + const [oldestExpandedChunk, setOldestExpandedChunk] = useState( + logChunksState.length - 1 + ) + const maxOpenedChunks = 2 const classes = useStyles() - const goToLogLine = (type: 'error' | 'warning', ind: number) => { - const line = document.getElementById(`${type}_${ind}`) + const goToLogLine = (logInstance: LogInstance, ind: number) => { + let chunkNumber = 0 + + for ( + let i = 0; + i <= Math.ceil(logObject.linesCount / defaultChunkSize); + i++ + ) { + if (logInstance.line < (i + 1) * defaultChunkSize) { + chunkNumber = i + + break + } + } + + setLogChunksState((prevState) => { + const newState = [...prevState] + newState[chunkNumber] = true + + const chunkToCollapse = getChunkToAutoCollapse() + + if (chunkToCollapse !== undefined) { + newState[chunkToCollapse] = false + } + + return newState + }) + + setScrollToLogInstance(logInstance) + } + + useEffect(() => { + // INFO: expand the last chunk by default + setLogChunksState((prevState) => { + const lastChunk = prevState.length - 1 + + const newState = [...prevState] + newState[lastChunk] = true + + return newState + }) + + setTimeout(() => { + scrollToTheBottom() + }, 100) + }, []) + + // INFO: scroll to the bottom of the log + const scrollToTheBottom = () => { const logWrapper: HTMLDivElement | null = document.querySelector(`#logWrapper`) - const logContainer: HTMLHeadElement | null = - document.querySelector(`#log_container`) - if (line && logWrapper && logContainer) { - line.style.backgroundColor = '#f6e30599' - logWrapper.scrollTop = - line.offsetTop - logWrapper.offsetTop + logContainer.offsetTop - - setTimeout(() => { - line.setAttribute('style', '') - }, 3000) + if (logWrapper) { + logWrapper.scrollTop = logWrapper.scrollHeight } } - const decodeHtml = (encodedString: string) => { - const tempElement = document.createElement('textarea') - tempElement.innerHTML = encodedString + const getChunkToAutoCollapse = () => { + const openedChunks = logChunksState + .map((chunkState: boolean, id: number) => (chunkState ? id : undefined)) + .filter((chunk) => chunk !== undefined) - return tempElement.value + if (openedChunks.length < maxOpenedChunks) return undefined + else { + const chunkToCollapse = oldestExpandedChunk + const newOldestChunk = openedChunks.filter( + (chunk) => chunk !== chunkToCollapse + )[0] + + if (newOldestChunk !== undefined) { + setOldestExpandedChunk(newOldestChunk) + + return chunkToCollapse + } + + return undefined + } } return ( @@ -100,9 +170,19 @@ const LogComponent = (props: LogComponentProps) => { logObject.errors.map((error, ind) => ( } + label={} key={`error_${ind}`} - onClick={() => goToLogLine('error', ind)} + onClick={() => { + setLogChunksState((prevState) => { + const newState = [...prevState] + + newState[ind] = true + + return newState + }) + + goToLogLine(error, ind) + }} /> ))} @@ -118,9 +198,19 @@ const LogComponent = (props: LogComponentProps) => { logObject.warnings.map((warning, ind) => ( } + label={} key={`warning_${ind}`} - onClick={() => goToLogLine('warning', ind)} + onClick={() => { + setLogChunksState((prevState) => { + const newState = [...prevState] + + newState[ind] = true + + return newState + }) + + goToLogLine(warning, ind) + }} /> ))} @@ -129,15 +219,48 @@ const LogComponent = (props: LogComponentProps) => { - - - {decodeHtml(logObject?.body || '')} - - + {Array.isArray(logChunks) ? ( + logChunks.map((chunk: string, id: number) => ( + { + setLogChunksState((prevState) => { + const newState = [...prevState] + const expand = !newState[chunkNumber] + + newState[chunkNumber] = expand + + if (expand) { + const chunkToCollapse = getChunkToAutoCollapse() + + if (chunkToCollapse !== undefined) { + newState[chunkToCollapse] = false + } + } + + return newState + }) + + setScrollToLogInstance(undefined) + }} + /> + )) + ) : ( + + + {logChunks} + + + )} ) : (
diff --git a/web/src/containers/Studio/internal/hooks/useEditor.ts b/web/src/containers/Studio/internal/hooks/useEditor.ts index 04459df..5baba2d 100644 --- a/web/src/containers/Studio/internal/hooks/useEditor.ts +++ b/web/src/containers/Studio/internal/hooks/useEditor.ts @@ -178,7 +178,8 @@ const useEditor = ({ const log: LogObject = { body: logLines.join(`\n`), errors, - warnings + warnings, + linesCount: logLines.length } setLog(log) diff --git a/web/src/utils/log.ts b/web/src/utils/log.ts index 4b8cc59..a3a7af9 100644 --- a/web/src/utils/log.ts +++ b/web/src/utils/log.ts @@ -1,21 +1,36 @@ +import { LogInstance } from './' + export const parseErrorsAndWarnings = (log: string) => { const logLines = log.split('\n') - const errorLines: string[] = [] - const warningLines: string[] = [] + const errorLines: LogInstance[] = [] + const warningLines: LogInstance[] = [] logLines.forEach((line: string, index: number) => { // INFO: check if content in element starts with ERROR if (/<.*>ERROR/gm.test(line)) { const errorLine = line.substring(line.indexOf('E'), line.length - 1) - errorLines.push(errorLine) + + errorLines.push({ + body: errorLine, + line: index, + type: 'error', + id: errorLines.length + }) } // INFO: check if line starts with ERROR else if (/^ERROR/gm.test(line)) { - errorLines.push(line) + errorLines.push({ + body: line, + line: index, + type: 'error', + id: errorLines.length + }) logLines[index] = - `` + + `` + logLines[index] + '' } @@ -23,12 +38,23 @@ export const parseErrorsAndWarnings = (log: string) => { // INFO: check if content in element starts with WARNING else if (/<.*>WARNING/gm.test(line)) { const warningLine = line.substring(line.indexOf('W'), line.length - 1) - warningLines.push(warningLine) + + warningLines.push({ + body: warningLine, + line: index, + type: 'warning', + id: warningLines.length + }) } // INFO: check if line starts with WARNING else if (/^WARNING/gm.test(line)) { - warningLines.push(line) + warningLines.push({ + body: line, + line: index, + type: 'warning', + id: warningLines.length + }) logLines[index] = `` + @@ -39,3 +65,38 @@ export const parseErrorsAndWarnings = (log: string) => { return { errors: errorLines, warnings: warningLines, logLines } } + +export const defaultChunkSize = 20000 + +export const isTheLastChunk = ( + lineCount: number, + chunkNumber: number, + chunkSize = defaultChunkSize +) => { + if (lineCount <= chunkSize) return true + + const chunksNumber = Math.ceil(lineCount / chunkSize) + + return chunkNumber === chunksNumber +} + +export const splitIntoChunks = (log: string, chunkSize = defaultChunkSize) => { + if (!log.length) return [] + + const logLines: string[] = log.split(`\n`) + + if (logLines.length <= chunkSize) return log + + const chunks: string[] = [] + + while (logLines.length) { + const chunk = logLines.splice(0, chunkSize) + + chunks.push(chunk.join(`\n`)) + } + + return chunks +} + +export const clearErrorsAndWarningsHtmlWrapping = (log: string) => + log.replace(/^]*>/gm, '').replace(/<\/font>/gm, '') diff --git a/web/src/utils/types.ts b/web/src/utils/types.ts index b0e9b1d..8a10b5a 100644 --- a/web/src/utils/types.ts +++ b/web/src/utils/types.ts @@ -40,8 +40,17 @@ export interface TreeNode { children: Array } +export interface LogInstance { + body: string + line: number + type: 'error' | 'warning' + id: number + ref?: any +} + export interface LogObject { body: string - errors?: string[] - warnings?: string[] + errors?: LogInstance[] + warnings?: LogInstance[] + linesCount: number }