mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
937
web/package-lock.json
generated
937
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,7 @@
|
||||
"react": "^17.0.2",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-highlight": "^0.15.0",
|
||||
"react-monaco-editor": "^0.48.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-toastify": "^9.0.1"
|
||||
@@ -41,6 +42,7 @@
|
||||
"@types/react": "^17.0.37",
|
||||
"@types/react-copy-to-clipboard": "^5.0.2",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@types/react-highlight": "^0.12.5",
|
||||
"@types/react-router-dom": "^5.3.1",
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-plugin-prismjs": "^2.1.0",
|
||||
@@ -59,6 +61,7 @@
|
||||
"style-loader": "^3.3.1",
|
||||
"ts-loader": "^9.2.6",
|
||||
"typescript": "^4.5.2",
|
||||
"typescript-plugin-css-modules": "^5.0.1",
|
||||
"webpack": "5.64.3",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-dev-server": "4.7.4"
|
||||
|
||||
@@ -9,7 +9,7 @@ import { PermissionsContext } from '../../../../context/permissionsContext'
|
||||
import {
|
||||
findExistingPermission,
|
||||
findUpdatingPermission
|
||||
} from '../../../../utils/helper'
|
||||
} from '../../../../utils'
|
||||
|
||||
const useAddPermission = () => {
|
||||
const {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Dispatch, SetStateAction } from 'react'
|
||||
import { Dispatch, SetStateAction } from 'react'
|
||||
|
||||
import {
|
||||
Backdrop,
|
||||
@@ -17,10 +17,14 @@ import { TabContext, TabList, TabPanel } from '@mui/lab'
|
||||
import FilePathInputModal from '../../components/filePathInputModal'
|
||||
import FileMenu from './internal/components/fileMenu'
|
||||
import RunMenu from './internal/components/runMenu'
|
||||
import LogComponent from './internal/components/log/logComponent'
|
||||
import LogTabWithIcons from './internal/components/log/logTabWithIcons'
|
||||
|
||||
import { usePrompt } from '../../utils/hooks'
|
||||
import { getLanguageFromExtension } from './internal/helper'
|
||||
import useEditor from './internal/hooks/useEditor'
|
||||
import { RunTimeType } from '../../context/appContext'
|
||||
import { LogObject } from '../../utils'
|
||||
|
||||
const StyledTabPanel = styled(TabPanel)(() => ({
|
||||
padding: '10px'
|
||||
@@ -108,6 +112,10 @@ const SASjsEditor = ({
|
||||
/>
|
||||
)
|
||||
|
||||
// INFO: variable indicating if selected run type is SAS if there are any errors or warnings in the log
|
||||
const logWithErrorsOrWarnings =
|
||||
selectedRunTime === RunTimeType.SAS && log && typeof log === 'object'
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}>
|
||||
<Backdrop
|
||||
@@ -145,7 +153,22 @@ const SASjsEditor = ({
|
||||
>
|
||||
<TabList onChange={handleTabChange} centered>
|
||||
<StyledTab label="Code" value="code" />
|
||||
<StyledTab label="Log" value="log" />
|
||||
<StyledTab
|
||||
label={logWithErrorsOrWarnings ? '' : 'log'}
|
||||
value="log"
|
||||
icon={
|
||||
logWithErrorsOrWarnings ? (
|
||||
<LogTabWithIcons log={log as LogObject} />
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
const logWrapper = document.querySelector(`#logWrapper`)
|
||||
|
||||
if (logWrapper) logWrapper.scrollTop = 0
|
||||
}}
|
||||
/>
|
||||
<StyledTab
|
||||
label={
|
||||
<Tooltip title="Displays content from the _webout fileref">
|
||||
@@ -195,15 +218,9 @@ const SASjsEditor = ({
|
||||
</Paper>
|
||||
</StyledTabPanel>
|
||||
<StyledTabPanel value="log">
|
||||
<div>
|
||||
<h2>Log</h2>
|
||||
<pre
|
||||
id="log"
|
||||
style={{ overflow: 'auto', height: 'calc(100vh - 220px)' }}
|
||||
>
|
||||
{log}
|
||||
</pre>
|
||||
</div>
|
||||
{log && (
|
||||
<LogComponent log={log} selectedRunTime={selectedRunTime} />
|
||||
)}
|
||||
</StyledTabPanel>
|
||||
<StyledTabPanel value="webout">
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
.ChunkHeader {
|
||||
color: #444;
|
||||
cursor: pointer;
|
||||
padding: 18px;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border: none;
|
||||
outline: none;
|
||||
transition: 0.4s;
|
||||
box-shadow: 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;
|
||||
}
|
||||
|
||||
.ChunkDetails {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ChunkExpandIcon {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.ChunkBody {
|
||||
background-color: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ChunksContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.LogContainer {
|
||||
background-color: #fbfbfb;
|
||||
border: 1px solid #e2e2e2;
|
||||
border-radius: 3px;
|
||||
min-height: 50px;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
white-space: pre-wrap;
|
||||
font-family: Monaco, Courier, monospace;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.LogWrapper {
|
||||
overflow-y: auto;
|
||||
max-height: calc(100vh - 130px);
|
||||
}
|
||||
|
||||
.LogBody {
|
||||
overflow: auto;
|
||||
height: calc(100vh - 220px);
|
||||
}
|
||||
|
||||
.TreeContainer {
|
||||
background-color: white;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.TabContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.TabDownloadIcon {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.HighlightedLine {
|
||||
background-color: #f6e30599;
|
||||
}
|
||||
|
||||
.Icon {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
|
||||
.GreenIcon {
|
||||
color: green;
|
||||
}
|
||||
168
web/src/containers/Studio/internal/components/log/logChunk.tsx
Normal file
168
web/src/containers/Studio/internal/components/log/logChunk.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
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 CheckIcon from '@mui/icons-material/Check'
|
||||
import FileDownloadIcon from '@mui/icons-material/FileDownload'
|
||||
import {
|
||||
defaultChunkSize,
|
||||
parseErrorsAndWarnings,
|
||||
LogInstance,
|
||||
clearErrorsAndWarningsHtmlWrapping,
|
||||
download
|
||||
} from '../../../../../utils'
|
||||
import { logStyles } from './logComponent'
|
||||
import classes from './log.module.css'
|
||||
|
||||
interface LogChunkProps {
|
||||
id: number
|
||||
text: string
|
||||
expanded: boolean
|
||||
logLineCount: number
|
||||
onClick: (evt: any, id: number) => void
|
||||
scrollToLogInstance?: LogInstance
|
||||
updated: number
|
||||
}
|
||||
|
||||
const LogChunk = (props: LogChunkProps) => {
|
||||
const { id, text, logLineCount } = props
|
||||
const [scrollToLogInstance, setScrollToLogInstance] = useState(
|
||||
props.scrollToLogInstance
|
||||
)
|
||||
const rowText = clearErrorsAndWarningsHtmlWrapping(text)
|
||||
const styles = logStyles()
|
||||
const [expanded, setExpanded] = useState(props.expanded)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setExpanded(props.expanded)
|
||||
}, [props.expanded])
|
||||
|
||||
useEffect(() => {
|
||||
if (props.expanded !== expanded) {
|
||||
setExpanded(props.expanded)
|
||||
}
|
||||
|
||||
if (
|
||||
props.scrollToLogInstance &&
|
||||
props.scrollToLogInstance !== scrollToLogInstance
|
||||
) {
|
||||
setScrollToLogInstance(props.scrollToLogInstance)
|
||||
}
|
||||
}, [props])
|
||||
|
||||
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) {
|
||||
line.className = classes.HighlightedLine
|
||||
|
||||
line.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
|
||||
setTimeout(() => {
|
||||
line.classList.remove(classes.HighlightedLine)
|
||||
|
||||
setScrollToLogInstance(undefined)
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
}, [expanded, scrollToLogInstance, props])
|
||||
|
||||
const { errors, warnings } = parseErrorsAndWarnings(text)
|
||||
|
||||
const getLineRange = (separator = ' ... ') =>
|
||||
`${id * defaultChunkSize}${separator}${
|
||||
(id + 1) * defaultChunkSize < logLineCount
|
||||
? (id + 1) * defaultChunkSize
|
||||
: logLineCount
|
||||
}`
|
||||
|
||||
return (
|
||||
<div onClick={(evt) => props.onClick(evt, id)}>
|
||||
<button className={classes.ChunkHeader}>
|
||||
<Typography variant="subtitle1">
|
||||
<div className={classes.ChunkDetails}>
|
||||
<span>{`Lines: ${getLineRange()}`}</span>
|
||||
{copied ? (
|
||||
<CheckIcon
|
||||
className={[classes.Icon, classes.GreenIcon].join(' ')}
|
||||
/>
|
||||
) : (
|
||||
<ContentCopyIcon
|
||||
className={classes.Icon}
|
||||
onClick={(evt: SyntheticEvent) => {
|
||||
evt.stopPropagation()
|
||||
|
||||
navigator.clipboard.writeText(rowText)
|
||||
|
||||
setCopied(true)
|
||||
|
||||
setTimeout(() => {
|
||||
setCopied(false)
|
||||
}, 1000)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<FileDownloadIcon
|
||||
onClick={(evt: SyntheticEvent) => {
|
||||
download(evt, rowText, `.${getLineRange('-')}`)
|
||||
}}
|
||||
/>
|
||||
{errors && errors.length !== 0 && (
|
||||
<ErrorOutline
|
||||
color="error"
|
||||
className={classes.Icon}
|
||||
onClick={() => {
|
||||
setScrollToLogInstance(errors[0])
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{warnings && warnings.length !== 0 && (
|
||||
<Warning
|
||||
className={[classes.Icon, classes.GreenIcon].join(' ')}
|
||||
onClick={(evt) => {
|
||||
if (expanded) evt.stopPropagation()
|
||||
|
||||
setScrollToLogInstance(warnings[0])
|
||||
}}
|
||||
/>
|
||||
)}{' '}
|
||||
<ExpandMoreIcon
|
||||
className={classes.ChunkExpandIcon}
|
||||
style={{
|
||||
transform: expanded ? 'rotate(180deg)' : 'unset'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Typography>
|
||||
</button>
|
||||
<div
|
||||
className={classes.ChunkBody}
|
||||
style={{
|
||||
display: expanded ? 'block' : 'none'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
id={`log_container`}
|
||||
className={[styles.expansionDescription, classes.LogContainer].join(
|
||||
' '
|
||||
)}
|
||||
>
|
||||
<Highlight className={'html'} innerHTML={true}>
|
||||
{expanded ? text : ''}
|
||||
</Highlight>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogChunk
|
||||
@@ -0,0 +1,243 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import TreeView from '@mui/lab/TreeView'
|
||||
import TreeItem from '@mui/lab/TreeItem'
|
||||
import { ChevronRight, ExpandMore } from '@mui/icons-material'
|
||||
import { Typography } from '@mui/material'
|
||||
import { ListItemText } from '@mui/material'
|
||||
import { makeStyles } from '@mui/styles'
|
||||
import Highlight from 'react-highlight'
|
||||
import { LogObject, defaultChunkSize } from '../../../../../utils'
|
||||
import { RunTimeType } from '../../../../../context/appContext'
|
||||
import { splitIntoChunks, LogInstance } from '../../../../../utils'
|
||||
import LogChunk from './logChunk'
|
||||
import classes from './log.module.css'
|
||||
|
||||
export const logStyles: any = makeStyles((theme: any) => ({
|
||||
expansionDescription: {
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
fontSize: theme.typography.pxToRem(12)
|
||||
},
|
||||
[theme.breakpoints.up('md')]: {
|
||||
fontSize: theme.typography.pxToRem(16)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
interface LogComponentProps {
|
||||
log: LogObject | string
|
||||
selectedRunTime: RunTimeType | string
|
||||
}
|
||||
|
||||
const LogComponent = (props: LogComponentProps) => {
|
||||
const { log, selectedRunTime } = props
|
||||
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 styles = logStyles()
|
||||
|
||||
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`)
|
||||
|
||||
if (logWrapper) {
|
||||
logWrapper.scrollTop = logWrapper.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
const getChunkToAutoCollapse = () => {
|
||||
const openedChunks = logChunksState
|
||||
.map((chunkState: boolean, id: number) => (chunkState ? id : undefined))
|
||||
.filter((chunk) => chunk !== undefined)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
const hasErrorsOrWarnings =
|
||||
logObject.errors?.length !== 0 || logObject.warnings?.length !== 0
|
||||
const logBody = typeof log === 'string' ? log : log.body
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedRunTime === RunTimeType.SAS && logObject.body ? (
|
||||
<div id="logWrapper" className={classes.LogWrapper}>
|
||||
<div>
|
||||
{hasErrorsOrWarnings && (
|
||||
<div className={classes.TreeContainer}>
|
||||
<TreeView
|
||||
defaultCollapseIcon={<ExpandMore />}
|
||||
defaultExpandIcon={<ChevronRight />}
|
||||
>
|
||||
{logObject.errors && logObject.errors.length !== 0 && (
|
||||
<TreeItem
|
||||
nodeId="errors"
|
||||
label={
|
||||
<Typography color="error">
|
||||
{`Errors (${logObject.errors.length})`}
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
{logObject.errors &&
|
||||
logObject.errors.map((error, ind) => (
|
||||
<TreeItem
|
||||
nodeId={`error_${ind}`}
|
||||
label={<ListItemText primary={error.body} />}
|
||||
key={`error_${ind}`}
|
||||
onClick={() => goToLogLine(error, ind)}
|
||||
/>
|
||||
))}
|
||||
</TreeItem>
|
||||
)}
|
||||
{logObject.warnings && logObject.warnings.length !== 0 && (
|
||||
<TreeItem
|
||||
nodeId="warnings"
|
||||
label={
|
||||
<Typography>{`Warnings (${logObject.warnings.length})`}</Typography>
|
||||
}
|
||||
>
|
||||
{logObject.warnings &&
|
||||
logObject.warnings.map((warning, ind) => (
|
||||
<TreeItem
|
||||
nodeId={`warning_${ind}`}
|
||||
label={<ListItemText primary={warning.body} />}
|
||||
key={`warning_${ind}`}
|
||||
onClick={() => goToLogLine(warning, ind)}
|
||||
/>
|
||||
))}
|
||||
</TreeItem>
|
||||
)}
|
||||
</TreeView>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={classes.ChunksContainer}>
|
||||
{Array.isArray(logChunks) ? (
|
||||
logChunks.map((chunk: string, id: number) => (
|
||||
<LogChunk
|
||||
id={id}
|
||||
text={chunk}
|
||||
expanded={logChunksState[id]}
|
||||
key={`log-chunk-${id}`}
|
||||
logLineCount={logObject.linesCount}
|
||||
scrollToLogInstance={scrollToLogInstance}
|
||||
updated={Date.now()}
|
||||
onClick={(_, 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={[
|
||||
styles.expansionDescription,
|
||||
classes.LogContainer
|
||||
].join(' ')}
|
||||
>
|
||||
<Highlight className={'html'} innerHTML={true}>
|
||||
{logChunks}
|
||||
</Highlight>
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<h2>Log</h2>
|
||||
<pre id="log" className={classes.LogBody}>
|
||||
{logBody}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogComponent
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ErrorOutline, Warning } from '@mui/icons-material'
|
||||
import FileDownloadIcon from '@mui/icons-material/FileDownload'
|
||||
import {
|
||||
LogObject,
|
||||
download,
|
||||
clearErrorsAndWarningsHtmlWrapping
|
||||
} from '../../../../../utils'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
import classes from './log.module.css'
|
||||
|
||||
interface LogTabProps {
|
||||
log: LogObject
|
||||
}
|
||||
|
||||
const LogTabWithIcons = (props: LogTabProps) => {
|
||||
const { errors, warnings, body } = props.log
|
||||
|
||||
return (
|
||||
<div className={classes.TabContainer}>
|
||||
<span>log</span>
|
||||
{errors && errors.length !== 0 && (
|
||||
<ErrorOutline color="error" className={classes.Icon} />
|
||||
)}
|
||||
{warnings && warnings.length !== 0 && (
|
||||
<Warning className={[classes.Icon, classes.GreenIcon].join(' ')} />
|
||||
)}
|
||||
<Tooltip
|
||||
title="Download entire log"
|
||||
onClick={(evt) => {
|
||||
download(evt, clearErrorsAndWarningsHtmlWrapping(body))
|
||||
}}
|
||||
>
|
||||
<FileDownloadIcon
|
||||
className={[classes.Icon, classes.TabDownloadIcon].join(' ')}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogTabWithIcons
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
useSnackbar,
|
||||
useStateWithCallback
|
||||
} from '../../../../utils/hooks'
|
||||
import { parseErrorsAndWarnings, LogObject } from '../../../../utils'
|
||||
|
||||
const SASJS_LOGS_SEPARATOR =
|
||||
'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
|
||||
@@ -41,10 +42,12 @@ const useEditor = ({
|
||||
|
||||
const [prevFileContent, setPrevFileContent] = useStateWithCallback('')
|
||||
const [fileContent, setFileContent] = useState('')
|
||||
const [log, setLog] = useState('')
|
||||
const [log, setLog] = useState<LogObject | string>()
|
||||
const [webout, setWebout] = useState('')
|
||||
const [runTimes, setRunTimes] = useState<string[]>([])
|
||||
const [selectedRunTime, setSelectedRunTime] = useState('')
|
||||
const [selectedRunTime, setSelectedRunTime] = useState<RunTimeType | string>(
|
||||
''
|
||||
)
|
||||
const [selectedFileExtension, setSelectedFileExtension] = useState('')
|
||||
const [openFilePathInputModal, setOpenFilePathInputModal] = useState(false)
|
||||
const [showDiff, setShowDiff] = useState(false)
|
||||
@@ -150,6 +153,13 @@ const useEditor = ({
|
||||
const runCode = useCallback(
|
||||
(code: string) => {
|
||||
setIsLoading(true)
|
||||
|
||||
// Scroll to bottom of log
|
||||
const logElement = document.getElementById('log')
|
||||
if (logElement) logElement.scrollTop = logElement.scrollHeight
|
||||
|
||||
setIsLoading(false)
|
||||
|
||||
axios
|
||||
.post(`/SASjsApi/code/execute`, {
|
||||
code: programPathInjection(
|
||||
@@ -160,8 +170,24 @@ const useEditor = ({
|
||||
runTime: selectedRunTime
|
||||
})
|
||||
.then((res: any) => {
|
||||
if (selectedRunTime === RunTimeType.SAS) {
|
||||
const { errors, warnings, logLines } = parseErrorsAndWarnings(
|
||||
res.data.split(SASJS_LOGS_SEPARATOR)[1]
|
||||
)
|
||||
|
||||
const log: LogObject = {
|
||||
body: logLines.join(`\n`),
|
||||
errors,
|
||||
warnings,
|
||||
linesCount: logLines.length
|
||||
}
|
||||
|
||||
setLog(log)
|
||||
} else {
|
||||
setLog(res.data.split(SASJS_LOGS_SEPARATOR)[1] ?? '')
|
||||
}
|
||||
|
||||
setWebout(res.data.split(SASJS_LOGS_SEPARATOR)[0] ?? '')
|
||||
setLog(res.data.split(SASJS_LOGS_SEPARATOR)[1] ?? '')
|
||||
setTab('log')
|
||||
|
||||
// Scroll to bottom of log
|
||||
@@ -249,7 +275,7 @@ const useEditor = ({
|
||||
}, [appContext.runTimes])
|
||||
|
||||
useEffect(() => {
|
||||
if (runTimes.length) setSelectedRunTime(runTimes[0])
|
||||
if (runTimes.length) setSelectedRunTime(runTimes[0] as RunTimeType)
|
||||
}, [runTimes])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -280,7 +306,6 @@ const useEditor = ({
|
||||
const content = localStorage.getItem('fileContent') ?? ''
|
||||
setFileContent(content)
|
||||
}
|
||||
setLog('')
|
||||
setWebout('')
|
||||
setTab('code')
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -294,7 +319,9 @@ const useEditor = ({
|
||||
|
||||
useEffect(() => {
|
||||
const fileExtension = selectedFileExtension.toLowerCase()
|
||||
if (runTimes.includes(fileExtension)) setSelectedRunTime(fileExtension)
|
||||
|
||||
if (runTimes.includes(fileExtension))
|
||||
setSelectedRunTime(fileExtension as RunTimeType)
|
||||
}, [selectedFileExtension, runTimes])
|
||||
|
||||
return {
|
||||
|
||||
4
web/src/types/declaration.d.ts
vendored
Normal file
4
web/src/types/declaration.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module '*.module.css' {
|
||||
const classes: { [key: string]: string }
|
||||
export default classes
|
||||
}
|
||||
3
web/src/utils/index.ts
Normal file
3
web/src/utils/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './log'
|
||||
export * from './types'
|
||||
export * from './helper'
|
||||
133
web/src/utils/log.ts
Normal file
133
web/src/utils/log.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { SyntheticEvent } from 'react'
|
||||
import { LogInstance } from './'
|
||||
|
||||
export const parseErrorsAndWarnings = (log: string) => {
|
||||
const logLines = log.split('\n')
|
||||
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({
|
||||
body: errorLine,
|
||||
line: index,
|
||||
type: 'error',
|
||||
id: errorLines.length
|
||||
})
|
||||
}
|
||||
|
||||
// INFO: check if line starts with ERROR
|
||||
else if (/^ERROR/gm.test(line)) {
|
||||
errorLines.push({
|
||||
body: line,
|
||||
line: index,
|
||||
type: 'error',
|
||||
id: errorLines.length
|
||||
})
|
||||
|
||||
logLines[index] =
|
||||
`<font id="error_${
|
||||
errorLines.length - 1
|
||||
}" style="color: red;" ref={scrollTo}>` +
|
||||
logLines[index] +
|
||||
'</font>'
|
||||
}
|
||||
|
||||
// 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({
|
||||
body: warningLine,
|
||||
line: index,
|
||||
type: 'warning',
|
||||
id: warningLines.length
|
||||
})
|
||||
}
|
||||
|
||||
// INFO: check if line starts with WARNING
|
||||
else if (/^WARNING/gm.test(line)) {
|
||||
warningLines.push({
|
||||
body: line,
|
||||
line: index,
|
||||
type: 'warning',
|
||||
id: warningLines.length
|
||||
})
|
||||
|
||||
logLines[index] =
|
||||
`<font id="warning_${warningLines.length - 1}" style="color: green;">` +
|
||||
logLines[index] +
|
||||
'</font>'
|
||||
}
|
||||
})
|
||||
|
||||
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) 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, '')
|
||||
|
||||
export const download = (evt: SyntheticEvent, log: string, fileName = '') => {
|
||||
evt.stopPropagation()
|
||||
|
||||
const padWithZero = (num: number) => (num < 9 ? `0${num}` : `${num}`)
|
||||
|
||||
const date = new Date()
|
||||
const datePrefix = [
|
||||
date.getFullYear(),
|
||||
padWithZero(date.getMonth() + 1),
|
||||
padWithZero(date.getDate()),
|
||||
padWithZero(date.getHours()),
|
||||
padWithZero(date.getMinutes()),
|
||||
padWithZero(date.getSeconds())
|
||||
].join('')
|
||||
|
||||
const file = new Blob([log])
|
||||
const url = URL.createObjectURL(file)
|
||||
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `${datePrefix}${fileName}.log`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
window.URL.revokeObjectURL(url)
|
||||
}, 0)
|
||||
}
|
||||
@@ -39,3 +39,18 @@ export interface TreeNode {
|
||||
isFolder: boolean
|
||||
children: Array<TreeNode>
|
||||
}
|
||||
|
||||
export interface LogInstance {
|
||||
body: string
|
||||
line: number
|
||||
type: 'error' | 'warning'
|
||||
id: number
|
||||
ref?: any
|
||||
}
|
||||
|
||||
export interface LogObject {
|
||||
body: string
|
||||
errors?: LogInstance[]
|
||||
warnings?: LogInstance[]
|
||||
linesCount: number
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
"jsx": "react-jsx",
|
||||
"plugins": [{ "name": "typescript-plugin-css-modules" }]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@@ -32,9 +32,18 @@ const config: Configuration = {
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
exclude: ['/node_modules/'],
|
||||
use: ['style-loader', 'css-loader']
|
||||
test: /\.css$/i,
|
||||
use: [
|
||||
'style-loader',
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
modules: {
|
||||
localIdentName: '[local]--[hash:base64:5]'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
|
||||
Reference in New Issue
Block a user