mirror of
https://github.com/sasjs/server.git
synced 2025-12-11 03:34:35 +00:00
feat(log): split large log into chunks
This commit is contained in:
153
web/src/containers/Studio/internal/components/log/logChunk.tsx
Normal file
153
web/src/containers/Studio/internal/components/log/logChunk.tsx
Normal file
@@ -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 (
|
||||||
|
<div onClick={(evt) => props.onClick(evt, id)}>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
color: '#444',
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: '18px',
|
||||||
|
width: '100%',
|
||||||
|
textAlign: 'left',
|
||||||
|
border: 'none',
|
||||||
|
outline: 'none',
|
||||||
|
transition: '0.4s',
|
||||||
|
boxShadow:
|
||||||
|
'rgba(0, 0, 0, 0.2) 0px 2px 1px -1px, rgba(0, 0, 0, 0.14) 0px 1px 1px 0px, rgba(0, 0, 0, 0.12) 0px 1px 3px 0px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="subtitle1">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 6,
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{`Lines: ${id * defaultChunkSize} ... ${
|
||||||
|
(id + 1) * defaultChunkSize < logLineCount
|
||||||
|
? (id + 1) * defaultChunkSize
|
||||||
|
: logLineCount
|
||||||
|
}`}</span>
|
||||||
|
<ContentCopyIcon
|
||||||
|
style={{ fontSize: 20 }}
|
||||||
|
onClick={(evt: SyntheticEvent) => {
|
||||||
|
evt.stopPropagation()
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
clearErrorsAndWarningsHtmlWrapping(text)
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors && errors.length !== 0 && (
|
||||||
|
<ErrorOutline color="error" style={{ fontSize: 20 }} />
|
||||||
|
)}
|
||||||
|
{warnings && warnings.length !== 0 && (
|
||||||
|
<Warning style={{ fontSize: 20, color: 'green' }} />
|
||||||
|
)}{' '}
|
||||||
|
<ExpandMoreIcon
|
||||||
|
style={{
|
||||||
|
marginLeft: 'auto',
|
||||||
|
transform: expanded ? 'rotate(180deg)' : 'unset'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '0 18px',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
display: expanded ? 'block' : 'none',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div id={`log_container`} className={classes.expansionDescription}>
|
||||||
|
<Highlight className={'html'} innerHTML={true}>
|
||||||
|
{expanded ? text : ''}
|
||||||
|
</Highlight>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LogChunk
|
||||||
@@ -5,8 +5,14 @@ import { Typography } from '@mui/material'
|
|||||||
import { ListItemText } from '@mui/material'
|
import { ListItemText } from '@mui/material'
|
||||||
import { makeStyles } from '@mui/styles'
|
import { makeStyles } from '@mui/styles'
|
||||||
import Highlight from 'react-highlight'
|
import Highlight from 'react-highlight'
|
||||||
import { LogObject } from '../../../../../utils'
|
import { LogObject, defaultChunkSize } from '../../../../../utils'
|
||||||
import { RunTimeType } from '../../../../../context/appContext'
|
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) => ({
|
const useStyles: any = makeStyles((theme: any) => ({
|
||||||
expansionDescription: {
|
expansionDescription: {
|
||||||
@@ -37,32 +43,96 @@ interface LogComponentProps {
|
|||||||
const LogComponent = (props: LogComponentProps) => {
|
const LogComponent = (props: LogComponentProps) => {
|
||||||
const { log, selectedRunTime } = props
|
const { log, selectedRunTime } = props
|
||||||
const logObject = log as LogObject
|
const logObject = log as LogObject
|
||||||
|
const logChunks = splitIntoChunks(logObject?.body || '')
|
||||||
|
const [logChunksState, setLogChunksState] = useState<boolean[]>(
|
||||||
|
new Array(logChunks.length).fill(false)
|
||||||
|
)
|
||||||
|
|
||||||
|
const [scrollToLogInstance, setScrollToLogInstance] = useState<LogInstance>()
|
||||||
|
const [oldestExpandedChunk, setOldestExpandedChunk] = useState<number>(
|
||||||
|
logChunksState.length - 1
|
||||||
|
)
|
||||||
|
const maxOpenedChunks = 2
|
||||||
|
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
const goToLogLine = (type: 'error' | 'warning', ind: number) => {
|
const goToLogLine = (logInstance: LogInstance, ind: number) => {
|
||||||
const line = document.getElementById(`${type}_${ind}`)
|
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 =
|
const logWrapper: HTMLDivElement | null =
|
||||||
document.querySelector(`#logWrapper`)
|
document.querySelector(`#logWrapper`)
|
||||||
const logContainer: HTMLHeadElement | null =
|
|
||||||
document.querySelector(`#log_container`)
|
|
||||||
|
|
||||||
if (line && logWrapper && logContainer) {
|
if (logWrapper) {
|
||||||
line.style.backgroundColor = '#f6e30599'
|
logWrapper.scrollTop = logWrapper.scrollHeight
|
||||||
logWrapper.scrollTop =
|
|
||||||
line.offsetTop - logWrapper.offsetTop + logContainer.offsetTop
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
line.setAttribute('style', '')
|
|
||||||
}, 3000)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const decodeHtml = (encodedString: string) => {
|
const getChunkToAutoCollapse = () => {
|
||||||
const tempElement = document.createElement('textarea')
|
const openedChunks = logChunksState
|
||||||
tempElement.innerHTML = encodedString
|
.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 (
|
return (
|
||||||
@@ -100,9 +170,19 @@ const LogComponent = (props: LogComponentProps) => {
|
|||||||
logObject.errors.map((error, ind) => (
|
logObject.errors.map((error, ind) => (
|
||||||
<TreeItem
|
<TreeItem
|
||||||
nodeId={`error_${ind}`}
|
nodeId={`error_${ind}`}
|
||||||
label={<ListItemText primary={error} />}
|
label={<ListItemText primary={error.body} />}
|
||||||
key={`error_${ind}`}
|
key={`error_${ind}`}
|
||||||
onClick={() => goToLogLine('error', ind)}
|
onClick={() => {
|
||||||
|
setLogChunksState((prevState) => {
|
||||||
|
const newState = [...prevState]
|
||||||
|
|
||||||
|
newState[ind] = true
|
||||||
|
|
||||||
|
return newState
|
||||||
|
})
|
||||||
|
|
||||||
|
goToLogLine(error, ind)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</TreeItem>
|
</TreeItem>
|
||||||
@@ -118,9 +198,19 @@ const LogComponent = (props: LogComponentProps) => {
|
|||||||
logObject.warnings.map((warning, ind) => (
|
logObject.warnings.map((warning, ind) => (
|
||||||
<TreeItem
|
<TreeItem
|
||||||
nodeId={`warning_${ind}`}
|
nodeId={`warning_${ind}`}
|
||||||
label={<ListItemText primary={warning} />}
|
label={<ListItemText primary={warning.body} />}
|
||||||
key={`warning_${ind}`}
|
key={`warning_${ind}`}
|
||||||
onClick={() => goToLogLine('warning', ind)}
|
onClick={() => {
|
||||||
|
setLogChunksState((prevState) => {
|
||||||
|
const newState = [...prevState]
|
||||||
|
|
||||||
|
newState[ind] = true
|
||||||
|
|
||||||
|
return newState
|
||||||
|
})
|
||||||
|
|
||||||
|
goToLogLine(warning, ind)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</TreeItem>
|
</TreeItem>
|
||||||
@@ -129,15 +219,48 @@ const LogComponent = (props: LogComponentProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Typography
|
{Array.isArray(logChunks) ? (
|
||||||
id={`log_container`}
|
logChunks.map((chunk: string, id: number) => (
|
||||||
variant="h5"
|
<LogChunk
|
||||||
className={classes.expansionDescription}
|
id={id}
|
||||||
>
|
text={chunk}
|
||||||
<Highlight className={'html'} innerHTML={true}>
|
expanded={logChunksState[id]}
|
||||||
{decodeHtml(logObject?.body || '')}
|
key={`log-chunk-${id}`}
|
||||||
</Highlight>
|
logLineCount={logObject.linesCount}
|
||||||
</Typography>
|
scrollToLogInstance={scrollToLogInstance}
|
||||||
|
onClick={(evt, chunkNumber) => {
|
||||||
|
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)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Typography
|
||||||
|
id={`log_container`}
|
||||||
|
variant="h5"
|
||||||
|
className={classes.expansionDescription}
|
||||||
|
>
|
||||||
|
<Highlight className={'html'} innerHTML={true}>
|
||||||
|
{logChunks}
|
||||||
|
</Highlight>
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -178,7 +178,8 @@ const useEditor = ({
|
|||||||
const log: LogObject = {
|
const log: LogObject = {
|
||||||
body: logLines.join(`\n`),
|
body: logLines.join(`\n`),
|
||||||
errors,
|
errors,
|
||||||
warnings
|
warnings,
|
||||||
|
linesCount: logLines.length
|
||||||
}
|
}
|
||||||
|
|
||||||
setLog(log)
|
setLog(log)
|
||||||
|
|||||||
@@ -1,21 +1,36 @@
|
|||||||
|
import { LogInstance } from './'
|
||||||
|
|
||||||
export const parseErrorsAndWarnings = (log: string) => {
|
export const parseErrorsAndWarnings = (log: string) => {
|
||||||
const logLines = log.split('\n')
|
const logLines = log.split('\n')
|
||||||
const errorLines: string[] = []
|
const errorLines: LogInstance[] = []
|
||||||
const warningLines: string[] = []
|
const warningLines: LogInstance[] = []
|
||||||
|
|
||||||
logLines.forEach((line: string, index: number) => {
|
logLines.forEach((line: string, index: number) => {
|
||||||
// INFO: check if content in element starts with ERROR
|
// INFO: check if content in element starts with ERROR
|
||||||
if (/<.*>ERROR/gm.test(line)) {
|
if (/<.*>ERROR/gm.test(line)) {
|
||||||
const errorLine = line.substring(line.indexOf('E'), line.length - 1)
|
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
|
// INFO: check if line starts with ERROR
|
||||||
else if (/^ERROR/gm.test(line)) {
|
else if (/^ERROR/gm.test(line)) {
|
||||||
errorLines.push(line)
|
errorLines.push({
|
||||||
|
body: line,
|
||||||
|
line: index,
|
||||||
|
type: 'error',
|
||||||
|
id: errorLines.length
|
||||||
|
})
|
||||||
|
|
||||||
logLines[index] =
|
logLines[index] =
|
||||||
`<font id="error_${errorLines.length - 1}" style="color: red;">` +
|
`<font id="error_${
|
||||||
|
errorLines.length - 1
|
||||||
|
}" style="color: red;" ref={scrollTo}>` +
|
||||||
logLines[index] +
|
logLines[index] +
|
||||||
'</font>'
|
'</font>'
|
||||||
}
|
}
|
||||||
@@ -23,12 +38,23 @@ export const parseErrorsAndWarnings = (log: string) => {
|
|||||||
// INFO: check if content in element starts with WARNING
|
// INFO: check if content in element starts with WARNING
|
||||||
else if (/<.*>WARNING/gm.test(line)) {
|
else if (/<.*>WARNING/gm.test(line)) {
|
||||||
const warningLine = line.substring(line.indexOf('W'), line.length - 1)
|
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
|
// INFO: check if line starts with WARNING
|
||||||
else if (/^WARNING/gm.test(line)) {
|
else if (/^WARNING/gm.test(line)) {
|
||||||
warningLines.push(line)
|
warningLines.push({
|
||||||
|
body: line,
|
||||||
|
line: index,
|
||||||
|
type: 'warning',
|
||||||
|
id: warningLines.length
|
||||||
|
})
|
||||||
|
|
||||||
logLines[index] =
|
logLines[index] =
|
||||||
`<font id="warning_${warningLines.length - 1}" style="color: green;">` +
|
`<font id="warning_${warningLines.length - 1}" style="color: green;">` +
|
||||||
@@ -39,3 +65,38 @@ export const parseErrorsAndWarnings = (log: string) => {
|
|||||||
|
|
||||||
return { errors: errorLines, warnings: warningLines, logLines }
|
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(/^<font[^>]*>/gm, '').replace(/<\/font>/gm, '')
|
||||||
|
|||||||
@@ -40,8 +40,17 @@ export interface TreeNode {
|
|||||||
children: Array<TreeNode>
|
children: Array<TreeNode>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LogInstance {
|
||||||
|
body: string
|
||||||
|
line: number
|
||||||
|
type: 'error' | 'warning'
|
||||||
|
id: number
|
||||||
|
ref?: any
|
||||||
|
}
|
||||||
|
|
||||||
export interface LogObject {
|
export interface LogObject {
|
||||||
body: string
|
body: string
|
||||||
errors?: string[]
|
errors?: LogInstance[]
|
||||||
warnings?: string[]
|
warnings?: LogInstance[]
|
||||||
|
linesCount: number
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user