mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adc5aca0f0 | ||
|
|
71c6be6b84 | ||
|
|
9c751877d1 | ||
|
|
2204d54cd6 | ||
|
|
f4eb75ff34 | ||
|
|
a3cde343b7 | ||
|
|
7a70d40dbf | ||
|
|
d27e070fc8 | ||
|
|
27e260e6a4 |
23
CHANGELOG.md
23
CHANGELOG.md
@@ -2,6 +2,29 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
### [0.0.42](https://github.com/sasjs/server/compare/v0.0.41...v0.0.42) (2022-03-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* execute api, webout as raw ([9c75187](https://github.com/sasjs/server/commit/9c751877d1ed0d0677aff816169a1df7c34c6bf5))
|
||||||
|
|
||||||
|
### [0.0.41](https://github.com/sasjs/server/compare/v0.0.40...v0.0.41) (2022-03-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **scroll:** closes [#100](https://github.com/sasjs/server/issues/100) ([f4eb75f](https://github.com/sasjs/server/commit/f4eb75ff347e78ac334e55ee26fbdd247bb8eaa2))
|
||||||
|
|
||||||
|
### [0.0.40](https://github.com/sasjs/server/compare/v0.0.39...v0.0.40) (2022-03-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **deploy:** validating empty file or service in filetree ([27e260e](https://github.com/sasjs/server/commit/27e260e6a453e9978830db63ab669bd48c029897))
|
||||||
|
* macros available for SAS ([7a70d40](https://github.com/sasjs/server/commit/7a70d40dbf0cd91cb3af156755f10006b860f917))
|
||||||
|
* moved macros from codebase to drive ([d27e070](https://github.com/sasjs/server/commit/d27e070fc83894854278df22a8223b8016a1f5f7))
|
||||||
|
|
||||||
### [0.0.39](https://github.com/sasjs/server/compare/v0.0.38...v0.0.39) (2022-03-23)
|
### [0.0.39](https://github.com/sasjs/server/compare/v0.0.38...v0.0.39) (2022-03-23)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
},
|
},
|
||||||
"nodemonConfig": {
|
"nodemonConfig": {
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"tmp/appStreamConfig.json"
|
"tmp/**/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,6 +161,8 @@ components:
|
|||||||
$ref: '#/components/schemas/FolderMember'
|
$ref: '#/components/schemas/FolderMember'
|
||||||
-
|
-
|
||||||
$ref: '#/components/schemas/ServiceMember'
|
$ref: '#/components/schemas/ServiceMember'
|
||||||
|
-
|
||||||
|
$ref: '#/components/schemas/FileMember'
|
||||||
type: array
|
type: array
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
@@ -172,20 +174,30 @@ components:
|
|||||||
enum:
|
enum:
|
||||||
- service
|
- service
|
||||||
type: string
|
type: string
|
||||||
MemberType.file:
|
|
||||||
enum:
|
|
||||||
- file
|
|
||||||
type: string
|
|
||||||
ServiceMember:
|
ServiceMember:
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
type:
|
type:
|
||||||
anyOf:
|
$ref: '#/components/schemas/MemberType.service'
|
||||||
-
|
code:
|
||||||
$ref: '#/components/schemas/MemberType.service'
|
type: string
|
||||||
-
|
required:
|
||||||
$ref: '#/components/schemas/MemberType.file'
|
- name
|
||||||
|
- type
|
||||||
|
- code
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
MemberType.file:
|
||||||
|
enum:
|
||||||
|
- file
|
||||||
|
type: string
|
||||||
|
FileMember:
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
$ref: '#/components/schemas/MemberType.file'
|
||||||
code:
|
code:
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
@@ -203,6 +215,8 @@ components:
|
|||||||
$ref: '#/components/schemas/FolderMember'
|
$ref: '#/components/schemas/FolderMember'
|
||||||
-
|
-
|
||||||
$ref: '#/components/schemas/ServiceMember'
|
$ref: '#/components/schemas/ServiceMember'
|
||||||
|
-
|
||||||
|
$ref: '#/components/schemas/FileMember'
|
||||||
type: array
|
type: array
|
||||||
required:
|
required:
|
||||||
- members
|
- members
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { asyncForEach, copy, createFolder, deleteFolder } from '@sasjs/utils'
|
import {
|
||||||
|
asyncForEach,
|
||||||
|
copy,
|
||||||
|
createFile,
|
||||||
|
createFolder,
|
||||||
|
deleteFolder,
|
||||||
|
listFilesInFolder
|
||||||
|
} from '@sasjs/utils'
|
||||||
|
|
||||||
import { apiRoot, sasJSCoreMacros } from '../src/utils'
|
import { apiRoot, sasJSCoreMacros, sasJSCoreMacrosInfo } from '../src/utils'
|
||||||
|
|
||||||
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
||||||
|
|
||||||
@@ -16,6 +23,10 @@ export const copySASjsCore = async () => {
|
|||||||
|
|
||||||
await copy(coreSubFolderPath, sasJSCoreMacros)
|
await copy(coreSubFolderPath, sasJSCoreMacros)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const fileNames = await listFilesInFolder(sasJSCoreMacros)
|
||||||
|
|
||||||
|
await createFile(sasJSCoreMacrosInfo, fileNames.join('\n'))
|
||||||
}
|
}
|
||||||
|
|
||||||
copySASjsCore()
|
copySASjsCore()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import cors from 'cors'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
connectDB,
|
connectDB,
|
||||||
|
copySASjsCore,
|
||||||
getWebBuildFolderPath,
|
getWebBuildFolderPath,
|
||||||
loadAppStreamConfig,
|
loadAppStreamConfig,
|
||||||
sasJSCoreMacros,
|
sasJSCoreMacros,
|
||||||
@@ -42,6 +43,8 @@ const onError: ErrorRequestHandler = (err, req, res, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default setProcessVariables().then(async () => {
|
export default setProcessVariables().then(async () => {
|
||||||
|
await copySASjsCore()
|
||||||
|
|
||||||
// loading these modules after setting up variables due to
|
// loading these modules after setting up variables due to
|
||||||
// multer's usage of process var process.driveLoc
|
// multer's usage of process var process.driveLoc
|
||||||
const { setupRoutes } = await import('./routes/setupRoutes')
|
const { setupRoutes } = await import('./routes/setupRoutes')
|
||||||
@@ -53,8 +56,6 @@ export default setProcessVariables().then(async () => {
|
|||||||
// index.html needs to be injected with some js script.
|
// index.html needs to be injected with some js script.
|
||||||
app.use(express.static(getWebBuildFolderPath()))
|
app.use(express.static(getWebBuildFolderPath()))
|
||||||
|
|
||||||
console.log('sasJSCoreMacros', sasJSCoreMacros)
|
|
||||||
|
|
||||||
app.use(onError)
|
app.use(onError)
|
||||||
|
|
||||||
await connectDB()
|
await connectDB()
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import {
|
|||||||
extractHeaders,
|
extractHeaders,
|
||||||
generateFileUploadSasCode,
|
generateFileUploadSasCode,
|
||||||
getTmpFilesFolderPath,
|
getTmpFilesFolderPath,
|
||||||
|
getTmpMacrosPath,
|
||||||
HTTPHeaders,
|
HTTPHeaders,
|
||||||
isDebugOn,
|
isDebugOn
|
||||||
sasJSCoreMacros
|
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
|
|
||||||
export interface ExecutionVars {
|
export interface ExecutionVars {
|
||||||
@@ -106,7 +106,7 @@ export class ExecutionController {
|
|||||||
`
|
`
|
||||||
|
|
||||||
program = `
|
program = `
|
||||||
options insert=(SASAUTOS="${sasJSCoreMacros}");
|
options insert=(SASAUTOS="${getTmpMacrosPath()}");
|
||||||
|
|
||||||
/* runtime vars */
|
/* runtime vars */
|
||||||
${varStatments}
|
${varStatments}
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { MemberType, FolderMember, ServiceMember, FileTree } from '../../types'
|
import {
|
||||||
|
MemberType,
|
||||||
|
FolderMember,
|
||||||
|
ServiceMember,
|
||||||
|
FileTree,
|
||||||
|
FileMember
|
||||||
|
} from '../../types'
|
||||||
import { getTmpFilesFolderPath } from '../../utils/file'
|
import { getTmpFilesFolderPath } from '../../utils/file'
|
||||||
import { createFolder, createFile, asyncForEach } from '@sasjs/utils'
|
import { createFolder, createFile, asyncForEach } from '@sasjs/utils'
|
||||||
|
|
||||||
// REFACTOR: export FileTreeCpntroller
|
// REFACTOR: export FileTreeCpntroller
|
||||||
export const createFileTree = async (
|
export const createFileTree = async (
|
||||||
members: (FolderMember | ServiceMember)[],
|
members: (FolderMember | ServiceMember | FileMember)[],
|
||||||
parentFolders: string[] = []
|
parentFolders: string[] = []
|
||||||
) => {
|
) => {
|
||||||
const destinationPath = path.join(
|
const destinationPath = path.join(
|
||||||
@@ -13,29 +19,32 @@ export const createFileTree = async (
|
|||||||
path.join(...parentFolders)
|
path.join(...parentFolders)
|
||||||
)
|
)
|
||||||
|
|
||||||
await asyncForEach(members, async (member: FolderMember | ServiceMember) => {
|
await asyncForEach(
|
||||||
let name = member.name
|
members,
|
||||||
|
async (member: FolderMember | ServiceMember | FileMember) => {
|
||||||
|
let name = member.name
|
||||||
|
|
||||||
if (member.type === MemberType.service) name += '.sas'
|
if (member.type === MemberType.service) name += '.sas'
|
||||||
|
|
||||||
if (member.type === MemberType.folder) {
|
if (member.type === MemberType.folder) {
|
||||||
await createFolder(path.join(destinationPath, name)).catch((err) =>
|
await createFolder(path.join(destinationPath, name)).catch((err) =>
|
||||||
Promise.reject({ error: err, failedToCreate: name })
|
Promise.reject({ error: err, failedToCreate: name })
|
||||||
)
|
)
|
||||||
|
|
||||||
await createFileTree(member.members, [...parentFolders, name]).catch(
|
await createFileTree(member.members, [...parentFolders, name]).catch(
|
||||||
(err) => Promise.reject({ error: err, failedToCreate: name })
|
(err) => Promise.reject({ error: err, failedToCreate: name })
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
const encoding = member.type === MemberType.file ? 'base64' : undefined
|
const encoding = member.type === MemberType.file ? 'base64' : undefined
|
||||||
|
|
||||||
await createFile(
|
await createFile(
|
||||||
path.join(destinationPath, name),
|
path.join(destinationPath, name),
|
||||||
member.code,
|
member.code,
|
||||||
encoding
|
encoding
|
||||||
).catch((err) => Promise.reject({ error: err, failedToCreate: name }))
|
).catch((err) => Promise.reject({ error: err, failedToCreate: name }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,28 @@
|
|||||||
export interface FileTree {
|
export enum MemberType {
|
||||||
members: (FolderMember | ServiceMember)[]
|
service = 'service',
|
||||||
|
file = 'file',
|
||||||
|
folder = 'folder'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MemberType {
|
export interface ServiceMember {
|
||||||
folder = 'folder',
|
name: string
|
||||||
service = 'service',
|
type: MemberType.service
|
||||||
file = 'file'
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileMember {
|
||||||
|
name: string
|
||||||
|
type: MemberType.file
|
||||||
|
code: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FolderMember {
|
export interface FolderMember {
|
||||||
name: string
|
name: string
|
||||||
type: MemberType.folder
|
type: MemberType.folder
|
||||||
members: (FolderMember | ServiceMember)[]
|
members: (FolderMember | ServiceMember | FileMember)[]
|
||||||
}
|
}
|
||||||
|
export interface FileTree {
|
||||||
export interface ServiceMember {
|
members: (FolderMember | ServiceMember | FileMember)[]
|
||||||
name: string
|
|
||||||
type: MemberType.service | MemberType.file
|
|
||||||
code: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isFileTree = (arg: any): arg is FileTree =>
|
export const isFileTree = (arg: any): arg is FileTree =>
|
||||||
@@ -25,11 +30,25 @@ export const isFileTree = (arg: any): arg is FileTree =>
|
|||||||
arg.members &&
|
arg.members &&
|
||||||
Array.isArray(arg.members) &&
|
Array.isArray(arg.members) &&
|
||||||
arg.members.filter(
|
arg.members.filter(
|
||||||
(member: FolderMember | ServiceMember) =>
|
(member: ServiceMember | FileMember | FolderMember) =>
|
||||||
!isFolderMember(member) && !isServiceMember(member)
|
!isServiceMember(member, '-') &&
|
||||||
|
!isFileMember(member, '-') &&
|
||||||
|
!isFolderMember(member, '-')
|
||||||
).length === 0
|
).length === 0
|
||||||
|
|
||||||
const isFolderMember = (arg: any): arg is FolderMember =>
|
const isServiceMember = (arg: any, pre: string): arg is ServiceMember =>
|
||||||
|
arg &&
|
||||||
|
typeof arg.name === 'string' &&
|
||||||
|
arg.type === MemberType.service &&
|
||||||
|
typeof arg.code === 'string'
|
||||||
|
|
||||||
|
const isFileMember = (arg: any, pre: string): arg is ServiceMember =>
|
||||||
|
arg &&
|
||||||
|
typeof arg.name === 'string' &&
|
||||||
|
arg.type === MemberType.file &&
|
||||||
|
typeof arg.code === 'string'
|
||||||
|
|
||||||
|
const isFolderMember = (arg: any, pre: string): arg is FolderMember =>
|
||||||
arg &&
|
arg &&
|
||||||
typeof arg.name === 'string' &&
|
typeof arg.name === 'string' &&
|
||||||
arg.type === MemberType.folder &&
|
arg.type === MemberType.folder &&
|
||||||
@@ -37,21 +56,7 @@ const isFolderMember = (arg: any): arg is FolderMember =>
|
|||||||
Array.isArray(arg.members) &&
|
Array.isArray(arg.members) &&
|
||||||
arg.members.filter(
|
arg.members.filter(
|
||||||
(member: FolderMember | ServiceMember) =>
|
(member: FolderMember | ServiceMember) =>
|
||||||
!isFolderMember(member) &&
|
!isServiceMember(member, pre + '-') &&
|
||||||
!isServiceMember(member) &&
|
!isFileMember(member, pre + '-') &&
|
||||||
!isFileMember(member)
|
!isFolderMember(member, pre + '-')
|
||||||
).length === 0
|
).length === 0
|
||||||
|
|
||||||
const isServiceMember = (arg: any): arg is ServiceMember =>
|
|
||||||
arg &&
|
|
||||||
typeof arg.name === 'string' &&
|
|
||||||
arg.type === MemberType.service &&
|
|
||||||
arg.code &&
|
|
||||||
typeof arg.code === 'string'
|
|
||||||
|
|
||||||
const isFileMember = (arg: any): arg is ServiceMember =>
|
|
||||||
arg &&
|
|
||||||
typeof arg.name === 'string' &&
|
|
||||||
arg.type === MemberType.file &&
|
|
||||||
arg.code &&
|
|
||||||
typeof arg.code === 'string'
|
|
||||||
|
|||||||
32
api/src/utils/copySASjsCore.ts
Normal file
32
api/src/utils/copySASjsCore.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import {
|
||||||
|
asyncForEach,
|
||||||
|
createFile,
|
||||||
|
createFolder,
|
||||||
|
deleteFolder,
|
||||||
|
readFile
|
||||||
|
} from '@sasjs/utils'
|
||||||
|
|
||||||
|
import { getTmpMacrosPath, sasJSCoreMacros, sasJSCoreMacrosInfo } from '.'
|
||||||
|
|
||||||
|
export const copySASjsCore = async () => {
|
||||||
|
console.log('Copying Macros from container to drive(tmp).')
|
||||||
|
|
||||||
|
const macrosDrivePath = getTmpMacrosPath()
|
||||||
|
|
||||||
|
await deleteFolder(macrosDrivePath)
|
||||||
|
await createFolder(macrosDrivePath)
|
||||||
|
|
||||||
|
const macros = await readFile(sasJSCoreMacrosInfo)
|
||||||
|
|
||||||
|
await asyncForEach(macros.split('\n'), async (macroName) => {
|
||||||
|
const macroFileSourcePath = path.join(sasJSCoreMacros, macroName)
|
||||||
|
const macroContent = await readFile(macroFileSourcePath)
|
||||||
|
|
||||||
|
const macroFileDestPath = path.join(macrosDrivePath, macroName)
|
||||||
|
|
||||||
|
await createFile(macroFileDestPath, macroContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('Macros Drive Path:', macrosDrivePath)
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ export const sysInitCompiledPath = path.join(
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
|
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
|
||||||
|
export const sasJSCoreMacrosInfo = path.join(apiRoot, 'sasjscore', '.macrolist')
|
||||||
|
|
||||||
export const getWebBuildFolderPath = () =>
|
export const getWebBuildFolderPath = () =>
|
||||||
path.join(codebaseRoot, 'web', 'build')
|
path.join(codebaseRoot, 'web', 'build')
|
||||||
@@ -18,6 +19,8 @@ export const getTmpFolderPath = () => process.driveLoc
|
|||||||
export const getTmpAppStreamConfigPath = () =>
|
export const getTmpAppStreamConfigPath = () =>
|
||||||
path.join(getTmpFolderPath(), 'appStreamConfig.json')
|
path.join(getTmpFolderPath(), 'appStreamConfig.json')
|
||||||
|
|
||||||
|
export const getTmpMacrosPath = () => path.join(getTmpFolderPath(), 'sasjscore')
|
||||||
|
|
||||||
export const getTmpUploadsPath = () => path.join(getTmpFolderPath(), 'uploads')
|
export const getTmpUploadsPath = () => path.join(getTmpFolderPath(), 'uploads')
|
||||||
|
|
||||||
export const getTmpFilesFolderPath = () =>
|
export const getTmpFilesFolderPath = () =>
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
export * from './appStreamConfig'
|
export * from './appStreamConfig'
|
||||||
export * from './connectDB'
|
export * from './connectDB'
|
||||||
|
export * from './copySASjsCore'
|
||||||
export * from './extractHeaders'
|
export * from './extractHeaders'
|
||||||
export * from './file'
|
export * from './file'
|
||||||
export * from './generateAccessToken'
|
export * from './generateAccessToken'
|
||||||
export * from './generateAuthCode'
|
export * from './generateAuthCode'
|
||||||
export * from './generateRefreshToken'
|
export * from './generateRefreshToken'
|
||||||
export * from './isDebugOn'
|
|
||||||
export * from './getCertificates'
|
export * from './getCertificates'
|
||||||
export * from './getDesktopFields'
|
export * from './getDesktopFields'
|
||||||
|
export * from './isDebugOn'
|
||||||
export * from './parseLogToArray'
|
export * from './parseLogToArray'
|
||||||
export * from './removeTokensInDB'
|
export * from './removeTokensInDB'
|
||||||
export * from './saveTokensInDB'
|
export * from './saveTokensInDB'
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.0.39",
|
"version": "0.0.42",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.0.39",
|
"version": "0.0.42",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.1",
|
||||||
"standard-version": "^9.3.2"
|
"standard-version": "^9.3.2"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.0.39",
|
"version": "0.0.42",
|
||||||
"description": "NodeJS wrapper for calling the SAS binary executable",
|
"description": "NodeJS wrapper for calling the SAS binary executable",
|
||||||
"repository": "https://github.com/sasjs/server",
|
"repository": "https://github.com/sasjs/server",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -54,11 +54,11 @@ const Studio = () => {
|
|||||||
|
|
||||||
let weboutString: string
|
let weboutString: string
|
||||||
try {
|
try {
|
||||||
weboutString = res.data.webout
|
weboutString = res.data._webout
|
||||||
.split('>>weboutBEGIN<<')[1]
|
.split('>>weboutBEGIN<<')[1]
|
||||||
.split('>>weboutEND<<')[0]
|
.split('>>weboutEND<<')[0]
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
weboutString = res?.data?.webout ?? ''
|
weboutString = res?.data?._webout ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
let webout: string
|
let webout: string
|
||||||
@@ -70,6 +70,9 @@ const Studio = () => {
|
|||||||
|
|
||||||
setWebout(`<pre><code>${webout}</code></pre>`)
|
setWebout(`<pre><code>${webout}</code></pre>`)
|
||||||
setTab('2')
|
setTab('2')
|
||||||
|
|
||||||
|
// Scroll to bottom of log
|
||||||
|
window.scrollTo(0, document.body.scrollHeight)
|
||||||
})
|
})
|
||||||
.catch((err) => console.log(err))
|
.catch((err) => console.log(err))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user