mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 11:24:35 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24966e695a | ||
|
|
5c40d8a342 | ||
| 6f5566dabb | |||
| d93470d183 | |||
| 330c020933 | |||
|
|
a810f6c7cf | ||
|
|
5d6c6086b4 | ||
|
|
0edcbdcefc | ||
|
|
ea0222f218 | ||
| edc2e2a302 | |||
|
|
efd2e1450e | ||
|
|
1092a73c10 | ||
| 9977c9d161 | |||
|
|
5c0eff5197 | ||
|
|
3bda991a58 | ||
| 0327f7c6ec | |||
| 92549402eb | |||
| 0749d65173 |
22
CHANGELOG.md
22
CHANGELOG.md
@@ -1,3 +1,25 @@
|
||||
## [0.21.3](https://github.com/sasjs/server/compare/v0.21.2...v0.21.3) (2022-09-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* return same tokens if not expired ([330c020](https://github.com/sasjs/server/commit/330c020933f1080261b38f07d6b627f6d7c62446))
|
||||
|
||||
## [0.21.2](https://github.com/sasjs/server/compare/v0.21.1...v0.21.2) (2022-09-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* default content-type for sas programs should be text/plain ([9977c9d](https://github.com/sasjs/server/commit/9977c9d161947b11d45ab2513f99a5320a3f5a06))
|
||||
* **studio:** inject program path to code before sending for execution ([edc2e2a](https://github.com/sasjs/server/commit/edc2e2a302ccea4985f3d6b83ef8c23620ab82b6))
|
||||
|
||||
## [0.21.1](https://github.com/sasjs/server/compare/v0.21.0...v0.21.1) (2022-09-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* SASJS_WEBOUT_HEADERS path for windows ([0749d65](https://github.com/sasjs/server/commit/0749d65173e8cfe9a93464711b7be1e123c289ff))
|
||||
|
||||
# [0.21.0](https://github.com/sasjs/server/compare/v0.20.0...v0.21.0) (2022-09-19)
|
||||
|
||||
|
||||
|
||||
14
api/package-lock.json
generated
14
api/package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "0.0.2",
|
||||
"dependencies": {
|
||||
"@sasjs/core": "^4.31.3",
|
||||
"@sasjs/utils": "2.42.1",
|
||||
"@sasjs/utils": "2.48.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"connect-mongo": "^4.6.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
@@ -1396,9 +1396,9 @@
|
||||
"integrity": "sha512-TpVqWl5bqp3JTQjIg0r4WiQg7Ima5f17eAJILJbdYDdXsnLXlA/Csbb95G7eDPhzWpM3C0NrzKek3yvCMGzXIA=="
|
||||
},
|
||||
"node_modules/@sasjs/utils": {
|
||||
"version": "2.42.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.42.1.tgz",
|
||||
"integrity": "sha512-DzHNYjeoj2eUkwV7Sa4eHCKRoTrYaQ6eyv6c1U5qOYXwVdZpMoYA3HFsHj55UcMOn2U3CXI5nrR7PZlUmVwVbQ==",
|
||||
"version": "2.48.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.48.1.tgz",
|
||||
"integrity": "sha512-Eu9p66JKLeTj0KK3kfY7YLQYq+MDMS1Q1/FOFfRe9hV23mFsuzierVMrnEYGK0JaHOogdHLmwzg6iVLDT8Jssg==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@types/fs-extra": "9.0.13",
|
||||
@@ -10974,9 +10974,9 @@
|
||||
"integrity": "sha512-TpVqWl5bqp3JTQjIg0r4WiQg7Ima5f17eAJILJbdYDdXsnLXlA/Csbb95G7eDPhzWpM3C0NrzKek3yvCMGzXIA=="
|
||||
},
|
||||
"@sasjs/utils": {
|
||||
"version": "2.42.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.42.1.tgz",
|
||||
"integrity": "sha512-DzHNYjeoj2eUkwV7Sa4eHCKRoTrYaQ6eyv6c1U5qOYXwVdZpMoYA3HFsHj55UcMOn2U3CXI5nrR7PZlUmVwVbQ==",
|
||||
"version": "2.48.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.48.1.tgz",
|
||||
"integrity": "sha512-Eu9p66JKLeTj0KK3kfY7YLQYq+MDMS1Q1/FOFfRe9hV23mFsuzierVMrnEYGK0JaHOogdHLmwzg6iVLDT8Jssg==",
|
||||
"requires": {
|
||||
"@types/fs-extra": "9.0.13",
|
||||
"@types/prompts": "2.0.13",
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"author": "4GL Ltd",
|
||||
"dependencies": {
|
||||
"@sasjs/core": "^4.31.3",
|
||||
"@sasjs/utils": "2.42.1",
|
||||
"@sasjs/utils": "2.48.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"connect-mongo": "^4.6.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
|
||||
@@ -660,10 +660,10 @@ paths:
|
||||
anyOf:
|
||||
- {type: string}
|
||||
- {type: string, format: byte}
|
||||
description: 'Execute SAS code.'
|
||||
summary: 'Run SAS Code and returns log'
|
||||
description: 'Execute Code on the Specified Runtime'
|
||||
summary: 'Run Code and Return Webout Content and Log'
|
||||
tags:
|
||||
- CODE
|
||||
- Code
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
@@ -1658,8 +1658,8 @@ paths:
|
||||
anyOf:
|
||||
- {type: string}
|
||||
- {type: string, format: byte}
|
||||
description: "Trigger a SAS or JS program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms"
|
||||
summary: 'Execute a Stored Program, returns raw _webout content.'
|
||||
description: "Trigger a Stored Program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms"
|
||||
summary: 'Execute a Stored Program, returns _webout and (optionally) log.'
|
||||
tags:
|
||||
- STP
|
||||
security:
|
||||
@@ -1667,7 +1667,7 @@ paths:
|
||||
bearerAuth: []
|
||||
parameters:
|
||||
-
|
||||
description: 'Location of SAS or JS code'
|
||||
description: 'Location of code in SASjs Drive'
|
||||
in: query
|
||||
name: _program
|
||||
required: true
|
||||
@@ -1685,8 +1685,8 @@ paths:
|
||||
anyOf:
|
||||
- {type: string}
|
||||
- {type: string, format: byte}
|
||||
description: "Trigger a SAS or JS program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms\n\nThe response will be a JSON object with the following root attributes:\nlog, webout, headers.\n\nThe webout attribute will be nested JSON ONLY if the response-header\ncontains a content-type of application/json AND it is valid JSON.\nOtherwise it will be a stringified version of the webout content."
|
||||
summary: 'Execute a Stored Program, return a JSON object'
|
||||
description: "Trigger a Stored Program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms"
|
||||
summary: 'Execute a Stored Program, returns _webout and (optionally) log.'
|
||||
tags:
|
||||
- STP
|
||||
security:
|
||||
@@ -1694,7 +1694,7 @@ paths:
|
||||
bearerAuth: []
|
||||
parameters:
|
||||
-
|
||||
description: 'Location of SAS or JS code'
|
||||
description: 'Location of code in SASjs Drive'
|
||||
in: query
|
||||
name: _program
|
||||
required: false
|
||||
@@ -1798,7 +1798,7 @@ tags:
|
||||
name: Client
|
||||
description: 'Operations about clients'
|
||||
-
|
||||
name: CODE
|
||||
name: Code
|
||||
description: 'Execution of code (various runtimes are supported)'
|
||||
-
|
||||
name: Drive
|
||||
|
||||
@@ -4,6 +4,7 @@ import { InfoJWT } from '../types'
|
||||
import {
|
||||
generateAccessToken,
|
||||
generateRefreshToken,
|
||||
getTokensFromDB,
|
||||
removeTokensInDB,
|
||||
saveTokensInDB
|
||||
} from '../utils'
|
||||
@@ -73,6 +74,15 @@ const token = async (data: any): Promise<TokenResponse> => {
|
||||
|
||||
AuthController.deleteCode(userInfo.userId, clientId)
|
||||
|
||||
// get tokens from DB
|
||||
const existingTokens = await getTokensFromDB(userInfo.userId, clientId)
|
||||
if (existingTokens) {
|
||||
return {
|
||||
accessToken: existingTokens.accessToken,
|
||||
refreshToken: existingTokens.refreshToken
|
||||
}
|
||||
}
|
||||
|
||||
const accessToken = generateAccessToken(userInfo)
|
||||
const refreshToken = generateRefreshToken(userInfo)
|
||||
|
||||
|
||||
@@ -24,11 +24,11 @@ interface ExecuteCodePayload {
|
||||
|
||||
@Security('bearerAuth')
|
||||
@Route('SASjsApi/code')
|
||||
@Tags('CODE')
|
||||
@Tags('Code')
|
||||
export class CodeController {
|
||||
/**
|
||||
* Execute SAS code.
|
||||
* @summary Run SAS Code and returns log
|
||||
* Execute Code on the Specified Runtime
|
||||
* @summary Run Code and Return Webout Content and Log
|
||||
*/
|
||||
@Post('/execute')
|
||||
public async executeCode(
|
||||
|
||||
@@ -92,6 +92,9 @@ export class SASSessionController extends SessionController {
|
||||
path: sessionFolder
|
||||
}
|
||||
|
||||
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
||||
await createFile(headersPath, 'Content-type: text/plain')
|
||||
|
||||
// we do not want to leave sessions running forever
|
||||
// we clean them up after a predefined period, if unused
|
||||
this.scheduleSessionDestroy(session)
|
||||
@@ -170,7 +173,7 @@ ${autoExecContent}`
|
||||
session.ready = true
|
||||
}
|
||||
|
||||
public async deleteSession(session: Session) {
|
||||
private async deleteSession(session: Session) {
|
||||
// remove the temporary files, to avoid buildup
|
||||
await deleteFolder(session.path)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isWindows } from '@sasjs/utils'
|
||||
import { escapeWinSlashes } from '@sasjs/utils'
|
||||
import { PreProgramVars, Session } from '../../types'
|
||||
import { generateFileUploadJSCode } from '../../utils'
|
||||
import { ExecutionVars } from './'
|
||||
@@ -21,13 +21,9 @@ export const createJSProgram = async (
|
||||
|
||||
const preProgramVarStatments = `
|
||||
let _webout = '';
|
||||
const weboutPath = '${
|
||||
isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath
|
||||
}';
|
||||
const _SASJS_TOKENFILE = '${
|
||||
isWindows() ? tokenFile.replace(/\\/g, '\\\\') : tokenFile
|
||||
}';
|
||||
const _SASJS_WEBOUT_HEADERS = '${headersPath}';
|
||||
const weboutPath = '${escapeWinSlashes(weboutPath)}';
|
||||
const _SASJS_TOKENFILE = '${escapeWinSlashes(tokenFile)}';
|
||||
const _SASJS_WEBOUT_HEADERS = '${escapeWinSlashes(headersPath)}';
|
||||
const _SASJS_USERNAME = '${preProgramVariables?.username}';
|
||||
const _SASJS_USERID = '${preProgramVariables?.userId}';
|
||||
const _SASJS_DISPLAYNAME = '${preProgramVariables?.displayName}';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isWindows } from '@sasjs/utils'
|
||||
import { escapeWinSlashes } from '@sasjs/utils'
|
||||
import { PreProgramVars, Session } from '../../types'
|
||||
import { generateFileUploadPythonCode } from '../../utils'
|
||||
import { ExecutionVars } from './'
|
||||
@@ -19,14 +19,10 @@ export const createPythonProgram = async (
|
||||
)
|
||||
|
||||
const preProgramVarStatments = `
|
||||
_SASJS_SESSION_PATH = '${
|
||||
isWindows() ? session.path.replace(/\\/g, '\\\\') : session.path
|
||||
}';
|
||||
_WEBOUT = '${isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath}';
|
||||
_SASJS_WEBOUT_HEADERS = '${headersPath}';
|
||||
_SASJS_TOKENFILE = '${
|
||||
isWindows() ? tokenFile.replace(/\\/g, '\\\\') : tokenFile
|
||||
}';
|
||||
_SASJS_SESSION_PATH = '${escapeWinSlashes(session.path)}';
|
||||
_WEBOUT = '${escapeWinSlashes(weboutPath)}';
|
||||
_SASJS_WEBOUT_HEADERS = '${escapeWinSlashes(headersPath)}';
|
||||
_SASJS_TOKENFILE = '${escapeWinSlashes(tokenFile)}';
|
||||
_SASJS_USERNAME = '${preProgramVariables?.username}';
|
||||
_SASJS_USERID = '${preProgramVariables?.userId}';
|
||||
_SASJS_DISPLAYNAME = '${preProgramVariables?.displayName}';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isWindows } from '@sasjs/utils'
|
||||
import { escapeWinSlashes } from '@sasjs/utils'
|
||||
import { PreProgramVars, Session } from '../../types'
|
||||
import { generateFileUploadRCode } from '../../utils'
|
||||
import { ExecutionVars } from '.'
|
||||
@@ -19,14 +19,10 @@ export const createRProgram = async (
|
||||
)
|
||||
|
||||
const preProgramVarStatments = `
|
||||
._SASJS_SESSION_PATH <- '${
|
||||
isWindows() ? session.path.replace(/\\/g, '\\\\') : session.path
|
||||
}';
|
||||
._WEBOUT <- '${isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath}';
|
||||
._SASJS_WEBOUT_HEADERS <- '${headersPath}';
|
||||
._SASJS_TOKENFILE <- '${
|
||||
isWindows() ? tokenFile.replace(/\\/g, '\\\\') : tokenFile
|
||||
}';
|
||||
._SASJS_SESSION_PATH <- '${escapeWinSlashes(session.path)}';
|
||||
._WEBOUT <- '${escapeWinSlashes(weboutPath)}';
|
||||
._SASJS_WEBOUT_HEADERS <- '${escapeWinSlashes(headersPath)}';
|
||||
._SASJS_TOKENFILE <- '${escapeWinSlashes(tokenFile)}';
|
||||
._SASJS_USERNAME <- '${preProgramVariables?.username}';
|
||||
._SASJS_USERID <- '${preProgramVariables?.userId}';
|
||||
._SASJS_DISPLAYNAME <- '${preProgramVariables?.displayName}';
|
||||
|
||||
@@ -8,6 +8,7 @@ export const createSASProgram = async (
|
||||
vars: ExecutionVars,
|
||||
session: Session,
|
||||
weboutPath: string,
|
||||
headersPath: string,
|
||||
tokenFile: string,
|
||||
otherArgs?: any
|
||||
) => {
|
||||
@@ -23,7 +24,7 @@ export const createSASProgram = async (
|
||||
%let _sasjs_displayname=${preProgramVariables?.displayName};
|
||||
%let _sasjs_apiserverurl=${preProgramVariables?.serverUrl};
|
||||
%let _sasjs_apipath=/SASjsApi/stp/execute;
|
||||
%let _sasjs_webout_headers=%sysfunc(pathname(work))/../stpsrv_header.txt;
|
||||
%let _sasjs_webout_headers=${headersPath};
|
||||
%let _metaperson=&_sasjs_displayname;
|
||||
%let _metauser=&_sasjs_username;
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ export const processProgram = async (
|
||||
vars,
|
||||
session,
|
||||
weboutPath,
|
||||
headersPath,
|
||||
tokenFile,
|
||||
otherArgs
|
||||
)
|
||||
|
||||
@@ -23,14 +23,14 @@ interface ExecutePostRequestPayload {
|
||||
@Tags('STP')
|
||||
export class STPController {
|
||||
/**
|
||||
* Trigger a SAS or JS program using the _program URL parameter.
|
||||
* Trigger a Stored Program using the _program URL parameter.
|
||||
*
|
||||
* Accepts URL parameters and file uploads. For more details, see docs:
|
||||
*
|
||||
* https://server.sasjs.io/storedprograms
|
||||
*
|
||||
* @summary Execute a Stored Program, returns raw _webout content.
|
||||
* @param _program Location of SAS or JS code
|
||||
* @summary Execute a Stored Program, returns _webout and (optionally) log.
|
||||
* @param _program Location of code in SASjs Drive
|
||||
* @example _program "/Projects/myApp/some/program"
|
||||
*/
|
||||
@Get('/execute')
|
||||
@@ -43,21 +43,15 @@ export class STPController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a SAS or JS program using the _program URL parameter.
|
||||
* Trigger a Stored Program using the _program URL parameter.
|
||||
*
|
||||
* Accepts URL parameters and file uploads. For more details, see docs:
|
||||
*
|
||||
* https://server.sasjs.io/storedprograms
|
||||
*
|
||||
* The response will be a JSON object with the following root attributes:
|
||||
* log, webout, headers.
|
||||
*
|
||||
* The webout attribute will be nested JSON ONLY if the response-header
|
||||
* contains a content-type of application/json AND it is valid JSON.
|
||||
* Otherwise it will be a stringified version of the webout content.
|
||||
*
|
||||
* @summary Execute a Stored Program, return a JSON object
|
||||
* @param _program Location of SAS or JS code
|
||||
* @summary Execute a Stored Program, returns _webout and (optionally) log.
|
||||
* @param _program Location of code in SASjs Drive
|
||||
* @example _program "/Projects/myApp/some/program"
|
||||
*/
|
||||
@Post('/execute')
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
authenticateRefreshToken
|
||||
} from '../../middlewares'
|
||||
|
||||
import { authorizeValidation, tokenValidation } from '../../utils'
|
||||
import { tokenValidation } from '../../utils'
|
||||
import { InfoJWT } from '../../types'
|
||||
|
||||
const authRouter = express.Router()
|
||||
|
||||
34
api/src/utils/getTokensFromDB.ts
Normal file
34
api/src/utils/getTokensFromDB.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
import User from '../model/User'
|
||||
|
||||
export const getTokensFromDB = async (userId: number, clientId: string) => {
|
||||
const user = await User.findOne({ id: userId })
|
||||
if (!user) return
|
||||
|
||||
const currentTokenObj = user.tokens.find(
|
||||
(tokenObj: any) => tokenObj.clientId === clientId
|
||||
)
|
||||
|
||||
if (currentTokenObj) {
|
||||
const accessToken = currentTokenObj.accessToken
|
||||
const refreshToken = currentTokenObj.refreshToken
|
||||
|
||||
const verifiedAccessToken: any = jwt.verify(
|
||||
accessToken,
|
||||
process.secrets.ACCESS_TOKEN_SECRET
|
||||
)
|
||||
|
||||
const verifiedRefreshToken: any = jwt.verify(
|
||||
refreshToken,
|
||||
process.secrets.REFRESH_TOKEN_SECRET
|
||||
)
|
||||
|
||||
if (
|
||||
verifiedAccessToken?.userId === userId &&
|
||||
verifiedAccessToken?.clientId === clientId &&
|
||||
verifiedRefreshToken?.userId === userId &&
|
||||
verifiedRefreshToken?.clientId === clientId
|
||||
)
|
||||
return { accessToken, refreshToken }
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ export * from './getDesktopFields'
|
||||
export * from './getPreProgramVariables'
|
||||
export * from './getRunTimeAndFilePath'
|
||||
export * from './getServerUrl'
|
||||
export * from './getTokensFromDB'
|
||||
export * from './instantiateLogger'
|
||||
export * from './isDebugOn'
|
||||
export * from './isPublicRoute'
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"description": "Operations about clients"
|
||||
},
|
||||
{
|
||||
"name": "CODE",
|
||||
"name": "Code",
|
||||
"description": "Execution of code (various runtimes are supported)"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { RunTimeType } from '../../../context/appContext'
|
||||
|
||||
export const getLanguageFromExtension = (extension: string) => {
|
||||
if (extension === 'js') return 'javascript'
|
||||
|
||||
@@ -12,3 +14,26 @@ export const getSelection = (editor: any) => {
|
||||
const selection = editor?.getModel().getValueInRange(editor?.getSelection())
|
||||
return selection ?? ''
|
||||
}
|
||||
|
||||
export const programPathInjection = (
|
||||
code: string,
|
||||
path: string,
|
||||
runtime: RunTimeType
|
||||
) => {
|
||||
if (path) {
|
||||
if (runtime === RunTimeType.JS) {
|
||||
return `const _PROGRAM = '${path}';\n${code}`
|
||||
}
|
||||
if (runtime === RunTimeType.PY) {
|
||||
return `_PROGRAM = '${path}';\n${code}`
|
||||
}
|
||||
if (runtime === RunTimeType.R) {
|
||||
return `._PROGRAM = '${path}';\n${code}`
|
||||
}
|
||||
if (runtime === RunTimeType.SAS) {
|
||||
return `%let _program = '${path}';\n${code}`
|
||||
}
|
||||
}
|
||||
|
||||
return code
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from 'react'
|
||||
import { DiffEditorDidMount, EditorDidMount, monaco } from 'react-monaco-editor'
|
||||
import { SelectChangeEvent } from '@mui/material'
|
||||
import { getSelection } from '../helper'
|
||||
import { getSelection, programPathInjection } from '../helper'
|
||||
import { AppContext, RunTimeType } from '../../../../context/appContext'
|
||||
import { AlertSeverityType } from '../../../../components/snackbar'
|
||||
import {
|
||||
@@ -151,7 +151,14 @@ const useEditor = ({
|
||||
const runCode = (code: string) => {
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime })
|
||||
.post(`/SASjsApi/code/execute`, {
|
||||
code: programPathInjection(
|
||||
code,
|
||||
selectedFilePath,
|
||||
selectedRunTime as RunTimeType
|
||||
),
|
||||
runTime: selectedRunTime
|
||||
})
|
||||
.then((res: any) => {
|
||||
setWebout(res.data.split(SASJS_LOGS_SEPARATOR)[0] ?? '')
|
||||
setLog(res.data.split(SASJS_LOGS_SEPARATOR)[1] ?? '')
|
||||
|
||||
Reference in New Issue
Block a user