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:
@@ -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={{
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user