1
0
mirror of https://github.com/sasjs/server.git synced 2025-12-11 03:34:35 +00:00

feat(log): added download chunk and entire log

This commit is contained in:
Yury Shkoda
2023-04-21 17:21:09 +03:00
parent 57b7f954a1
commit a38a9f9c3d
3 changed files with 126 additions and 54 deletions

View File

@@ -5,12 +5,14 @@ import { ErrorOutline, Warning } from '@mui/icons-material'
import ContentCopyIcon from '@mui/icons-material/ContentCopy' import ContentCopyIcon from '@mui/icons-material/ContentCopy'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import CheckIcon from '@mui/icons-material/Check' import CheckIcon from '@mui/icons-material/Check'
import FileDownloadIcon from '@mui/icons-material/FileDownload'
import { makeStyles } from '@mui/styles' import { makeStyles } from '@mui/styles'
import { import {
defaultChunkSize, defaultChunkSize,
parseErrorsAndWarnings, parseErrorsAndWarnings,
LogInstance, LogInstance,
clearErrorsAndWarningsHtmlWrapping clearErrorsAndWarningsHtmlWrapping,
download
} from '../../../../../utils' } from '../../../../../utils'
const useStyles: any = makeStyles((theme: any) => ({ const useStyles: any = makeStyles((theme: any) => ({
@@ -44,7 +46,11 @@ interface LogChunkProps {
} }
const LogChunk = (props: LogChunkProps) => { const LogChunk = (props: LogChunkProps) => {
const { id, text, logLineCount, scrollToLogInstance } = props const { id, text, logLineCount } = props
const [scrollToLogInstance, setScrollToLogInstance] = useState(
props.scrollToLogInstance
)
const rowText = clearErrorsAndWarningsHtmlWrapping(text)
const classes = useStyles() const classes = useStyles()
const [expanded, setExpanded] = useState(props.expanded) const [expanded, setExpanded] = useState(props.expanded)
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
@@ -78,6 +84,13 @@ const LogChunk = (props: LogChunkProps) => {
const { errors, warnings } = parseErrorsAndWarnings(text) const { errors, warnings } = parseErrorsAndWarnings(text)
const getLineRange = (separator = ' ... ') =>
`${id * defaultChunkSize}${separator}${
(id + 1) * defaultChunkSize < logLineCount
? (id + 1) * defaultChunkSize
: logLineCount
}`
return ( return (
<div onClick={(evt) => props.onClick(evt, id)}> <div onClick={(evt) => props.onClick(evt, id)}>
<button <button
@@ -103,11 +116,7 @@ const LogChunk = (props: LogChunkProps) => {
alignItems: 'center' alignItems: 'center'
}} }}
> >
<span>{`Lines: ${id * defaultChunkSize} ... ${ <span>{`Lines: ${getLineRange()}`}</span>
(id + 1) * defaultChunkSize < logLineCount
? (id + 1) * defaultChunkSize
: logLineCount
}`}</span>
{copied ? ( {copied ? (
<CheckIcon style={{ fontSize: 20, color: 'green' }} /> <CheckIcon style={{ fontSize: 20, color: 'green' }} />
) : ( ) : (
@@ -116,9 +125,7 @@ const LogChunk = (props: LogChunkProps) => {
onClick={(evt: SyntheticEvent) => { onClick={(evt: SyntheticEvent) => {
evt.stopPropagation() evt.stopPropagation()
navigator.clipboard.writeText( navigator.clipboard.writeText(rowText)
clearErrorsAndWarningsHtmlWrapping(text)
)
setCopied(true) setCopied(true)
@@ -128,11 +135,27 @@ const LogChunk = (props: LogChunkProps) => {
}} }}
/> />
)} )}
<FileDownloadIcon
onClick={(evt: SyntheticEvent) => {
download(evt, rowText, `log.${getLineRange('-')}`)
}}
/>
{errors && errors.length !== 0 && ( {errors && errors.length !== 0 && (
<ErrorOutline color="error" style={{ fontSize: 20 }} /> <ErrorOutline
color="error"
style={{ fontSize: 20 }}
onClick={() => {
setScrollToLogInstance(errors[0])
}}
/>
)} )}
{warnings && warnings.length !== 0 && ( {warnings && warnings.length !== 0 && (
<Warning style={{ fontSize: 20, color: 'green' }} /> <Warning
style={{ fontSize: 20, color: 'green' }}
onClick={() => {
setScrollToLogInstance(warnings[0])
}}
/>
)}{' '} )}{' '}
<ExpandMoreIcon <ExpandMoreIcon
style={{ style={{

View File

@@ -1,3 +1,4 @@
import { useEffect, useState, SyntheticEvent } from 'react'
import TreeView from '@mui/lab/TreeView' import TreeView from '@mui/lab/TreeView'
import TreeItem from '@mui/lab/TreeItem' import TreeItem from '@mui/lab/TreeItem'
import { ChevronRight, ExpandMore } from '@mui/icons-material' import { ChevronRight, ExpandMore } from '@mui/icons-material'
@@ -7,12 +8,15 @@ import { makeStyles } from '@mui/styles'
import Highlight from 'react-highlight' import Highlight from 'react-highlight'
import { LogObject, defaultChunkSize } from '../../../../../utils' import { LogObject, defaultChunkSize } from '../../../../../utils'
import { RunTimeType } from '../../../../../context/appContext' import { RunTimeType } from '../../../../../context/appContext'
import { splitIntoChunks, LogInstance } from '../../../../../utils' import {
splitIntoChunks,
LogInstance,
clearErrorsAndWarningsHtmlWrapping,
download
} from '../../../../../utils'
import LogChunk from './logChunk' import LogChunk from './logChunk'
import { useEffect, useState } from 'react' import FileDownloadIcon from '@mui/icons-material/FileDownload'
import { Button } from '@mui/material'
// TODO:
// link to download log.log
const useStyles: any = makeStyles((theme: any) => ({ const useStyles: any = makeStyles((theme: any) => ({
expansionDescription: { expansionDescription: {
@@ -136,6 +140,7 @@ const LogComponent = (props: LogComponentProps) => {
const hasErrorsOrWarnings = const hasErrorsOrWarnings =
logObject.errors?.length !== 0 || logObject.warnings?.length !== 0 logObject.errors?.length !== 0 || logObject.warnings?.length !== 0
const logBody = typeof log === 'string' ? log : log.body
return ( return (
<> <>
@@ -204,48 +209,50 @@ const LogComponent = (props: LogComponentProps) => {
)} )}
</div> </div>
{Array.isArray(logChunks) ? ( <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
logChunks.map((chunk: string, id: number) => ( {Array.isArray(logChunks) ? (
<LogChunk logChunks.map((chunk: string, id: number) => (
id={id} <LogChunk
text={chunk} id={id}
expanded={logChunksState[id]} text={chunk}
key={`log-chunk-${id}`} expanded={logChunksState[id]}
logLineCount={logObject.linesCount} key={`log-chunk-${id}`}
scrollToLogInstance={scrollToLogInstance} logLineCount={logObject.linesCount}
onClick={(_, chunkNumber) => { scrollToLogInstance={scrollToLogInstance}
setLogChunksState((prevState) => { onClick={(_, chunkNumber) => {
const newState = [...prevState] setLogChunksState((prevState) => {
const expand = !newState[chunkNumber] const newState = [...prevState]
const expand = !newState[chunkNumber]
newState[chunkNumber] = expand newState[chunkNumber] = expand
if (expand) { if (expand) {
const chunkToCollapse = getChunkToAutoCollapse() const chunkToCollapse = getChunkToAutoCollapse()
if (chunkToCollapse !== undefined) { if (chunkToCollapse !== undefined) {
newState[chunkToCollapse] = false newState[chunkToCollapse] = false
}
} }
}
return newState return newState
}) })
setScrollToLogInstance(undefined) setScrollToLogInstance(undefined)
}} }}
/> />
)) ))
) : ( ) : (
<Typography <Typography
id={`log_container`} id={`log_container`}
variant="h5" variant="h5"
className={classes.expansionDescription} className={classes.expansionDescription}
> >
<Highlight className={'html'} innerHTML={true}> <Highlight className={'html'} innerHTML={true}>
{logChunks} {logChunks}
</Highlight> </Highlight>
</Typography> </Typography>
)} )}
</div>
</div> </div>
) : ( ) : (
<div> <div>
@@ -254,10 +261,29 @@ const LogComponent = (props: LogComponentProps) => {
id="log" id="log"
style={{ overflow: 'auto', height: 'calc(100vh - 220px)' }} style={{ overflow: 'auto', height: 'calc(100vh - 220px)' }}
> >
{typeof log === 'string' ? log : log.body} {logBody}
</pre> </pre>
</div> </div>
)} )}
<div
style={{
display: 'flex',
justifyContent: 'center',
marginTop: 10
}}
>
<Button
onClick={(evt: SyntheticEvent) => {
download(evt, clearErrorsAndWarningsHtmlWrapping(logBody))
}}
variant="contained"
color="primary"
startIcon={<FileDownloadIcon />}
>
download entire log
</Button>
</div>
</> </>
) )
} }

View File

@@ -1,3 +1,4 @@
import { SyntheticEvent } from 'react'
import { LogInstance } from './' import { LogInstance } from './'
export const parseErrorsAndWarnings = (log: string) => { export const parseErrorsAndWarnings = (log: string) => {
@@ -100,3 +101,25 @@ export const splitIntoChunks = (log: string, chunkSize = defaultChunkSize) => {
export const clearErrorsAndWarningsHtmlWrapping = (log: string) => export const clearErrorsAndWarningsHtmlWrapping = (log: string) =>
log.replace(/^<font[^>]*>/gm, '').replace(/<\/font>/gm, '') log.replace(/^<font[^>]*>/gm, '').replace(/<\/font>/gm, '')
export const download = (
evt: SyntheticEvent,
log: string,
fileName = 'log'
) => {
evt.stopPropagation()
const file = new Blob([log])
const url = URL.createObjectURL(file)
const a = document.createElement('a')
a.href = url
a.download = `${fileName}.log`
document.body.appendChild(a)
a.click()
setTimeout(() => {
document.body.removeChild(a)
window.URL.revokeObjectURL(url)
}, 0)
}