mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
998ef213e9 | ||
|
|
f8b0f98678 | ||
| 9640f65264 | |||
| c574b42235 | |||
| 468d1a929d | |||
| 7cdffe30e3 | |||
| 3b1fcb937d | |||
| 3c987c61dd | |||
| 0a780697da | |||
| 83d819df53 | |||
|
|
95df2b21d6 | ||
|
|
accdf914f1 | ||
| 15bdd2d7f0 | |||
| 2ce947d216 | |||
| ce2114e3f6 | |||
| 6c7550286b | |||
| 2360e104bd | |||
| 420a61a5a6 | |||
| 04e0f9efe3 | |||
| 99172cd9ed | |||
| 57daad0c26 | |||
| cc1e4543fc | |||
| 03cb89d14f | |||
| 72140d73c2 | |||
| efcefd2a42 | |||
| 06d7c91fc3 | |||
| 7010a6a120 | |||
| fdcaba9d56 | |||
| 48688a6547 | |||
| 0ce94a553e | |||
| 941917e508 | |||
|
|
5706371ffd | ||
|
|
ce5218a227 | ||
|
|
8b62755f39 | ||
|
|
cb84c3ebbb | ||
|
|
526402fd73 | ||
| 177675bc89 | |||
| 721165ff12 | |||
| 08e0c61e0f | |||
|
|
1b234eb2b1 | ||
|
|
ef25eec11f | ||
| 3e53f70928 | |||
| 0f19384999 | |||
| 63dd6813c0 | |||
| 299512135d | |||
| 6c35412d2f | |||
| 27410bc32b | |||
| 849b2dd468 | |||
|
|
a1a182698e | ||
|
|
4be692b24b | ||
|
|
d2ddd8aaca | ||
|
|
3a45e8f525 | ||
|
|
c0e2f55a7b | ||
|
|
aa027414ed | ||
|
|
8c4c52b1a9 | ||
|
|
ff420434ae | ||
|
|
65e6de9663 | ||
| 30d7a65358 | |||
| 5e930f14d2 | |||
| 9bc68b1cdc |
68
CHANGELOG.md
68
CHANGELOG.md
@@ -1,3 +1,71 @@
|
|||||||
|
## [0.12.1](https://github.com/sasjs/server/compare/v0.12.0...v0.12.1) (2022-07-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **web:** disable launch icon button when file content is not saved ([c574b42](https://github.com/sasjs/server/commit/c574b4223591c4a6cd3ef5e146ce99cd8f7c9190))
|
||||||
|
* **web:** saveAs functionality fixed in studio page ([3c987c6](https://github.com/sasjs/server/commit/3c987c61ddc258f991e2bf38c1f16a0c4248d6ae))
|
||||||
|
* **web:** show original name as default name in rename file/folder modal ([9640f65](https://github.com/sasjs/server/commit/9640f6526496f3564664ccb1f834d0f659dcad4e))
|
||||||
|
* **web:** webout tab item fixed in studio page ([7cdffe3](https://github.com/sasjs/server/commit/7cdffe30e36e5cad0284f48ea97925958e12704c))
|
||||||
|
* **web:** when no file is selected save the editor content to local storage ([3b1fcb9](https://github.com/sasjs/server/commit/3b1fcb937d06d02ab99c9e8dbe307012d48a7a3a))
|
||||||
|
|
||||||
|
# [0.12.0](https://github.com/sasjs/server/compare/v0.11.5...v0.12.0) (2022-07-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fileTree api response to include an additional attribute isFolder ([0f19384](https://github.com/sasjs/server/commit/0f193849994f1ac8a071afa8f10af5b46f86663d))
|
||||||
|
* remove drive component ([06d7c91](https://github.com/sasjs/server/commit/06d7c91fc34620a954df1fd1c682eff370f79ca6))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add api end point for delete folder ([08e0c61](https://github.com/sasjs/server/commit/08e0c61e0fd7041d6cded6f4d71fbb410e5615ce))
|
||||||
|
* add sidebar(drive) to left of studio ([6c35412](https://github.com/sasjs/server/commit/6c35412d2f5180d4e49b12e616576d8b8dacb7d8))
|
||||||
|
* created api endpoint for adding empty folder in drive ([941917e](https://github.com/sasjs/server/commit/941917e508ece5009135f9dddf99775dd4002f78))
|
||||||
|
* implemented api for renaming file/folder ([fdcaba9](https://github.com/sasjs/server/commit/fdcaba9d56cddea5d56d7de5a172f1bb49be3db5))
|
||||||
|
* implemented delete file/folder functionality ([177675b](https://github.com/sasjs/server/commit/177675bc897416f7994dd849dc7bb11ba072efe9))
|
||||||
|
* implemented functionality for adding file/folder from sidebar context menu ([0ce94a5](https://github.com/sasjs/server/commit/0ce94a553e53bfcdbd6273b26b322095a080a341))
|
||||||
|
* implemented the functionality for renaming file/folder from context menu ([7010a6a](https://github.com/sasjs/server/commit/7010a6a1201720d0eb4093267a344fb828b90a2f))
|
||||||
|
* prevent user from leaving studio page when there are unsaved changes ([6c75502](https://github.com/sasjs/server/commit/6c7550286b5f505e9dfe8ca63c62fa1db1b60b2e))
|
||||||
|
* **web:** add difference view editor in studio ([420a61a](https://github.com/sasjs/server/commit/420a61a5a6b11dcb5eb0a652ea9cecea5c3bee5f))
|
||||||
|
|
||||||
|
## [0.11.5](https://github.com/sasjs/server/compare/v0.11.4...v0.11.5) (2022-07-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Revert "fix(security): missing cookie flags are added" ([ce5218a](https://github.com/sasjs/server/commit/ce5218a2278cc750f2b1032024685dc6cd72f796))
|
||||||
|
|
||||||
|
## [0.11.4](https://github.com/sasjs/server/compare/v0.11.3...v0.11.4) (2022-07-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **security:** missing cookie flags are added ([526402f](https://github.com/sasjs/server/commit/526402fd73407ee4fa2d31092111a7e6a1741487))
|
||||||
|
|
||||||
|
## [0.11.3](https://github.com/sasjs/server/compare/v0.11.2...v0.11.3) (2022-07-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* filePath fix in code.js file for windows ([2995121](https://github.com/sasjs/server/commit/299512135d77c2ac9e34853cf35aee6f2e1d4da4))
|
||||||
|
|
||||||
|
## [0.11.2](https://github.com/sasjs/server/compare/v0.11.1...v0.11.2) (2022-07-18)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* apply icon option only for sas.exe ([d2ddd8a](https://github.com/sasjs/server/commit/d2ddd8aacadfdd143026881f2c6ae8c6b277610a))
|
||||||
|
|
||||||
|
## [0.11.1](https://github.com/sasjs/server/compare/v0.11.0...v0.11.1) (2022-07-18)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bank operator ([aa02741](https://github.com/sasjs/server/commit/aa027414ed3ce51f1014ef36c4191e064b2e963d))
|
||||||
|
* ensuring nosplash option only applies for sas.exe ([65e6de9](https://github.com/sasjs/server/commit/65e6de966383fe49a919b1f901d77c7f1e402c9b)), closes [#229](https://github.com/sasjs/server/issues/229)
|
||||||
|
|
||||||
# [0.11.0](https://github.com/sasjs/server/compare/v0.10.0...v0.11.0) (2022-07-16)
|
# [0.11.0](https://github.com/sasjs/server/compare/v0.10.0...v0.11.0) (2022-07-16)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,7 @@ import {
|
|||||||
moveFile,
|
moveFile,
|
||||||
createFolder,
|
createFolder,
|
||||||
deleteFile as deleteFileOnSystem,
|
deleteFile as deleteFileOnSystem,
|
||||||
|
deleteFolder as deleteFolderOnSystem,
|
||||||
folderExists,
|
folderExists,
|
||||||
listFilesInFolder,
|
listFilesInFolder,
|
||||||
listSubFoldersInFolder,
|
listSubFoldersInFolder,
|
||||||
@@ -58,11 +59,32 @@ interface GetFileTreeResponse {
|
|||||||
tree: TreeNode
|
tree: TreeNode
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateFileResponse {
|
interface FileFolderResponse {
|
||||||
status: string
|
status: string
|
||||||
message?: string
|
message?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AddFolderPayload {
|
||||||
|
/**
|
||||||
|
* Location of folder
|
||||||
|
* @example "/Public/someFolder"
|
||||||
|
*/
|
||||||
|
folderPath: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RenamePayload {
|
||||||
|
/**
|
||||||
|
* Old path of file/folder
|
||||||
|
* @example "/Public/someFolder"
|
||||||
|
*/
|
||||||
|
oldPath: string
|
||||||
|
/**
|
||||||
|
* New path of file/folder
|
||||||
|
* @example "/Public/newFolder"
|
||||||
|
*/
|
||||||
|
newPath: string
|
||||||
|
}
|
||||||
|
|
||||||
const fileTreeExample = getTreeExample()
|
const fileTreeExample = getTreeExample()
|
||||||
|
|
||||||
const successDeployResponse: DeployResponse = {
|
const successDeployResponse: DeployResponse = {
|
||||||
@@ -143,7 +165,7 @@ export class DriveController {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @summary Delete file from SASjs Drive
|
* @summary Delete file from SASjs Drive
|
||||||
* @query _filePath Location of SAS program
|
* @query _filePath Location of file
|
||||||
* @example _filePath "/Public/somefolder/some.file"
|
* @example _filePath "/Public/somefolder/some.file"
|
||||||
*/
|
*/
|
||||||
@Delete('/file')
|
@Delete('/file')
|
||||||
@@ -151,20 +173,31 @@ export class DriveController {
|
|||||||
return deleteFile(_filePath)
|
return deleteFile(_filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @summary Delete folder from SASjs Drive
|
||||||
|
* @query _folderPath Location of folder
|
||||||
|
* @example _folderPath "/Public/somefolder/"
|
||||||
|
*/
|
||||||
|
@Delete('/folder')
|
||||||
|
public async deleteFolder(@Query() _folderPath: string) {
|
||||||
|
return deleteFolder(_folderPath)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It's optional to either provide `_filePath` in url as query parameter
|
* It's optional to either provide `_filePath` in url as query parameter
|
||||||
* Or provide `filePath` in body as form field.
|
* Or provide `filePath` in body as form field.
|
||||||
* But it's required to provide else API will respond with Bad Request.
|
* But it's required to provide else API will respond with Bad Request.
|
||||||
*
|
*
|
||||||
* @summary Create a file in SASjs Drive
|
* @summary Create a file in SASjs Drive
|
||||||
* @param _filePath Location of SAS program
|
* @param _filePath Location of file
|
||||||
* @example _filePath "/Public/somefolder/some.file.sas"
|
* @example _filePath "/Public/somefolder/some.file.sas"
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Example<UpdateFileResponse>({
|
@Example<FileFolderResponse>({
|
||||||
status: 'success'
|
status: 'success'
|
||||||
})
|
})
|
||||||
@Response<UpdateFileResponse>(403, 'File already exists', {
|
@Response<FileFolderResponse>(403, 'File already exists', {
|
||||||
status: 'failure',
|
status: 'failure',
|
||||||
message: 'File request failed.'
|
message: 'File request failed.'
|
||||||
})
|
})
|
||||||
@@ -173,10 +206,28 @@ export class DriveController {
|
|||||||
@UploadedFile() file: Express.Multer.File,
|
@UploadedFile() file: Express.Multer.File,
|
||||||
@Query() _filePath?: string,
|
@Query() _filePath?: string,
|
||||||
@FormField() filePath?: string
|
@FormField() filePath?: string
|
||||||
): Promise<UpdateFileResponse> {
|
): Promise<FileFolderResponse> {
|
||||||
return saveFile((_filePath ?? filePath)!, file)
|
return saveFile((_filePath ?? filePath)!, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Create an empty folder in SASjs Drive
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Example<FileFolderResponse>({
|
||||||
|
status: 'success'
|
||||||
|
})
|
||||||
|
@Response<FileFolderResponse>(409, 'Folder already exists', {
|
||||||
|
status: 'failure',
|
||||||
|
message: 'Add folder request failed.'
|
||||||
|
})
|
||||||
|
@Post('/folder')
|
||||||
|
public async addFolder(
|
||||||
|
@Body() body: AddFolderPayload
|
||||||
|
): Promise<FileFolderResponse> {
|
||||||
|
return addFolder(body.folderPath)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It's optional to either provide `_filePath` in url as query parameter
|
* It's optional to either provide `_filePath` in url as query parameter
|
||||||
* Or provide `filePath` in body as form field.
|
* Or provide `filePath` in body as form field.
|
||||||
@@ -187,10 +238,10 @@ export class DriveController {
|
|||||||
* @example _filePath "/Public/somefolder/some.file.sas"
|
* @example _filePath "/Public/somefolder/some.file.sas"
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Example<UpdateFileResponse>({
|
@Example<FileFolderResponse>({
|
||||||
status: 'success'
|
status: 'success'
|
||||||
})
|
})
|
||||||
@Response<UpdateFileResponse>(403, `File doesn't exist`, {
|
@Response<FileFolderResponse>(403, `File doesn't exist`, {
|
||||||
status: 'failure',
|
status: 'failure',
|
||||||
message: 'File request failed.'
|
message: 'File request failed.'
|
||||||
})
|
})
|
||||||
@@ -199,10 +250,28 @@ export class DriveController {
|
|||||||
@UploadedFile() file: Express.Multer.File,
|
@UploadedFile() file: Express.Multer.File,
|
||||||
@Query() _filePath?: string,
|
@Query() _filePath?: string,
|
||||||
@FormField() filePath?: string
|
@FormField() filePath?: string
|
||||||
): Promise<UpdateFileResponse> {
|
): Promise<FileFolderResponse> {
|
||||||
return updateFile((_filePath ?? filePath)!, file)
|
return updateFile((_filePath ?? filePath)!, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Renames a file/folder in SASjs Drive
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Example<FileFolderResponse>({
|
||||||
|
status: 'success'
|
||||||
|
})
|
||||||
|
@Response<FileFolderResponse>(409, 'Folder already exists', {
|
||||||
|
status: 'failure',
|
||||||
|
message: 'rename request failed.'
|
||||||
|
})
|
||||||
|
@Post('/rename')
|
||||||
|
public async rename(
|
||||||
|
@Body() body: RenamePayload
|
||||||
|
): Promise<FileFolderResponse> {
|
||||||
|
return rename(body.oldPath, body.newPath)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Fetch file tree within SASjs Drive.
|
* @summary Fetch file tree within SASjs Drive.
|
||||||
*
|
*
|
||||||
@@ -249,20 +318,26 @@ const getFile = async (req: express.Request, filePath: string) => {
|
|||||||
.join(getFilesFolder(), filePath)
|
.join(getFilesFolder(), filePath)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!filePathFull.includes(driveFilesPath)) {
|
if (!filePathFull.includes(driveFilesPath))
|
||||||
throw new Error('Cannot get file outside drive.')
|
throw {
|
||||||
}
|
code: 400,
|
||||||
|
status: 'Bad Request',
|
||||||
|
message: `Can't get file outside drive.`
|
||||||
|
}
|
||||||
|
|
||||||
if (!(await fileExists(filePathFull))) {
|
if (!(await fileExists(filePathFull)))
|
||||||
throw new Error("File doesn't exist.")
|
throw {
|
||||||
}
|
code: 404,
|
||||||
|
status: 'Not Found',
|
||||||
|
message: `File doesn't exist.`
|
||||||
|
}
|
||||||
|
|
||||||
const extension = path.extname(filePathFull).toLowerCase()
|
const extension = path.extname(filePathFull).toLowerCase()
|
||||||
if (extension === '.sas') {
|
if (extension === '.sas') {
|
||||||
req.res?.setHeader('Content-type', 'text/plain')
|
req.res?.setHeader('Content-type', 'text/plain')
|
||||||
}
|
}
|
||||||
|
|
||||||
req.res?.sendFile(path.resolve(filePathFull))
|
req.res?.sendFile(path.resolve(filePathFull), { dotfiles: 'allow' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFolder = async (folderPath?: string) => {
|
const getFolder = async (folderPath?: string) => {
|
||||||
@@ -273,17 +348,26 @@ const getFolder = async (folderPath?: string) => {
|
|||||||
.join(getFilesFolder(), folderPath)
|
.join(getFilesFolder(), folderPath)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!folderPathFull.includes(driveFilesPath)) {
|
if (!folderPathFull.includes(driveFilesPath))
|
||||||
throw new Error('Cannot get folder outside drive.')
|
throw {
|
||||||
}
|
code: 400,
|
||||||
|
status: 'Bad Request',
|
||||||
|
message: `Can't get folder outside drive.`
|
||||||
|
}
|
||||||
|
|
||||||
if (!(await folderExists(folderPathFull))) {
|
if (!(await folderExists(folderPathFull)))
|
||||||
throw new Error("Folder doesn't exist.")
|
throw {
|
||||||
}
|
code: 404,
|
||||||
|
status: 'Not Found',
|
||||||
|
message: `Folder doesn't exist.`
|
||||||
|
}
|
||||||
|
|
||||||
if (!(await isFolder(folderPathFull))) {
|
if (!(await isFolder(folderPathFull)))
|
||||||
throw new Error('Not a Folder.')
|
throw {
|
||||||
}
|
code: 400,
|
||||||
|
status: 'Bad Request',
|
||||||
|
message: 'Not a Folder.'
|
||||||
|
}
|
||||||
|
|
||||||
const files: string[] = await listFilesInFolder(folderPathFull)
|
const files: string[] = await listFilesInFolder(folderPathFull)
|
||||||
const folders: string[] = await listSubFoldersInFolder(folderPathFull)
|
const folders: string[] = await listSubFoldersInFolder(folderPathFull)
|
||||||
@@ -302,19 +386,51 @@ const deleteFile = async (filePath: string) => {
|
|||||||
.join(getFilesFolder(), filePath)
|
.join(getFilesFolder(), filePath)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!filePathFull.includes(driveFilesPath)) {
|
if (!filePathFull.includes(driveFilesPath))
|
||||||
throw new Error('Cannot delete file outside drive.')
|
throw {
|
||||||
}
|
code: 400,
|
||||||
|
status: 'Bad Request',
|
||||||
|
message: `Can't delete file outside drive.`
|
||||||
|
}
|
||||||
|
|
||||||
if (!(await fileExists(filePathFull))) {
|
if (!(await fileExists(filePathFull)))
|
||||||
throw new Error('File does not exist.')
|
throw {
|
||||||
}
|
code: 404,
|
||||||
|
status: 'Not Found',
|
||||||
|
message: `File doesn't exist.`
|
||||||
|
}
|
||||||
|
|
||||||
await deleteFileOnSystem(filePathFull)
|
await deleteFileOnSystem(filePathFull)
|
||||||
|
|
||||||
return { status: 'success' }
|
return { status: 'success' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteFolder = async (folderPath: string) => {
|
||||||
|
const driveFolderPath = getFilesFolder()
|
||||||
|
|
||||||
|
const folderPathFull = path
|
||||||
|
.join(getFilesFolder(), folderPath)
|
||||||
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
|
if (!folderPathFull.includes(driveFolderPath))
|
||||||
|
throw {
|
||||||
|
code: 400,
|
||||||
|
status: 'Bad Request',
|
||||||
|
message: `Can't delete folder outside drive.`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await folderExists(folderPathFull)))
|
||||||
|
throw {
|
||||||
|
code: 404,
|
||||||
|
status: 'Not Found',
|
||||||
|
message: `Folder doesn't exist.`
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteFolderOnSystem(folderPathFull)
|
||||||
|
|
||||||
|
return { status: 'success' }
|
||||||
|
}
|
||||||
|
|
||||||
const saveFile = async (
|
const saveFile = async (
|
||||||
filePath: string,
|
filePath: string,
|
||||||
multerFile: Express.Multer.File
|
multerFile: Express.Multer.File
|
||||||
@@ -325,13 +441,19 @@ const saveFile = async (
|
|||||||
.join(driveFilesPath, filePath)
|
.join(driveFilesPath, filePath)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!filePathFull.includes(driveFilesPath)) {
|
if (!filePathFull.includes(driveFilesPath))
|
||||||
throw new Error('Cannot put file outside drive.')
|
throw {
|
||||||
}
|
code: 400,
|
||||||
|
status: 'Bad Request',
|
||||||
|
message: `Can't put file outside drive.`
|
||||||
|
}
|
||||||
|
|
||||||
if (await fileExists(filePathFull)) {
|
if (await fileExists(filePathFull))
|
||||||
throw new Error('File already exists.')
|
throw {
|
||||||
}
|
code: 409,
|
||||||
|
status: 'Conflict',
|
||||||
|
message: 'File already exists.'
|
||||||
|
}
|
||||||
|
|
||||||
const folderPath = path.dirname(filePathFull)
|
const folderPath = path.dirname(filePathFull)
|
||||||
await createFolder(folderPath)
|
await createFolder(folderPath)
|
||||||
@@ -340,6 +462,88 @@ const saveFile = async (
|
|||||||
return { status: 'success' }
|
return { status: 'success' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addFolder = async (folderPath: string): Promise<FileFolderResponse> => {
|
||||||
|
const drivePath = getFilesFolder()
|
||||||
|
|
||||||
|
const folderPathFull = path
|
||||||
|
.join(drivePath, folderPath)
|
||||||
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
|
if (!folderPathFull.includes(drivePath))
|
||||||
|
throw {
|
||||||
|
code: 400,
|
||||||
|
status: 'Bad Request',
|
||||||
|
message: `Can't put folder outside drive.`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await folderExists(folderPathFull))
|
||||||
|
throw {
|
||||||
|
code: 409,
|
||||||
|
status: 'Conflict',
|
||||||
|
message: 'Folder already exists.'
|
||||||
|
}
|
||||||
|
|
||||||
|
await createFolder(folderPathFull)
|
||||||
|
|
||||||
|
return { status: 'success' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const rename = async (
|
||||||
|
oldPath: string,
|
||||||
|
newPath: string
|
||||||
|
): Promise<FileFolderResponse> => {
|
||||||
|
const drivePath = getFilesFolder()
|
||||||
|
|
||||||
|
const oldPathFull = path
|
||||||
|
.join(drivePath, oldPath)
|
||||||
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
|
const newPathFull = path
|
||||||
|
.join(drivePath, newPath)
|
||||||
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
|
if (!oldPathFull.includes(drivePath))
|
||||||
|
throw {
|
||||||
|
code: 400,
|
||||||
|
status: 'Bad Request',
|
||||||
|
message: `Old path can't be outside of drive.`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newPathFull.includes(drivePath))
|
||||||
|
throw {
|
||||||
|
code: 400,
|
||||||
|
status: 'Bad Request',
|
||||||
|
message: `New path can't be outside of drive.`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await isFolder(oldPathFull)) {
|
||||||
|
if (await folderExists(newPathFull))
|
||||||
|
throw {
|
||||||
|
code: 409,
|
||||||
|
status: 'Conflict',
|
||||||
|
message: 'Folder with new name already exists.'
|
||||||
|
}
|
||||||
|
else moveFile(oldPathFull, newPathFull)
|
||||||
|
|
||||||
|
return { status: 'success' }
|
||||||
|
} else if (await fileExists(oldPathFull)) {
|
||||||
|
if (await fileExists(newPathFull))
|
||||||
|
throw {
|
||||||
|
code: 409,
|
||||||
|
status: 'Conflict',
|
||||||
|
message: 'File with new name already exists.'
|
||||||
|
}
|
||||||
|
else moveFile(oldPathFull, newPathFull)
|
||||||
|
return { status: 'success' }
|
||||||
|
}
|
||||||
|
|
||||||
|
throw {
|
||||||
|
code: 404,
|
||||||
|
status: 'Not Found',
|
||||||
|
message: 'No file/folder found for provided path.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updateFile = async (
|
const updateFile = async (
|
||||||
filePath: string,
|
filePath: string,
|
||||||
multerFile: Express.Multer.File
|
multerFile: Express.Multer.File
|
||||||
@@ -350,13 +554,19 @@ const updateFile = async (
|
|||||||
.join(driveFilesPath, filePath)
|
.join(driveFilesPath, filePath)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!filePathFull.includes(driveFilesPath)) {
|
if (!filePathFull.includes(driveFilesPath))
|
||||||
throw new Error('Cannot modify file outside drive.')
|
throw {
|
||||||
}
|
code: 400,
|
||||||
|
status: 'Bad Request',
|
||||||
|
message: `Can't modify file outside drive.`
|
||||||
|
}
|
||||||
|
|
||||||
if (!(await fileExists(filePathFull))) {
|
if (!(await fileExists(filePathFull)))
|
||||||
throw new Error(`File doesn't exist.`)
|
throw {
|
||||||
}
|
code: 404,
|
||||||
|
status: 'Not Found',
|
||||||
|
message: `File doesn't exist.`
|
||||||
|
}
|
||||||
|
|
||||||
await moveFile(multerFile.path, filePathFull)
|
await moveFile(multerFile.path, filePathFull)
|
||||||
|
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ export class ExecutionController {
|
|||||||
name: 'files',
|
name: 'files',
|
||||||
relativePath: '',
|
relativePath: '',
|
||||||
absolutePath: getFilesFolder(),
|
absolutePath: getFilesFolder(),
|
||||||
|
isFolder: true,
|
||||||
children: []
|
children: []
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,15 +153,22 @@ export class ExecutionController {
|
|||||||
const currentNode = stack.pop()
|
const currentNode = stack.pop()
|
||||||
|
|
||||||
if (currentNode) {
|
if (currentNode) {
|
||||||
|
currentNode.isFolder = fs
|
||||||
|
.statSync(currentNode.absolutePath)
|
||||||
|
.isDirectory()
|
||||||
|
|
||||||
const children = fs.readdirSync(currentNode.absolutePath)
|
const children = fs.readdirSync(currentNode.absolutePath)
|
||||||
|
|
||||||
for (let child of children) {
|
for (let child of children) {
|
||||||
const absoluteChildPath = `${currentNode.absolutePath}/${child}`
|
const absoluteChildPath = path.join(currentNode.absolutePath, child)
|
||||||
|
// relative path will only be used in frontend component
|
||||||
|
// so, no need to convert '/' to platform specific separator
|
||||||
const relativeChildPath = `${currentNode.relativePath}/${child}`
|
const relativeChildPath = `${currentNode.relativePath}/${child}`
|
||||||
const childNode: TreeNode = {
|
const childNode: TreeNode = {
|
||||||
name: child,
|
name: child,
|
||||||
relativePath: relativeChildPath,
|
relativePath: relativeChildPath,
|
||||||
absolutePath: absoluteChildPath,
|
absolutePath: absoluteChildPath,
|
||||||
|
isFolder: false,
|
||||||
children: []
|
children: []
|
||||||
}
|
}
|
||||||
currentNode.children.push(childNode)
|
currentNode.children.push(childNode)
|
||||||
|
|||||||
@@ -101,8 +101,8 @@ ${autoExecContent}`
|
|||||||
session.path,
|
session.path,
|
||||||
'-AUTOEXEC',
|
'-AUTOEXEC',
|
||||||
autoExecPath,
|
autoExecPath,
|
||||||
isWindows() ? '-nosplash' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
|
||||||
isWindows() ? '-icon' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
|
||||||
isWindows() ? '-nologo' : ''
|
isWindows() ? '-nologo' : ''
|
||||||
])
|
])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ let _webout = '';
|
|||||||
const weboutPath = '${
|
const weboutPath = '${
|
||||||
isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath
|
isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath
|
||||||
}';
|
}';
|
||||||
const _sasjs_tokenfile = '${tokenFile}';
|
const _sasjs_tokenfile = '${
|
||||||
|
isWindows() ? tokenFile.replace(/\\/g, '\\\\') : tokenFile
|
||||||
|
}';
|
||||||
const _sasjs_username = '${preProgramVariables?.username}';
|
const _sasjs_username = '${preProgramVariables?.username}';
|
||||||
const _sasjs_userid = '${preProgramVariables?.userId}';
|
const _sasjs_userid = '${preProgramVariables?.userId}';
|
||||||
const _sasjs_displayname = '${preProgramVariables?.displayName}';
|
const _sasjs_displayname = '${preProgramVariables?.displayName}';
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ import {
|
|||||||
extractName,
|
extractName,
|
||||||
fileBodyValidation,
|
fileBodyValidation,
|
||||||
fileParamValidation,
|
fileParamValidation,
|
||||||
|
folderBodyValidation,
|
||||||
folderParamValidation,
|
folderParamValidation,
|
||||||
isZipFile
|
isZipFile,
|
||||||
|
renameBodyValidation
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
|
|
||||||
const controller = new DriveController()
|
const controller = new DriveController()
|
||||||
@@ -119,7 +121,11 @@ driveRouter.get('/file', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
await controller.getFile(req, query._filePath)
|
await controller.getFile(req, query._filePath)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
const statusCode = err.code
|
||||||
|
|
||||||
|
delete err.code
|
||||||
|
|
||||||
|
res.status(statusCode).send(err.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -132,7 +138,11 @@ driveRouter.get('/folder', async (req, res) => {
|
|||||||
const response = await controller.getFolder(query._folderPath)
|
const response = await controller.getFolder(query._folderPath)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
const statusCode = err.code
|
||||||
|
|
||||||
|
delete err.code
|
||||||
|
|
||||||
|
res.status(statusCode).send(err.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -145,7 +155,28 @@ driveRouter.delete('/file', async (req, res) => {
|
|||||||
const response = await controller.deleteFile(query._filePath)
|
const response = await controller.deleteFile(query._filePath)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
const statusCode = err.code
|
||||||
|
|
||||||
|
delete err.code
|
||||||
|
|
||||||
|
res.status(statusCode).send(err.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
driveRouter.delete('/folder', async (req, res) => {
|
||||||
|
const { error: errQ, value: query } = folderParamValidation(req.query, true)
|
||||||
|
|
||||||
|
if (errQ) return res.status(400).send(errQ.details[0].message)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await controller.deleteFolder(query._folderPath)
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
const statusCode = err.code
|
||||||
|
|
||||||
|
delete err.code
|
||||||
|
|
||||||
|
res.status(statusCode).send(err.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -172,11 +203,33 @@ driveRouter.post(
|
|||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
await deleteFile(req.file.path)
|
await deleteFile(req.file.path)
|
||||||
res.status(403).send(err.toString())
|
|
||||||
|
const statusCode = err.code
|
||||||
|
|
||||||
|
delete err.code
|
||||||
|
|
||||||
|
res.status(statusCode).send(err.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
driveRouter.post('/folder', async (req, res) => {
|
||||||
|
const { error, value: body } = folderBodyValidation(req.body)
|
||||||
|
|
||||||
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await controller.addFolder(body)
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
const statusCode = err.code
|
||||||
|
|
||||||
|
delete err.code
|
||||||
|
|
||||||
|
res.status(statusCode).send(err.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
driveRouter.patch(
|
driveRouter.patch(
|
||||||
'/file',
|
'/file',
|
||||||
(...arg) => multerSingle('file', arg),
|
(...arg) => multerSingle('file', arg),
|
||||||
@@ -200,11 +253,33 @@ driveRouter.patch(
|
|||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
await deleteFile(req.file.path)
|
await deleteFile(req.file.path)
|
||||||
res.status(403).send(err.toString())
|
|
||||||
|
const statusCode = err.code
|
||||||
|
|
||||||
|
delete err.code
|
||||||
|
|
||||||
|
res.status(statusCode).send(err.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
driveRouter.post('/rename', async (req, res) => {
|
||||||
|
const { error, value: body } = renameBodyValidation(req.body)
|
||||||
|
|
||||||
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await controller.rename(body)
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
const statusCode = err.code
|
||||||
|
|
||||||
|
delete err.code
|
||||||
|
|
||||||
|
res.status(statusCode).send(err.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
driveRouter.get('/fileTree', async (req, res) => {
|
driveRouter.get('/fileTree', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const response = await controller.getFileTree()
|
const response = await controller.getFileTree()
|
||||||
|
|||||||
@@ -89,6 +89,12 @@ describe('drive', () => {
|
|||||||
principalId: dbUser.id,
|
principalId: dbUser.id,
|
||||||
setting: PermissionSetting.grant
|
setting: PermissionSetting.grant
|
||||||
})
|
})
|
||||||
|
await permissionController.createPermission({
|
||||||
|
uri: '/SASjsApi/drive/rename',
|
||||||
|
principalType: PrincipalType.user,
|
||||||
|
principalId: dbUser.id,
|
||||||
|
setting: PermissionSetting.grant
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -543,29 +549,29 @@ describe('drive', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if folder is not present', async () => {
|
it('should respond with Not Found if folder is not present', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get(getFolderApi)
|
.get(getFolderApi)
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.query({ _folderPath: `/my/path/code-${generateTimestamp()}` })
|
.query({ _folderPath: `/my/path/code-${generateTimestamp()}` })
|
||||||
.expect(403)
|
.expect(404)
|
||||||
|
|
||||||
expect(res.text).toEqual(`Error: Folder doesn't exist.`)
|
expect(res.text).toEqual(`Folder doesn't exist.`)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if folderPath outside Drive', async () => {
|
it('should respond with Bad Request if folderPath outside Drive', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get(getFolderApi)
|
.get(getFolderApi)
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.query({ _folderPath: '/../path/code.sas' })
|
.query({ _folderPath: '/../path/code.sas' })
|
||||||
.expect(403)
|
.expect(400)
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: Cannot get folder outside drive.')
|
expect(res.text).toEqual(`Can't get folder outside drive.`)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if folderPath is of a file', async () => {
|
it('should respond with Bad Request if folderPath is of a file', async () => {
|
||||||
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
|
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
const filePath = '/my/path/code.sas'
|
const filePath = '/my/path/code.sas'
|
||||||
|
|
||||||
@@ -576,12 +582,96 @@ describe('drive', () => {
|
|||||||
.get(getFolderApi)
|
.get(getFolderApi)
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.query({ _folderPath: filePath })
|
.query({ _folderPath: filePath })
|
||||||
.expect(403)
|
.expect(400)
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: Not a Folder.')
|
expect(res.text).toEqual('Not a Folder.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('post', () => {
|
||||||
|
const folderApi = '/SASjsApi/drive/folder'
|
||||||
|
const pathToDrive = fileUtilModules.getFilesFolder()
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await deleteFolder(path.join(pathToDrive, 'post'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create a folder on drive', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post(folderApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ folderPath: '/post/folder' })
|
||||||
|
|
||||||
|
expect(res.statusCode).toEqual(200)
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
status: 'success'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Conflict if the folder already exists', async () => {
|
||||||
|
await createFolder(path.join(pathToDrive, '/post/folder'))
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post(folderApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ folderPath: '/post/folder' })
|
||||||
|
.expect(409)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`Folder already exists.`)
|
||||||
|
|
||||||
|
expect(res.statusCode).toEqual(409)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if the folderPath is outside drive', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post(folderApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ folderPath: '../sample' })
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`Can't put folder outside drive.`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('delete', () => {
|
||||||
|
const folderApi = '/SASjsApi/drive/folder'
|
||||||
|
const pathToDrive = fileUtilModules.getFilesFolder()
|
||||||
|
|
||||||
|
it('should delete a folder on drive', async () => {
|
||||||
|
await createFolder(path.join(pathToDrive, 'delete'))
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.delete(folderApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.query({ _folderPath: 'delete' })
|
||||||
|
|
||||||
|
expect(res.statusCode).toEqual(200)
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
status: 'success'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Not Found if the folder does not exists', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.delete(folderApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.query({ _folderPath: 'notExists' })
|
||||||
|
.expect(404)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`Folder doesn't exist.`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if the folderPath is outside drive', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.delete(folderApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.query({ _folderPath: '../outsideDrive' })
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`Can't delete folder outside drive.`)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('file', () => {
|
describe('file', () => {
|
||||||
@@ -627,7 +717,7 @@ describe('drive', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if file is already present', async () => {
|
it('should respond with Conflict if file is already present', async () => {
|
||||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
const pathToUpload = `/my/path/code-${generateTimestamp()}.sas`
|
const pathToUpload = `/my/path/code-${generateTimestamp()}.sas`
|
||||||
|
|
||||||
@@ -642,13 +732,13 @@ describe('drive', () => {
|
|||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.field('filePath', pathToUpload)
|
.field('filePath', pathToUpload)
|
||||||
.attach('file', fileToAttachPath)
|
.attach('file', fileToAttachPath)
|
||||||
.expect(403)
|
.expect(409)
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: File already exists.')
|
expect(res.text).toEqual('File already exists.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if filePath outside Drive', async () => {
|
it('should respond with Bad Request if filePath outside Drive', async () => {
|
||||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
const pathToUpload = '/../path/code.sas'
|
const pathToUpload = '/../path/code.sas'
|
||||||
|
|
||||||
@@ -657,9 +747,9 @@ describe('drive', () => {
|
|||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.field('filePath', pathToUpload)
|
.field('filePath', pathToUpload)
|
||||||
.attach('file', fileToAttachPath)
|
.attach('file', fileToAttachPath)
|
||||||
.expect(403)
|
.expect(400)
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: Cannot put file outside drive.')
|
expect(res.text).toEqual(`Can't put file outside drive.`)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -794,19 +884,19 @@ describe('drive', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if file is not present', async () => {
|
it('should respond with Not Found if file is not present', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.patch('/SASjsApi/drive/file')
|
.patch('/SASjsApi/drive/file')
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.field('filePath', `/my/path/code-3.sas`)
|
.field('filePath', `/my/path/code-3.sas`)
|
||||||
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||||
.expect(403)
|
.expect(404)
|
||||||
|
|
||||||
expect(res.text).toEqual(`Error: File doesn't exist.`)
|
expect(res.text).toEqual(`File doesn't exist.`)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if filePath outside Drive', async () => {
|
it('should respond with Bad Request if filePath outside Drive', async () => {
|
||||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
const pathToUpload = '/../path/code.sas'
|
const pathToUpload = '/../path/code.sas'
|
||||||
|
|
||||||
@@ -815,9 +905,9 @@ describe('drive', () => {
|
|||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.field('filePath', pathToUpload)
|
.field('filePath', pathToUpload)
|
||||||
.attach('file', fileToAttachPath)
|
.attach('file', fileToAttachPath)
|
||||||
.expect(403)
|
.expect(400)
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: Cannot modify file outside drive.')
|
expect(res.text).toEqual(`Can't modify file outside drive.`)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -922,25 +1012,25 @@ describe('drive', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if file is not present', async () => {
|
it('should respond with Not Found if file is not present', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get('/SASjsApi/drive/file')
|
.get('/SASjsApi/drive/file')
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.query({ _filePath: `/my/path/code-4.sas` })
|
.query({ _filePath: `/my/path/code-4.sas` })
|
||||||
.expect(403)
|
.expect(404)
|
||||||
|
|
||||||
expect(res.text).toEqual(`Error: File doesn't exist.`)
|
expect(res.text).toEqual(`File doesn't exist.`)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if filePath outside Drive', async () => {
|
it('should respond with Bad Request if filePath outside Drive', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get('/SASjsApi/drive/file')
|
.get('/SASjsApi/drive/file')
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.query({ _filePath: '/../path/code.sas' })
|
.query({ _filePath: '/../path/code.sas' })
|
||||||
.expect(403)
|
.expect(400)
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: Cannot get file outside drive.')
|
expect(res.text).toEqual(`Can't get file outside drive.`)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -966,6 +1056,139 @@ describe('drive', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('rename', () => {
|
||||||
|
const renameApi = '/SASjsApi/drive/rename'
|
||||||
|
const pathToDrive = fileUtilModules.getFilesFolder()
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await deleteFolder(path.join(pathToDrive, 'rename'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should rename a folder', async () => {
|
||||||
|
await createFolder(path.join(pathToDrive, 'rename', 'folder'))
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post(renameApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ oldPath: '/rename/folder', newPath: '/rename/renamed' })
|
||||||
|
|
||||||
|
expect(res.statusCode).toEqual(200)
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
status: 'success'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should rename a file', async () => {
|
||||||
|
await createFile(
|
||||||
|
path.join(pathToDrive, 'rename', 'file.txt'),
|
||||||
|
'some file content'
|
||||||
|
)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post(renameApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({
|
||||||
|
oldPath: '/rename/file.txt',
|
||||||
|
newPath: '/rename/renamed.txt'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(res.statusCode).toEqual(200)
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
status: 'success'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if the oldPath is missing', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post(renameApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ newPath: 'newPath' })
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`\"oldPath\" is required`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if the newPath is missing', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post(renameApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ oldPath: 'oldPath' })
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`\"newPath\" is required`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if the oldPath is outside drive', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post(renameApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ oldPath: '../outside', newPath: 'renamed' })
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`Old path can't be outside of drive.`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if the newPath is outside drive', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post(renameApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ oldPath: 'older', newPath: '../outside' })
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`New path can't be outside of drive.`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Not Found if the folder does not exist', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post(renameApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ oldPath: '/rename/not exists', newPath: '/rename/renamed' })
|
||||||
|
.expect(404)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('No file/folder found for provided path.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Conflict if the folder already exists', async () => {
|
||||||
|
await createFolder(path.join(pathToDrive, 'rename', 'folder'))
|
||||||
|
await createFolder(path.join(pathToDrive, 'rename', 'exists'))
|
||||||
|
const res = await request(app)
|
||||||
|
.post(renameApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ oldPath: '/rename/folder', newPath: '/rename/exists' })
|
||||||
|
.expect(409)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Folder with new name already exists.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Not Found if the file does not exist', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post(renameApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ oldPath: '/rename/file.txt', newPath: '/rename/renamed.txt' })
|
||||||
|
.expect(404)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('No file/folder found for provided path.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Conflict if the file already exists', async () => {
|
||||||
|
await createFile(
|
||||||
|
path.join(pathToDrive, 'rename', 'file.txt'),
|
||||||
|
'some file content'
|
||||||
|
)
|
||||||
|
await createFile(
|
||||||
|
path.join(pathToDrive, 'rename', 'exists.txt'),
|
||||||
|
'some existing content'
|
||||||
|
)
|
||||||
|
const res = await request(app)
|
||||||
|
.post(renameApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ oldPath: '/rename/file.txt', newPath: '/rename/exists.txt' })
|
||||||
|
.expect(409)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('File with new name already exists.')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const getExampleService = (): ServiceMember =>
|
const getExampleService = (): ServiceMember =>
|
||||||
|
|||||||
@@ -2,5 +2,6 @@ export interface TreeNode {
|
|||||||
name: string
|
name: string
|
||||||
relativePath: string
|
relativePath: string
|
||||||
absolutePath: string
|
absolutePath: string
|
||||||
|
isFolder: boolean
|
||||||
children: Array<TreeNode>
|
children: Array<TreeNode>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const StaticAuthorizedRoutes = [
|
|||||||
'/SASjsApi/drive/file',
|
'/SASjsApi/drive/file',
|
||||||
'/SASjsApi/drive/folder',
|
'/SASjsApi/drive/folder',
|
||||||
'/SASjsApi/drive/fileTree',
|
'/SASjsApi/drive/fileTree',
|
||||||
|
'/SASjsApi/drive/rename',
|
||||||
'/SASjsApi/permission'
|
'/SASjsApi/permission'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { MulterFile } from '../types/Upload'
|
import { MulterFile } from '../types/Upload'
|
||||||
import { listFilesInFolder, readFileBinary } from '@sasjs/utils'
|
import { listFilesInFolder, readFileBinary, isWindows } from '@sasjs/utils'
|
||||||
|
|
||||||
interface FilenameMapSingle {
|
interface FilenameMapSingle {
|
||||||
fieldName: string
|
fieldName: string
|
||||||
@@ -118,7 +118,9 @@ export const generateFileUploadJSCode = async (
|
|||||||
if (fileName.includes('req_file')) {
|
if (fileName.includes('req_file')) {
|
||||||
fileCount++
|
fileCount++
|
||||||
const filePath = path.join(sessionFolder, fileName)
|
const filePath = path.join(sessionFolder, fileName)
|
||||||
uploadCode += `\nconst _WEBIN_FILEREF${fileCount} = fs.readFileSync('${filePath}')`
|
uploadCode += `\nconst _WEBIN_FILEREF${fileCount} = fs.readFileSync('${
|
||||||
|
isWindows() ? filePath.replace(/\\/g, '\\\\') : filePath
|
||||||
|
}')`
|
||||||
uploadCode += `\nconst _WEBIN_FILENAME${fileCount} = '${filesNamesMap[fileName].originalName}'`
|
uploadCode += `\nconst _WEBIN_FILENAME${fileCount} = '${filesNamesMap[fileName].originalName}'`
|
||||||
uploadCode += `\nconst _WEBIN_NAME${fileCount} = '${filesNamesMap[fileName].fieldName}'`
|
uploadCode += `\nconst _WEBIN_NAME${fileCount} = '${filesNamesMap[fileName].fieldName}'`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,9 +138,23 @@ export const fileParamValidation = (data: any): Joi.ValidationResult =>
|
|||||||
_filePath: filePathSchema
|
_filePath: filePathSchema
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
export const folderParamValidation = (data: any): Joi.ValidationResult =>
|
export const folderParamValidation = (
|
||||||
|
data: any,
|
||||||
|
folderPathRequired?: boolean
|
||||||
|
): Joi.ValidationResult =>
|
||||||
Joi.object({
|
Joi.object({
|
||||||
_folderPath: Joi.string()
|
_folderPath: folderPathRequired ? Joi.string().required() : Joi.string()
|
||||||
|
}).validate(data)
|
||||||
|
|
||||||
|
export const folderBodyValidation = (data: any): Joi.ValidationResult =>
|
||||||
|
Joi.object({
|
||||||
|
folderPath: Joi.string().required()
|
||||||
|
}).validate(data)
|
||||||
|
|
||||||
|
export const renameBodyValidation = (data: any): Joi.ValidationResult =>
|
||||||
|
Joi.object({
|
||||||
|
oldPath: Joi.string().required(),
|
||||||
|
newPath: Joi.string().required()
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
export const runCodeValidation = (data: any): Joi.ValidationResult =>
|
export const runCodeValidation = (data: any): Joi.ValidationResult =>
|
||||||
|
|||||||
@@ -12,28 +12,16 @@
|
|||||||
},
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
"name": "Info",
|
"name": "Auth",
|
||||||
"description": "Get Server Information"
|
"description": "Operations about auth"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Session",
|
|
||||||
"description": "Get Session information"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "User",
|
|
||||||
"description": "Operations with users"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Permission",
|
|
||||||
"description": "Operations about permissions"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Client",
|
"name": "Client",
|
||||||
"description": "Operations about clients"
|
"description": "Operations about clients"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Auth",
|
"name": "CODE",
|
||||||
"description": "Operations about auth"
|
"description": "Execution of code (various runtimes are supported)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Drive",
|
"name": "Drive",
|
||||||
@@ -43,13 +31,25 @@
|
|||||||
"name": "Group",
|
"name": "Group",
|
||||||
"description": "Operations on groups and group memberships"
|
"description": "Operations on groups and group memberships"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Info",
|
||||||
|
"description": "Get Server Information"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Permission",
|
||||||
|
"description": "Operations about permissions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Session",
|
||||||
|
"description": "Get Session information"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "STP",
|
"name": "STP",
|
||||||
"description": "Execution of Stored Programs"
|
"description": "Execution of Stored Programs"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "CODE",
|
"name": "User",
|
||||||
"description": "Execution of code (various runtimes are supported)"
|
"description": "Operations with users"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Web",
|
"name": "Web",
|
||||||
|
|||||||
241
web/package-lock.json
generated
241
web/package-lock.json
generated
@@ -10,7 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.4.1",
|
"@emotion/react": "^11.4.1",
|
||||||
"@emotion/styled": "^11.3.0",
|
"@emotion/styled": "^11.3.0",
|
||||||
"@mui/icons-material": "^5.0.3",
|
"@mui/icons-material": "^5.8.4",
|
||||||
"@mui/lab": "^5.0.0-alpha.50",
|
"@mui/lab": "^5.0.0-alpha.50",
|
||||||
"@mui/material": "^5.0.3",
|
"@mui/material": "^5.0.3",
|
||||||
"@mui/styles": "^5.0.1",
|
"@mui/styles": "^5.0.1",
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
"react-copy-to-clipboard": "^5.1.0",
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-monaco-editor": "^0.48.0",
|
"react-monaco-editor": "^0.48.0",
|
||||||
"react-router-dom": "^5.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
"react-toastify": "^9.0.1"
|
"react-toastify": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1836,9 +1836,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/runtime": {
|
"node_modules/@babel/runtime": {
|
||||||
"version": "7.16.3",
|
"version": "7.18.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.6.tgz",
|
||||||
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
|
"integrity": "sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"regenerator-runtime": "^0.13.4"
|
"regenerator-runtime": "^0.13.4"
|
||||||
},
|
},
|
||||||
@@ -2312,19 +2312,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/icons-material": {
|
"node_modules/@mui/icons-material": {
|
||||||
"version": "5.1.0",
|
"version": "5.8.4",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.8.4.tgz",
|
||||||
"integrity": "sha512-GD2cNZ2XTqoxX6DMUg+tos1fDUVg6kXWxwo9UuBiRIhK8N+B7CG7vjRDf28LLmewcqIjxqy+T2SEVqDLy1FOYQ==",
|
"integrity": "sha512-9Z/vyj2szvEhGWDvb+gG875bOGm8b8rlHBKOD1+nA3PcgC3fV6W1AU6pfOorPeBfH2X4mb9Boe97vHvaSndQvA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.16.0"
|
"@babel/runtime": "^7.17.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
},
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/mui"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@mui/material": "^5.0.0",
|
"@mui/material": "^5.0.0",
|
||||||
"@types/react": "^16.8.6 || ^17.0.0",
|
"@types/react": "^17.0.0 || ^18.0.0",
|
||||||
"react": "^17.0.2"
|
"react": "^17.0.0 || ^18.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@types/react": {
|
"@types/react": {
|
||||||
@@ -7128,16 +7132,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/history": {
|
"node_modules/history": {
|
||||||
"version": "4.10.1",
|
"version": "5.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
|
||||||
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
|
"integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.1.2",
|
"@babel/runtime": "^7.7.6"
|
||||||
"loose-envify": "^1.2.0",
|
|
||||||
"resolve-pathname": "^3.0.0",
|
|
||||||
"tiny-invariant": "^1.0.2",
|
|
||||||
"tiny-warning": "^1.0.0",
|
|
||||||
"value-equal": "^1.0.1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hoist-non-react-statics": {
|
"node_modules/hoist-non-react-statics": {
|
||||||
@@ -7829,11 +7828,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/isarray": {
|
|
||||||
"version": "0.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
|
||||||
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
|
|
||||||
},
|
|
||||||
"node_modules/isexe": {
|
"node_modules/isexe": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
@@ -8392,19 +8386,6 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mini-create-react-context": {
|
|
||||||
"version": "0.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
|
|
||||||
"integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.12.1",
|
|
||||||
"tiny-warning": "^1.0.3"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"prop-types": "^15.0.0",
|
|
||||||
"react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/minimalistic-assert": {
|
"node_modules/minimalistic-assert": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||||
@@ -8967,14 +8948,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
|
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
|
||||||
},
|
},
|
||||||
"node_modules/path-to-regexp": {
|
|
||||||
"version": "1.8.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
|
|
||||||
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
|
|
||||||
"dependencies": {
|
|
||||||
"isarray": "0.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/path-type": {
|
"node_modules/path-type": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||||
@@ -9362,47 +9335,29 @@
|
|||||||
"react": "^17.x"
|
"react": "^17.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router": {
|
|
||||||
"version": "5.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz",
|
|
||||||
"integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.12.13",
|
|
||||||
"history": "^4.9.0",
|
|
||||||
"hoist-non-react-statics": "^3.1.0",
|
|
||||||
"loose-envify": "^1.3.1",
|
|
||||||
"mini-create-react-context": "^0.4.0",
|
|
||||||
"path-to-regexp": "^1.7.0",
|
|
||||||
"prop-types": "^15.6.2",
|
|
||||||
"react-is": "^16.6.0",
|
|
||||||
"tiny-invariant": "^1.0.2",
|
|
||||||
"tiny-warning": "^1.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=15"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-router-dom": {
|
"node_modules/react-router-dom": {
|
||||||
"version": "5.3.0",
|
"version": "6.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
|
||||||
"integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==",
|
"integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.13",
|
"history": "^5.2.0",
|
||||||
"history": "^4.9.0",
|
"react-router": "6.3.0"
|
||||||
"loose-envify": "^1.3.1",
|
|
||||||
"prop-types": "^15.6.2",
|
|
||||||
"react-router": "5.2.1",
|
|
||||||
"tiny-invariant": "^1.0.2",
|
|
||||||
"tiny-warning": "^1.0.0"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=15"
|
"react": ">=16.8",
|
||||||
|
"react-dom": ">=16.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router/node_modules/react-is": {
|
"node_modules/react-router-dom/node_modules/react-router": {
|
||||||
"version": "16.13.1",
|
"version": "6.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
"integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"history": "^5.2.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-toastify": {
|
"node_modules/react-toastify": {
|
||||||
"version": "9.0.1",
|
"version": "9.0.1",
|
||||||
@@ -9679,11 +9634,6 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/resolve-pathname": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
|
|
||||||
},
|
|
||||||
"node_modules/retry": {
|
"node_modules/retry": {
|
||||||
"version": "0.13.1",
|
"version": "0.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
||||||
@@ -10349,11 +10299,6 @@
|
|||||||
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
|
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/tiny-invariant": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg=="
|
|
||||||
},
|
|
||||||
"node_modules/tiny-warning": {
|
"node_modules/tiny-warning": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||||
@@ -10733,11 +10678,6 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/value-equal": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
|
|
||||||
},
|
|
||||||
"node_modules/vary": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
@@ -12642,9 +12582,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/runtime": {
|
"@babel/runtime": {
|
||||||
"version": "7.16.3",
|
"version": "7.18.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.6.tgz",
|
||||||
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
|
"integrity": "sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"regenerator-runtime": "^0.13.4"
|
"regenerator-runtime": "^0.13.4"
|
||||||
}
|
}
|
||||||
@@ -12989,11 +12929,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@mui/icons-material": {
|
"@mui/icons-material": {
|
||||||
"version": "5.1.0",
|
"version": "5.8.4",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.8.4.tgz",
|
||||||
"integrity": "sha512-GD2cNZ2XTqoxX6DMUg+tos1fDUVg6kXWxwo9UuBiRIhK8N+B7CG7vjRDf28LLmewcqIjxqy+T2SEVqDLy1FOYQ==",
|
"integrity": "sha512-9Z/vyj2szvEhGWDvb+gG875bOGm8b8rlHBKOD1+nA3PcgC3fV6W1AU6pfOorPeBfH2X4mb9Boe97vHvaSndQvA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.16.0"
|
"@babel/runtime": "^7.17.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@mui/lab": {
|
"@mui/lab": {
|
||||||
@@ -16587,16 +16527,11 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"history": {
|
"history": {
|
||||||
"version": "4.10.1",
|
"version": "5.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
|
||||||
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
|
"integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.1.2",
|
"@babel/runtime": "^7.7.6"
|
||||||
"loose-envify": "^1.2.0",
|
|
||||||
"resolve-pathname": "^3.0.0",
|
|
||||||
"tiny-invariant": "^1.0.2",
|
|
||||||
"tiny-warning": "^1.0.0",
|
|
||||||
"value-equal": "^1.0.1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hoist-non-react-statics": {
|
"hoist-non-react-statics": {
|
||||||
@@ -17084,11 +17019,6 @@
|
|||||||
"is-docker": "^2.0.0"
|
"is-docker": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"isarray": {
|
|
||||||
"version": "0.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
|
||||||
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
|
|
||||||
},
|
|
||||||
"isexe": {
|
"isexe": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
@@ -17530,15 +17460,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||||
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="
|
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="
|
||||||
},
|
},
|
||||||
"mini-create-react-context": {
|
|
||||||
"version": "0.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
|
|
||||||
"integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
|
|
||||||
"requires": {
|
|
||||||
"@babel/runtime": "^7.12.1",
|
|
||||||
"tiny-warning": "^1.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"minimalistic-assert": {
|
"minimalistic-assert": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||||
@@ -17961,14 +17882,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
|
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
|
||||||
},
|
},
|
||||||
"path-to-regexp": {
|
|
||||||
"version": "1.8.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
|
|
||||||
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
|
|
||||||
"requires": {
|
|
||||||
"isarray": "0.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"path-type": {
|
"path-type": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||||
@@ -18260,44 +18173,25 @@
|
|||||||
"prop-types": "^15.8.1"
|
"prop-types": "^15.8.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-router": {
|
"react-router-dom": {
|
||||||
"version": "5.2.1",
|
"version": "6.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
|
||||||
"integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==",
|
"integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.12.13",
|
"history": "^5.2.0",
|
||||||
"history": "^4.9.0",
|
"react-router": "6.3.0"
|
||||||
"hoist-non-react-statics": "^3.1.0",
|
|
||||||
"loose-envify": "^1.3.1",
|
|
||||||
"mini-create-react-context": "^0.4.0",
|
|
||||||
"path-to-regexp": "^1.7.0",
|
|
||||||
"prop-types": "^15.6.2",
|
|
||||||
"react-is": "^16.6.0",
|
|
||||||
"tiny-invariant": "^1.0.2",
|
|
||||||
"tiny-warning": "^1.0.0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-is": {
|
"react-router": {
|
||||||
"version": "16.13.1",
|
"version": "6.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
"integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
|
||||||
|
"requires": {
|
||||||
|
"history": "^5.2.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-router-dom": {
|
|
||||||
"version": "5.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz",
|
|
||||||
"integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==",
|
|
||||||
"requires": {
|
|
||||||
"@babel/runtime": "^7.12.13",
|
|
||||||
"history": "^4.9.0",
|
|
||||||
"loose-envify": "^1.3.1",
|
|
||||||
"prop-types": "^15.6.2",
|
|
||||||
"react-router": "5.2.1",
|
|
||||||
"tiny-invariant": "^1.0.2",
|
|
||||||
"tiny-warning": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"react-toastify": {
|
"react-toastify": {
|
||||||
"version": "9.0.1",
|
"version": "9.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.0.1.tgz",
|
||||||
@@ -18520,11 +18414,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||||
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
|
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
|
||||||
},
|
},
|
||||||
"resolve-pathname": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
|
|
||||||
},
|
|
||||||
"retry": {
|
"retry": {
|
||||||
"version": "0.13.1",
|
"version": "0.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
||||||
@@ -19026,11 +18915,6 @@
|
|||||||
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
|
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"tiny-invariant": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg=="
|
|
||||||
},
|
|
||||||
"tiny-warning": {
|
"tiny-warning": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||||
@@ -19320,11 +19204,6 @@
|
|||||||
"homedir-polyfill": "^1.0.1"
|
"homedir-polyfill": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"value-equal": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
|
|
||||||
},
|
|
||||||
"vary": {
|
"vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.4.1",
|
"@emotion/react": "^11.4.1",
|
||||||
"@emotion/styled": "^11.3.0",
|
"@emotion/styled": "^11.3.0",
|
||||||
"@mui/icons-material": "^5.0.3",
|
"@mui/icons-material": "^5.8.4",
|
||||||
"@mui/lab": "^5.0.0-alpha.50",
|
"@mui/lab": "^5.0.0-alpha.50",
|
||||||
"@mui/material": "^5.0.3",
|
"@mui/material": "^5.0.3",
|
||||||
"@mui/styles": "^5.0.1",
|
"@mui/styles": "^5.0.1",
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
"react-copy-to-clipboard": "^5.1.0",
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-monaco-editor": "^0.48.0",
|
"react-monaco-editor": "^0.48.0",
|
||||||
"react-router-dom": "^5.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
"react-toastify": "^9.0.1"
|
"react-toastify": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import React, { useContext } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { Route, HashRouter, Switch } from 'react-router-dom'
|
import { Route, HashRouter, Routes } from 'react-router-dom'
|
||||||
import { ThemeProvider } from '@mui/material/styles'
|
import { ThemeProvider } from '@mui/material/styles'
|
||||||
import { theme } from './theme'
|
import { theme } from './theme'
|
||||||
|
|
||||||
import Login from './components/login'
|
import Login from './components/login'
|
||||||
import Header from './components/header'
|
import Header from './components/header'
|
||||||
import Home from './components/home'
|
import Home from './components/home'
|
||||||
import Drive from './containers/Drive'
|
|
||||||
import Studio from './containers/Studio'
|
import Studio from './containers/Studio'
|
||||||
import Settings from './containers/Settings'
|
import Settings from './containers/Settings'
|
||||||
|
|
||||||
@@ -22,11 +21,9 @@ function App() {
|
|||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<Header />
|
<Header />
|
||||||
<Switch>
|
<Routes>
|
||||||
<Route path="/">
|
<Route path="/" element={<Login />} />
|
||||||
<Login />
|
</Routes>
|
||||||
</Route>
|
|
||||||
</Switch>
|
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
)
|
)
|
||||||
@@ -36,23 +33,12 @@ function App() {
|
|||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<Header />
|
<Header />
|
||||||
<Switch>
|
<Routes>
|
||||||
<Route exact path="/">
|
<Route path="/" element={<Home />} />
|
||||||
<Home />
|
<Route path="/SASjsStudio" element={<Studio />} />
|
||||||
</Route>
|
<Route path="/SASjsSettings" element={<Settings />} />
|
||||||
<Route exact path="/SASjsDrive">
|
<Route path="/SASjsLogon" element={<AuthCode />} />
|
||||||
<Drive />
|
</Routes>
|
||||||
</Route>
|
|
||||||
<Route exact path="/SASjsStudio">
|
|
||||||
<Studio />
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/SASjsSettings">
|
|
||||||
<Settings />
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/SASjsLogon">
|
|
||||||
<AuthCode />
|
|
||||||
</Route>
|
|
||||||
</Switch>
|
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
@@ -18,22 +18,27 @@ const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
type DeleteModalProps = {
|
type DeleteConfirmationModalProps = {
|
||||||
open: boolean
|
open: boolean
|
||||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
deletePermission: () => void
|
message: string
|
||||||
|
_delete: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const DeleteModal = ({ open, setOpen, deletePermission }: DeleteModalProps) => {
|
const DeleteConfirmationModal = ({
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
message,
|
||||||
|
_delete
|
||||||
|
}: DeleteConfirmationModalProps) => {
|
||||||
return (
|
return (
|
||||||
<BootstrapDialog onClose={() => setOpen(false)} open={open}>
|
<BootstrapDialog onClose={() => setOpen(false)} open={open}>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Typography gutterBottom>
|
<Typography gutterBottom>{message}</Typography>
|
||||||
Are you sure you want to delete this permission?
|
|
||||||
</Typography>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button color="error" onClick={() => deletePermission()}>
|
<Button onClick={() => setOpen(false)}>Cancel</Button>
|
||||||
|
<Button color="error" onClick={() => _delete()}>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
@@ -41,4 +46,4 @@ const DeleteModal = ({ open, setOpen, deletePermission }: DeleteModalProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DeleteModal
|
export default DeleteConfirmationModal
|
||||||
76
web/src/components/filePathInputModal.tsx
Normal file
76
web/src/components/filePathInputModal.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
import { Button, DialogActions, DialogContent, TextField } from '@mui/material'
|
||||||
|
|
||||||
|
import { BootstrapDialogTitle } from './dialogTitle'
|
||||||
|
import { BootstrapDialog } from './modal'
|
||||||
|
|
||||||
|
type FilePathInputModalProps = {
|
||||||
|
open: boolean
|
||||||
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
saveFile: (filePath: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const FilePathInputModal = ({
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
saveFile
|
||||||
|
}: FilePathInputModalProps) => {
|
||||||
|
const [filePath, setFilePath] = useState('')
|
||||||
|
const [hasError, setHasError] = useState(false)
|
||||||
|
const [errorText, setErrorText] = useState('')
|
||||||
|
|
||||||
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = event.target.value
|
||||||
|
|
||||||
|
const specialChars = /[`!@#$%^&*()_+\-=[\]{};':"\\|,<>?~]/
|
||||||
|
const fileExtension = /\.(exe|sh|htaccess)$/i
|
||||||
|
|
||||||
|
if (specialChars.test(value)) {
|
||||||
|
setHasError(true)
|
||||||
|
setErrorText('can not have special characters')
|
||||||
|
} else if (fileExtension.test(value)) {
|
||||||
|
setHasError(true)
|
||||||
|
setErrorText('can not save file with extensions [exe, sh, htaccess]')
|
||||||
|
} else {
|
||||||
|
setHasError(false)
|
||||||
|
setErrorText('')
|
||||||
|
}
|
||||||
|
setFilePath(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BootstrapDialog fullWidth onClose={() => setOpen(false)} open={open}>
|
||||||
|
<BootstrapDialogTitle id="abort-modal" handleOpen={setOpen}>
|
||||||
|
Save File
|
||||||
|
</BootstrapDialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
label="File Path"
|
||||||
|
value={filePath}
|
||||||
|
onChange={handleChange}
|
||||||
|
error={hasError}
|
||||||
|
helperText={errorText}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="contained" onClick={() => setOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => {
|
||||||
|
saveFile(filePath)
|
||||||
|
}}
|
||||||
|
disabled={hasError || !filePath}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</BootstrapDialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FilePathInputModal
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useContext } from 'react'
|
import React, { useState, useEffect, useContext } from 'react'
|
||||||
import { Link, useHistory, useLocation } from 'react-router-dom'
|
import { Link, useNavigate, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AppBar,
|
AppBar,
|
||||||
@@ -24,7 +24,7 @@ const baseUrl =
|
|||||||
const validTabs = ['/', '/SASjsDrive', '/SASjsStudio']
|
const validTabs = ['/', '/SASjsDrive', '/SASjsStudio']
|
||||||
|
|
||||||
const Header = (props: any) => {
|
const Header = (props: any) => {
|
||||||
const history = useHistory()
|
const navigate = useNavigate()
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const appContext = useContext(AppContext)
|
const appContext = useContext(AppContext)
|
||||||
const [tabValue, setTabValue] = useState(
|
const [tabValue, setTabValue] = useState(
|
||||||
@@ -74,7 +74,7 @@ const Header = (props: any) => {
|
|||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTabValue('/')
|
setTabValue('/')
|
||||||
history.push('/')
|
navigate('/')
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tabs
|
<Tabs
|
||||||
@@ -83,12 +83,6 @@ const Header = (props: any) => {
|
|||||||
onChange={handleTabChange}
|
onChange={handleTabChange}
|
||||||
>
|
>
|
||||||
<Tab label="Home" value="/" to="/" component={Link} />
|
<Tab label="Home" value="/" to="/" component={Link} />
|
||||||
<Tab
|
|
||||||
label="Drive"
|
|
||||||
value="/SASjsDrive"
|
|
||||||
to="/SASjsDrive"
|
|
||||||
component={Link}
|
|
||||||
/>
|
|
||||||
<Tab
|
<Tab
|
||||||
label="Studio"
|
label="Studio"
|
||||||
value="/SASjsStudio"
|
value="/SASjsStudio"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { styled } from '@mui/material/styles'
|
|||||||
|
|
||||||
import { BootstrapDialogTitle } from './dialogTitle'
|
import { BootstrapDialogTitle } from './dialogTitle'
|
||||||
|
|
||||||
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
export const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
||||||
'& .MuiDialogContent-root': {
|
'& .MuiDialogContent-root': {
|
||||||
padding: theme.spacing(2)
|
padding: theme.spacing(2)
|
||||||
},
|
},
|
||||||
@@ -14,7 +14,7 @@ const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export interface ModalProps {
|
type ModalProps = {
|
||||||
open: boolean
|
open: boolean
|
||||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
title: string
|
title: string
|
||||||
|
|||||||
92
web/src/components/nameInputModal.tsx
Normal file
92
web/src/components/nameInputModal.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
import { Button, DialogActions, DialogContent, TextField } from '@mui/material'
|
||||||
|
|
||||||
|
import { BootstrapDialogTitle } from './dialogTitle'
|
||||||
|
import { BootstrapDialog } from './modal'
|
||||||
|
|
||||||
|
type NameInputModalProps = {
|
||||||
|
open: boolean
|
||||||
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
title: string
|
||||||
|
isFolder: boolean
|
||||||
|
actionLabel: string
|
||||||
|
action: (name: string) => void
|
||||||
|
defaultName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const NameInputModal = ({
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
title,
|
||||||
|
isFolder,
|
||||||
|
actionLabel,
|
||||||
|
action,
|
||||||
|
defaultName
|
||||||
|
}: NameInputModalProps) => {
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [hasError, setHasError] = useState(false)
|
||||||
|
const [errorText, setErrorText] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (defaultName) setName(defaultName)
|
||||||
|
}, [defaultName])
|
||||||
|
|
||||||
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = event.target.value
|
||||||
|
|
||||||
|
const folderNameRegex = /[`!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?~]/
|
||||||
|
const fileNameRegex = /[`!@#$%^&*()_+\-=[\]{};':"\\|,<>/?~]/
|
||||||
|
const fileNameExtensionRegex = /.(exe|sh|htaccess)$/i
|
||||||
|
|
||||||
|
const specialChars = isFolder ? folderNameRegex : fileNameRegex
|
||||||
|
|
||||||
|
if (specialChars.test(value)) {
|
||||||
|
setHasError(true)
|
||||||
|
setErrorText('can not have special characters')
|
||||||
|
} else if (!isFolder && fileNameExtensionRegex.test(value)) {
|
||||||
|
setHasError(true)
|
||||||
|
setErrorText('can not add file with extensions [exe, sh, htaccess]')
|
||||||
|
} else {
|
||||||
|
setHasError(false)
|
||||||
|
setErrorText('')
|
||||||
|
}
|
||||||
|
|
||||||
|
setName(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BootstrapDialog fullWidth onClose={() => setOpen(false)} open={open}>
|
||||||
|
<BootstrapDialogTitle id="abort-modal" handleOpen={setOpen}>
|
||||||
|
{title}
|
||||||
|
</BootstrapDialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
label={isFolder ? 'Folder Name' : 'File Name'}
|
||||||
|
value={name}
|
||||||
|
onChange={handleChange}
|
||||||
|
error={hasError}
|
||||||
|
helperText={errorText}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="contained" onClick={() => setOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => {
|
||||||
|
action(name)
|
||||||
|
}}
|
||||||
|
disabled={hasError || !name}
|
||||||
|
>
|
||||||
|
{actionLabel}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</BootstrapDialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NameInputModal
|
||||||
243
web/src/components/tree.tsx
Normal file
243
web/src/components/tree.tsx
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { Menu, MenuItem } from '@mui/material'
|
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
||||||
|
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
|
||||||
|
|
||||||
|
import DeleteConfirmationModal from './deleteConfirmationModal'
|
||||||
|
import NameInputModal from './nameInputModal'
|
||||||
|
|
||||||
|
import { TreeNode } from '../utils/types'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
node: TreeNode
|
||||||
|
selectedFilePath: string
|
||||||
|
handleSelect: (filePath: string) => void
|
||||||
|
deleteNode: (path: string, isFolder: boolean) => void
|
||||||
|
addFile: (path: string) => void
|
||||||
|
addFolder: (path: string) => void
|
||||||
|
rename: (oldPath: string, newPath: string) => void
|
||||||
|
defaultExpanded?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const TreeView = ({
|
||||||
|
node,
|
||||||
|
selectedFilePath,
|
||||||
|
handleSelect,
|
||||||
|
deleteNode,
|
||||||
|
addFile,
|
||||||
|
addFolder,
|
||||||
|
rename,
|
||||||
|
defaultExpanded
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
style={{
|
||||||
|
listStyle: 'none',
|
||||||
|
padding: '0.25rem 0.85rem',
|
||||||
|
width: 'max-content'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TreeViewNode
|
||||||
|
node={node}
|
||||||
|
selectedFilePath={selectedFilePath}
|
||||||
|
handleSelect={handleSelect}
|
||||||
|
deleteNode={deleteNode}
|
||||||
|
addFile={addFile}
|
||||||
|
addFolder={addFolder}
|
||||||
|
rename={rename}
|
||||||
|
defaultExpanded={defaultExpanded}
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TreeView
|
||||||
|
|
||||||
|
const TreeViewNode = ({
|
||||||
|
node,
|
||||||
|
selectedFilePath,
|
||||||
|
handleSelect,
|
||||||
|
deleteNode,
|
||||||
|
addFile,
|
||||||
|
addFolder,
|
||||||
|
rename,
|
||||||
|
defaultExpanded
|
||||||
|
}: Props) => {
|
||||||
|
const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] =
|
||||||
|
useState(false)
|
||||||
|
const [deleteConfirmationModalMessage, setDeleteConfirmationModalMessage] =
|
||||||
|
useState('')
|
||||||
|
const [nameInputModalOpen, setNameInputModalOpen] = useState(false)
|
||||||
|
const [nameInputModalTitle, setNameInputModalTitle] = useState('')
|
||||||
|
const [nameInputModalActionLabel, setNameInputModalActionLabel] = useState('')
|
||||||
|
const [nameInputModalForFolder, setNameInputModalForFolder] = useState(false)
|
||||||
|
const [childVisible, setChildVisibility] = useState(false)
|
||||||
|
const [contextMenu, setContextMenu] = useState<{
|
||||||
|
mouseX: number
|
||||||
|
mouseY: number
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
|
const handleContextMenu = (event: React.MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
setContextMenu(
|
||||||
|
contextMenu === null
|
||||||
|
? {
|
||||||
|
mouseX: event.clientX + 2,
|
||||||
|
mouseY: event.clientY - 6
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasChild = node.children.length ? true : false
|
||||||
|
|
||||||
|
const handleItemClick = () => {
|
||||||
|
if (node.children.length) {
|
||||||
|
setChildVisibility((v) => !v)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSelect(node.relativePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (defaultExpanded && defaultExpanded[0] === node.relativePath) {
|
||||||
|
setChildVisibility(true)
|
||||||
|
defaultExpanded.shift()
|
||||||
|
}
|
||||||
|
}, [defaultExpanded, node.relativePath])
|
||||||
|
|
||||||
|
const handleDeleteItemClick = () => {
|
||||||
|
setContextMenu(null)
|
||||||
|
setDeleteConfirmationModalOpen(true)
|
||||||
|
setDeleteConfirmationModalMessage(
|
||||||
|
`Are you sure you want to delete ${node.isFolder ? 'folder' : 'file'} "${
|
||||||
|
node.relativePath
|
||||||
|
}"?`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteConfirm = () => {
|
||||||
|
setDeleteConfirmationModalOpen(false)
|
||||||
|
deleteNode(node.relativePath, node.isFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNewFolderItemClick = () => {
|
||||||
|
setContextMenu(null)
|
||||||
|
setNameInputModalOpen(true)
|
||||||
|
setNameInputModalTitle('Add Folder')
|
||||||
|
setNameInputModalActionLabel('Add')
|
||||||
|
setNameInputModalForFolder(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNewFileItemClick = () => {
|
||||||
|
setContextMenu(null)
|
||||||
|
setNameInputModalOpen(true)
|
||||||
|
setNameInputModalTitle('Add File')
|
||||||
|
setNameInputModalActionLabel('Add')
|
||||||
|
setNameInputModalForFolder(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addFileFolder = (name: string) => {
|
||||||
|
setNameInputModalOpen(false)
|
||||||
|
const path = node.relativePath + '/' + name
|
||||||
|
if (nameInputModalForFolder) addFolder(path)
|
||||||
|
else addFile(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRenameItemClick = () => {
|
||||||
|
setContextMenu(null)
|
||||||
|
setNameInputModalOpen(true)
|
||||||
|
setNameInputModalTitle('Rename')
|
||||||
|
setNameInputModalActionLabel('Rename')
|
||||||
|
setNameInputModalForFolder(node.isFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renameFileFolder = (name: string) => {
|
||||||
|
setNameInputModalOpen(false)
|
||||||
|
const oldPath = node.relativePath
|
||||||
|
const splittedPath = node.relativePath.split('/')
|
||||||
|
splittedPath.splice(-1, 1, name)
|
||||||
|
const newPath = splittedPath.join('/')
|
||||||
|
rename(oldPath, newPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div onContextMenu={handleContextMenu} style={{ cursor: 'context-menu' }}>
|
||||||
|
<li style={{ display: 'list-item' }}>
|
||||||
|
<div
|
||||||
|
className={`tree-item-label ${
|
||||||
|
selectedFilePath === node.relativePath ? 'selected' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => handleItemClick()}
|
||||||
|
>
|
||||||
|
{hasChild &&
|
||||||
|
(childVisible ? <ExpandMoreIcon /> : <ChevronRightIcon />)}
|
||||||
|
<div>{node.name}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasChild &&
|
||||||
|
childVisible &&
|
||||||
|
node.children.map((child, index) => (
|
||||||
|
<TreeView
|
||||||
|
key={node.relativePath + '-' + index}
|
||||||
|
node={child}
|
||||||
|
selectedFilePath={selectedFilePath}
|
||||||
|
handleSelect={handleSelect}
|
||||||
|
deleteNode={deleteNode}
|
||||||
|
addFile={addFile}
|
||||||
|
addFolder={addFolder}
|
||||||
|
rename={rename}
|
||||||
|
defaultExpanded={defaultExpanded}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</li>
|
||||||
|
<DeleteConfirmationModal
|
||||||
|
open={deleteConfirmationModalOpen}
|
||||||
|
setOpen={setDeleteConfirmationModalOpen}
|
||||||
|
message={deleteConfirmationModalMessage}
|
||||||
|
_delete={deleteConfirm}
|
||||||
|
/>
|
||||||
|
<NameInputModal
|
||||||
|
open={nameInputModalOpen}
|
||||||
|
setOpen={setNameInputModalOpen}
|
||||||
|
title={nameInputModalTitle}
|
||||||
|
isFolder={nameInputModalForFolder}
|
||||||
|
actionLabel={nameInputModalActionLabel}
|
||||||
|
action={
|
||||||
|
nameInputModalActionLabel === 'Add' ? addFileFolder : renameFileFolder
|
||||||
|
}
|
||||||
|
defaultName={node.relativePath.split('/').pop()}
|
||||||
|
/>
|
||||||
|
<Menu
|
||||||
|
open={contextMenu !== null}
|
||||||
|
onClose={() => setContextMenu(null)}
|
||||||
|
anchorReference="anchorPosition"
|
||||||
|
anchorPosition={
|
||||||
|
contextMenu !== null
|
||||||
|
? { top: contextMenu.mouseY, left: contextMenu.mouseX }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{node.isFolder && (
|
||||||
|
<div>
|
||||||
|
<MenuItem onClick={handleNewFolderItemClick}>Add Folder</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
disabled={!node.relativePath}
|
||||||
|
onClick={handleNewFileItemClick}
|
||||||
|
>
|
||||||
|
Add File
|
||||||
|
</MenuItem>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<MenuItem disabled={!node.relativePath} onClick={handleRenameItemClick}>
|
||||||
|
Rename
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem disabled={!node.relativePath} onClick={handleDeleteItemClick}>
|
||||||
|
Delete
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react'
|
|
||||||
import { useLocation } from 'react-router-dom'
|
|
||||||
import axios from 'axios'
|
|
||||||
|
|
||||||
import CssBaseline from '@mui/material/CssBaseline'
|
|
||||||
import Box from '@mui/material/Box'
|
|
||||||
|
|
||||||
import SideBar from './sideBar'
|
|
||||||
import Main from './main'
|
|
||||||
|
|
||||||
export interface TreeNode {
|
|
||||||
name: string
|
|
||||||
relativePath: string
|
|
||||||
absolutePath: string
|
|
||||||
children: Array<TreeNode>
|
|
||||||
}
|
|
||||||
|
|
||||||
const Drive = () => {
|
|
||||||
const location = useLocation()
|
|
||||||
const baseUrl = window.location.origin
|
|
||||||
|
|
||||||
const [selectedFilePath, setSelectedFilePath] = useState('')
|
|
||||||
const [directoryData, setDirectoryData] = useState<TreeNode | null>(null)
|
|
||||||
|
|
||||||
const setFilePathOnMount = useCallback(() => {
|
|
||||||
const queryParams = new URLSearchParams(location.search)
|
|
||||||
setSelectedFilePath(queryParams.get('filePath') ?? '')
|
|
||||||
}, [location.search])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
axios
|
|
||||||
.get(`/SASjsApi/drive/fileTree`)
|
|
||||||
.then((res: any) => {
|
|
||||||
if (res.data && res.data?.status === 'success') {
|
|
||||||
setDirectoryData(res.data.tree)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
setFilePathOnMount()
|
|
||||||
}, [setFilePathOnMount])
|
|
||||||
|
|
||||||
const handleSelect = (node: TreeNode) => {
|
|
||||||
if (node.children.length) return
|
|
||||||
|
|
||||||
if (!node.name.includes('.')) return
|
|
||||||
|
|
||||||
window.history.pushState(
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
`${baseUrl}/#/SASjsDrive?filePath=${node.relativePath}`
|
|
||||||
)
|
|
||||||
setSelectedFilePath(node.relativePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeFileFromTree = (path: string) => {
|
|
||||||
if (directoryData) {
|
|
||||||
const newTree = JSON.parse(JSON.stringify(directoryData)) as TreeNode
|
|
||||||
findAndRemoveNode(newTree, newTree, path)
|
|
||||||
setDirectoryData(newTree)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const findAndRemoveNode = (
|
|
||||||
node: TreeNode,
|
|
||||||
parentNode: TreeNode,
|
|
||||||
path: string
|
|
||||||
) => {
|
|
||||||
if (node.relativePath === path) {
|
|
||||||
removeNodeFromParent(parentNode, path)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (Array.isArray(node.children)) {
|
|
||||||
for (let i = 0; i < node.children.length; i++) {
|
|
||||||
if (findAndRemoveNode(node.children[i], node, path)) return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeNodeFromParent = (parent: TreeNode, path: string) => {
|
|
||||||
const index = parent.children.findIndex(
|
|
||||||
(node) => node.relativePath === path
|
|
||||||
)
|
|
||||||
if (index !== -1) {
|
|
||||||
parent.children.splice(index, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{ display: 'flex' }}>
|
|
||||||
<CssBaseline />
|
|
||||||
<SideBar
|
|
||||||
selectedFilePath={selectedFilePath}
|
|
||||||
directoryData={directoryData}
|
|
||||||
handleSelect={handleSelect}
|
|
||||||
/>
|
|
||||||
<Main
|
|
||||||
selectedFilePath={selectedFilePath}
|
|
||||||
removeFileFromTree={removeFileFromTree}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Drive
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
import axios from 'axios'
|
|
||||||
|
|
||||||
import Editor from 'react-monaco-editor'
|
|
||||||
|
|
||||||
import Box from '@mui/material/Box'
|
|
||||||
import Paper from '@mui/material/Paper'
|
|
||||||
import Stack from '@mui/material/Stack'
|
|
||||||
import Button from '@mui/material/Button'
|
|
||||||
import Toolbar from '@mui/material/Toolbar'
|
|
||||||
import CircularProgress from '@mui/material/CircularProgress'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
selectedFilePath: string
|
|
||||||
removeFileFromTree: (path: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const Main = (props: Props) => {
|
|
||||||
const baseUrl = window.location.origin
|
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
const [fileContentBeforeEdit, setFileContentBeforeEdit] = useState('')
|
|
||||||
const [fileContent, setFileContent] = useState('')
|
|
||||||
const [editMode, setEditMode] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (props.selectedFilePath) {
|
|
||||||
setIsLoading(true)
|
|
||||||
axios
|
|
||||||
.get(`/SASjsApi/drive/file?_filePath=${props.selectedFilePath}`)
|
|
||||||
.then((res: any) => {
|
|
||||||
setFileContent(res.data)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [props.selectedFilePath])
|
|
||||||
|
|
||||||
const handleDeleteBtnClick = () => {
|
|
||||||
setIsLoading(true)
|
|
||||||
|
|
||||||
const filePath = props.selectedFilePath
|
|
||||||
|
|
||||||
axios
|
|
||||||
.delete(`/SASjsApi/drive/file?_filePath=${filePath}`)
|
|
||||||
.then((res) => {
|
|
||||||
setFileContent('')
|
|
||||||
props.removeFileFromTree(filePath)
|
|
||||||
window.history.pushState('', '', `${baseUrl}/#/SASjsDrive`)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEditSaveBtnClick = () => {
|
|
||||||
if (!editMode) {
|
|
||||||
setFileContentBeforeEdit(fileContent)
|
|
||||||
setEditMode(true)
|
|
||||||
} else {
|
|
||||||
setIsLoading(true)
|
|
||||||
|
|
||||||
const formData = new FormData()
|
|
||||||
|
|
||||||
const stringBlob = new Blob([fileContent], { type: 'text/plain' })
|
|
||||||
formData.append('file', stringBlob, 'filename.sas')
|
|
||||||
formData.append('filePath', props.selectedFilePath)
|
|
||||||
|
|
||||||
axios
|
|
||||||
.patch(`/SASjsApi/drive/file`, formData)
|
|
||||||
.then((res) => {
|
|
||||||
setEditMode(false)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCancelExecuteBtnClick = () => {
|
|
||||||
if (editMode) {
|
|
||||||
setFileContent(fileContentBeforeEdit)
|
|
||||||
setEditMode(false)
|
|
||||||
} else {
|
|
||||||
window.open(
|
|
||||||
`${baseUrl}/SASjsApi/stp/execute?_program=${props.selectedFilePath}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
|
|
||||||
<Toolbar />
|
|
||||||
<Paper
|
|
||||||
sx={{
|
|
||||||
height: '75vh',
|
|
||||||
padding: '10px',
|
|
||||||
overflow: 'auto',
|
|
||||||
position: 'relative'
|
|
||||||
}}
|
|
||||||
elevation={3}
|
|
||||||
>
|
|
||||||
{isLoading && (
|
|
||||||
<CircularProgress
|
|
||||||
style={{ position: 'absolute', left: '50%', top: '50%' }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!isLoading && props?.selectedFilePath && !editMode && (
|
|
||||||
<code style={{ whiteSpace: 'break-spaces' }}>{fileContent}</code>
|
|
||||||
)}
|
|
||||||
{!isLoading && props?.selectedFilePath && editMode && (
|
|
||||||
<Editor
|
|
||||||
height="95%"
|
|
||||||
language="sas"
|
|
||||||
value={fileContent}
|
|
||||||
onChange={(val) => {
|
|
||||||
if (val) setFileContent(val)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Paper>
|
|
||||||
<Stack
|
|
||||||
spacing={3}
|
|
||||||
direction="row"
|
|
||||||
sx={{ justifyContent: 'center', marginTop: '20px' }}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={handleDeleteBtnClick}
|
|
||||||
disabled={isLoading || !props?.selectedFilePath}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={handleEditSaveBtnClick}
|
|
||||||
disabled={isLoading || !props?.selectedFilePath}
|
|
||||||
>
|
|
||||||
{!editMode ? 'Edit' : 'Save'}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={handleCancelExecuteBtnClick}
|
|
||||||
disabled={isLoading || !props?.selectedFilePath}
|
|
||||||
>
|
|
||||||
{editMode ? 'Cancel' : 'Execute'}
|
|
||||||
</Button>
|
|
||||||
{props?.selectedFilePath && (
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
component={Link}
|
|
||||||
to={`/SASjsStudio?_program=${props.selectedFilePath}`}
|
|
||||||
>
|
|
||||||
Open in Studio
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Main
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import React, { useMemo } from 'react'
|
|
||||||
|
|
||||||
import { makeStyles } from '@mui/styles'
|
|
||||||
|
|
||||||
import Box from '@mui/material/Box'
|
|
||||||
import Drawer from '@mui/material/Drawer'
|
|
||||||
import Toolbar from '@mui/material/Toolbar'
|
|
||||||
import ListItem from '@mui/material/ListItem'
|
|
||||||
import ListItemText from '@mui/material/ListItemText'
|
|
||||||
|
|
||||||
import TreeView from '@mui/lab/TreeView'
|
|
||||||
import TreeItem from '@mui/lab/TreeItem'
|
|
||||||
|
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
|
||||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
|
|
||||||
|
|
||||||
import { TreeNode } from '.'
|
|
||||||
|
|
||||||
const useStyles = makeStyles(() => ({
|
|
||||||
root: {
|
|
||||||
'& .MuiTreeItem-content': {
|
|
||||||
width: 'auto'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
listItem: {
|
|
||||||
padding: 0
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
const drawerWidth = 240
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
selectedFilePath: string
|
|
||||||
directoryData: TreeNode | null
|
|
||||||
handleSelect: (node: TreeNode) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const SideBar = ({ selectedFilePath, directoryData, handleSelect }: Props) => {
|
|
||||||
const classes = useStyles()
|
|
||||||
|
|
||||||
const defaultExpanded = useMemo(() => {
|
|
||||||
const splittedPath = selectedFilePath.split('/')
|
|
||||||
const arr = ['']
|
|
||||||
let nodeId = ''
|
|
||||||
splittedPath.forEach((path) => {
|
|
||||||
if (path !== '') {
|
|
||||||
nodeId += '/' + path
|
|
||||||
arr.push(nodeId)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return arr
|
|
||||||
}, [selectedFilePath])
|
|
||||||
|
|
||||||
const renderTree = (nodes: TreeNode) => (
|
|
||||||
<TreeItem
|
|
||||||
classes={{ root: classes.root }}
|
|
||||||
key={nodes.relativePath}
|
|
||||||
nodeId={nodes.relativePath}
|
|
||||||
label={
|
|
||||||
<ListItem
|
|
||||||
className={classes.listItem}
|
|
||||||
onClick={() => handleSelect(nodes)}
|
|
||||||
>
|
|
||||||
<ListItemText primary={nodes.name} />
|
|
||||||
</ListItem>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{Array.isArray(nodes.children)
|
|
||||||
? nodes.children.map((node) => renderTree(node))
|
|
||||||
: null}
|
|
||||||
</TreeItem>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Drawer
|
|
||||||
variant="permanent"
|
|
||||||
sx={{
|
|
||||||
width: drawerWidth,
|
|
||||||
flexShrink: 0,
|
|
||||||
[`& .MuiDrawer-paper`]: { width: drawerWidth, boxSizing: 'border-box' }
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Toolbar />
|
|
||||||
<Box sx={{ overflow: 'auto' }}>
|
|
||||||
{directoryData && (
|
|
||||||
<TreeView
|
|
||||||
defaultCollapseIcon={<ExpandMoreIcon />}
|
|
||||||
defaultExpandIcon={<ChevronRightIcon />}
|
|
||||||
defaultExpanded={defaultExpanded}
|
|
||||||
selected={defaultExpanded.slice(-1)}
|
|
||||||
>
|
|
||||||
{renderTree(directoryData)}
|
|
||||||
</TreeView>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Drawer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SideBar
|
|
||||||
@@ -28,7 +28,7 @@ import Modal from '../../components/modal'
|
|||||||
import PermissionFilterModal from './permissionFilterModal'
|
import PermissionFilterModal from './permissionFilterModal'
|
||||||
import AddPermissionModal from './addPermissionModal'
|
import AddPermissionModal from './addPermissionModal'
|
||||||
import UpdatePermissionModal from './updatePermissionModal'
|
import UpdatePermissionModal from './updatePermissionModal'
|
||||||
import DeleteModal from './deletePermissionModal'
|
import DeleteConfirmationModal from '../../components/deleteConfirmationModal'
|
||||||
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
|
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -61,7 +61,10 @@ const Permission = () => {
|
|||||||
const [addPermissionModalOpen, setAddPermissionModalOpen] = useState(false)
|
const [addPermissionModalOpen, setAddPermissionModalOpen] = useState(false)
|
||||||
const [updatePermissionModalOpen, setUpdatePermissionModalOpen] =
|
const [updatePermissionModalOpen, setUpdatePermissionModalOpen] =
|
||||||
useState(false)
|
useState(false)
|
||||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
|
const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] =
|
||||||
|
useState(false)
|
||||||
|
const [deleteConfirmationModalMessage, setDeleteConfirmationModalMessage] =
|
||||||
|
useState('')
|
||||||
const [selectedPermission, setSelectedPermission] =
|
const [selectedPermission, setSelectedPermission] =
|
||||||
useState<PermissionResponse>()
|
useState<PermissionResponse>()
|
||||||
const [filterModalOpen, setFilterModalOpen] = useState(false)
|
const [filterModalOpen, setFilterModalOpen] = useState(false)
|
||||||
@@ -236,11 +239,14 @@ const Permission = () => {
|
|||||||
|
|
||||||
const handleDeletePermissionClick = (permission: PermissionResponse) => {
|
const handleDeletePermissionClick = (permission: PermissionResponse) => {
|
||||||
setSelectedPermission(permission)
|
setSelectedPermission(permission)
|
||||||
setDeleteModalOpen(true)
|
setDeleteConfirmationModalOpen(true)
|
||||||
|
setDeleteConfirmationModalMessage(
|
||||||
|
'Are you sure you want to delete this permission?'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const deletePermission = () => {
|
const deletePermission = () => {
|
||||||
setDeleteModalOpen(false)
|
setDeleteConfirmationModalOpen(false)
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
axios
|
axios
|
||||||
.delete(`/SASjsApi/permission/${selectedPermission?.permissionId}`)
|
.delete(`/SASjsApi/permission/${selectedPermission?.permissionId}`)
|
||||||
@@ -338,10 +344,11 @@ const Permission = () => {
|
|||||||
permission={selectedPermission}
|
permission={selectedPermission}
|
||||||
updatePermission={updatePermission}
|
updatePermission={updatePermission}
|
||||||
/>
|
/>
|
||||||
<DeleteModal
|
<DeleteConfirmationModal
|
||||||
open={deleteModalOpen}
|
open={deleteConfirmationModalOpen}
|
||||||
setOpen={setDeleteModalOpen}
|
setOpen={setDeleteConfirmationModalOpen}
|
||||||
deletePermission={deletePermission}
|
message={deleteConfirmationModalMessage}
|
||||||
|
_delete={deletePermission}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
|||||||
657
web/src/containers/Studio/editor.tsx
Normal file
657
web/src/containers/Studio/editor.tsx
Normal file
@@ -0,0 +1,657 @@
|
|||||||
|
import React, { useEffect, useRef, useState, useContext } from 'react'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Backdrop,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Paper,
|
||||||
|
Select,
|
||||||
|
SelectChangeEvent,
|
||||||
|
Tab,
|
||||||
|
Tooltip,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material'
|
||||||
|
import { styled } from '@mui/material/styles'
|
||||||
|
|
||||||
|
import {
|
||||||
|
RocketLaunch,
|
||||||
|
MoreVert,
|
||||||
|
Save,
|
||||||
|
SaveAs,
|
||||||
|
Difference,
|
||||||
|
Edit
|
||||||
|
} from '@mui/icons-material'
|
||||||
|
import Editor, {
|
||||||
|
MonacoDiffEditor,
|
||||||
|
DiffEditorDidMount,
|
||||||
|
EditorDidMount
|
||||||
|
} from 'react-monaco-editor'
|
||||||
|
import { TabContext, TabList, TabPanel } from '@mui/lab'
|
||||||
|
|
||||||
|
import { AppContext, RunTimeType } from '../../context/appContext'
|
||||||
|
|
||||||
|
import FilePathInputModal from '../../components/filePathInputModal'
|
||||||
|
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
|
||||||
|
import Modal from '../../components/modal'
|
||||||
|
|
||||||
|
import { usePrompt, useStateWithCallback } from '../../utils/hooks'
|
||||||
|
|
||||||
|
const StyledTabPanel = styled(TabPanel)(() => ({
|
||||||
|
padding: '10px'
|
||||||
|
}))
|
||||||
|
|
||||||
|
const StyledTab = styled(Tab)(() => ({
|
||||||
|
fontSize: '1rem',
|
||||||
|
color: 'gray',
|
||||||
|
'&.Mui-selected': {
|
||||||
|
color: 'black'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
type SASjsEditorProps = {
|
||||||
|
selectedFilePath: string
|
||||||
|
setSelectedFilePath: (filePath: string, refreshSideBar?: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = window.location.origin
|
||||||
|
|
||||||
|
const SASjsEditor = ({
|
||||||
|
selectedFilePath,
|
||||||
|
setSelectedFilePath
|
||||||
|
}: SASjsEditorProps) => {
|
||||||
|
const appContext = useContext(AppContext)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [openModal, setOpenModal] = useState(false)
|
||||||
|
const [modalTitle, setModalTitle] = useState('')
|
||||||
|
const [modalPayload, setModalPayload] = useState('')
|
||||||
|
const [openSnackbar, setOpenSnackbar] = useState(false)
|
||||||
|
const [snackbarMessage, setSnackbarMessage] = useState('')
|
||||||
|
const [snackbarSeverity, setSnackbarSeverity] = useState<AlertSeverityType>(
|
||||||
|
AlertSeverityType.Success
|
||||||
|
)
|
||||||
|
const [prevFileContent, setPrevFileContent] = useStateWithCallback('')
|
||||||
|
const [fileContent, setFileContent] = useState('')
|
||||||
|
const [log, setLog] = useState('')
|
||||||
|
const [ctrlPressed, setCtrlPressed] = useState(false)
|
||||||
|
const [webout, setWebout] = useState('')
|
||||||
|
const [tab, setTab] = useState('1')
|
||||||
|
const [runTimes, setRunTimes] = useState<string[]>([])
|
||||||
|
const [selectedRunTime, setSelectedRunTime] = useState('')
|
||||||
|
const [selectedFileExtension, setSelectedFileExtension] = useState('')
|
||||||
|
const [openFilePathInputModal, setOpenFilePathInputModal] = useState(false)
|
||||||
|
const [showDiff, setShowDiff] = useState(false)
|
||||||
|
|
||||||
|
const editorRef = useRef(null as any)
|
||||||
|
|
||||||
|
const diffEditorRef = useRef(null as any)
|
||||||
|
|
||||||
|
const handleEditorDidMount: EditorDidMount = (editor) => {
|
||||||
|
editor.focus()
|
||||||
|
editorRef.current = editor
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDiffEditorDidMount: DiffEditorDidMount = (diffEditor) => {
|
||||||
|
diffEditor.focus()
|
||||||
|
diffEditorRef.current = diffEditor
|
||||||
|
}
|
||||||
|
|
||||||
|
usePrompt(
|
||||||
|
'Changes you made may not be saved.',
|
||||||
|
prevFileContent !== fileContent && !!selectedFilePath
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRunTimes(Object.values(appContext.runTimes))
|
||||||
|
}, [appContext.runTimes])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (runTimes.length) setSelectedRunTime(runTimes[0])
|
||||||
|
}, [runTimes])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedFilePath) {
|
||||||
|
setIsLoading(true)
|
||||||
|
setSelectedFileExtension(selectedFilePath.split('.').pop() ?? '')
|
||||||
|
axios
|
||||||
|
.get(`/SASjsApi/drive/file?_filePath=${selectedFilePath}`)
|
||||||
|
.then((res: any) => {
|
||||||
|
setPrevFileContent(res.data)
|
||||||
|
setFileContent(res.data)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setModalTitle('Abort')
|
||||||
|
setModalPayload(
|
||||||
|
typeof err.response.data === 'object'
|
||||||
|
? JSON.stringify(err.response.data)
|
||||||
|
: err.response.data
|
||||||
|
)
|
||||||
|
setOpenModal(true)
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false))
|
||||||
|
} else {
|
||||||
|
const content = localStorage.getItem('fileContent') ?? ''
|
||||||
|
setFileContent(content)
|
||||||
|
}
|
||||||
|
setLog('')
|
||||||
|
setWebout('')
|
||||||
|
setTab('1')
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [selectedFilePath])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fileContent.length && !selectedFilePath) {
|
||||||
|
localStorage.setItem('fileContent', fileContent)
|
||||||
|
}
|
||||||
|
}, [fileContent, selectedFilePath])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (runTimes.includes(selectedFileExtension))
|
||||||
|
setSelectedRunTime(selectedFileExtension)
|
||||||
|
}, [selectedFileExtension, runTimes])
|
||||||
|
|
||||||
|
const handleTabChange = (_e: any, newValue: string) => {
|
||||||
|
setTab(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSelection = () => {
|
||||||
|
const editor = editorRef.current as any
|
||||||
|
const selection = editor?.getModel().getValueInRange(editor?.getSelection())
|
||||||
|
return selection ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRunBtnClick = () => runCode(getSelection() || fileContent)
|
||||||
|
|
||||||
|
const runCode = (code: string) => {
|
||||||
|
setIsLoading(true)
|
||||||
|
axios
|
||||||
|
.post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime })
|
||||||
|
.then((res: any) => {
|
||||||
|
const parsedLog = res?.data?.log
|
||||||
|
.map((logLine: any) => logLine.line)
|
||||||
|
.join('\n')
|
||||||
|
|
||||||
|
setLog(parsedLog)
|
||||||
|
|
||||||
|
setWebout(`${res.data?._webout}`)
|
||||||
|
setTab('2')
|
||||||
|
|
||||||
|
// Scroll to bottom of log
|
||||||
|
window.scrollTo(0, document.body.scrollHeight)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setModalTitle('Abort')
|
||||||
|
setModalPayload(
|
||||||
|
typeof err.response.data === 'object'
|
||||||
|
? JSON.stringify(err.response.data)
|
||||||
|
: err.response.data
|
||||||
|
)
|
||||||
|
setOpenModal(true)
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (event: any) => {
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
if (event.key === 'v') {
|
||||||
|
setCtrlPressed(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Enter') runCode(getSelection() || fileContent)
|
||||||
|
if (!ctrlPressed) setCtrlPressed(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyUp = (event: any) => {
|
||||||
|
if (!event.ctrlKey && ctrlPressed) setCtrlPressed(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChangeRunTime = (event: SelectChangeEvent) => {
|
||||||
|
setSelectedRunTime(event.target.value as RunTimeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFilePathInput = (filePath: string) => {
|
||||||
|
setOpenFilePathInputModal(false)
|
||||||
|
saveFile(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveFile = (filePath?: string) => {
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
if (filePath) {
|
||||||
|
filePath = filePath.startsWith('/') ? filePath : `/${filePath}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
const stringBlob = new Blob([fileContent], { type: 'text/plain' })
|
||||||
|
formData.append('file', stringBlob, 'filename.sas')
|
||||||
|
formData.append('filePath', filePath ?? selectedFilePath)
|
||||||
|
|
||||||
|
const axiosPromise = filePath
|
||||||
|
? axios.post('/SASjsApi/drive/file', formData)
|
||||||
|
: axios.patch('/SASjsApi/drive/file', formData)
|
||||||
|
|
||||||
|
axiosPromise
|
||||||
|
.then(() => {
|
||||||
|
if (filePath && fileContent === prevFileContent) {
|
||||||
|
// when fileContent and prevFileContent is same,
|
||||||
|
// callback function in setPrevFileContent method is not called
|
||||||
|
// because behind the scene useEffect hook is being used
|
||||||
|
// for calling callback function, and it's only fired when the
|
||||||
|
// new value is not equal to old value.
|
||||||
|
// So, we'll have to explicitly update the selected file path
|
||||||
|
|
||||||
|
setSelectedFilePath(filePath, true)
|
||||||
|
} else {
|
||||||
|
setPrevFileContent(fileContent, () => {
|
||||||
|
if (filePath) {
|
||||||
|
setSelectedFilePath(filePath, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setSnackbarMessage('File saved!')
|
||||||
|
setSnackbarSeverity(AlertSeverityType.Success)
|
||||||
|
setOpenSnackbar(true)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setModalTitle('Abort')
|
||||||
|
setModalPayload(
|
||||||
|
typeof err.response.data === 'object'
|
||||||
|
? JSON.stringify(err.response.data)
|
||||||
|
: err.response.data
|
||||||
|
)
|
||||||
|
setOpenModal(true)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}>
|
||||||
|
<Backdrop
|
||||||
|
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||||
|
open={isLoading}
|
||||||
|
>
|
||||||
|
<CircularProgress color="inherit" />
|
||||||
|
</Backdrop>
|
||||||
|
{selectedFilePath && !runTimes.includes(selectedFileExtension) ? (
|
||||||
|
<Box sx={{ marginTop: '10px' }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<FileMenu
|
||||||
|
showDiff={showDiff}
|
||||||
|
setShowDiff={setShowDiff}
|
||||||
|
prevFileContent={prevFileContent}
|
||||||
|
currentFileContent={fileContent}
|
||||||
|
selectedFilePath={selectedFilePath}
|
||||||
|
setOpenFilePathInputModal={setOpenFilePathInputModal}
|
||||||
|
saveFile={saveFile}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Paper
|
||||||
|
sx={{
|
||||||
|
height: 'calc(100vh - 140px)',
|
||||||
|
padding: '10px',
|
||||||
|
margin: '0 24px',
|
||||||
|
overflow: 'auto',
|
||||||
|
position: 'relative'
|
||||||
|
}}
|
||||||
|
elevation={3}
|
||||||
|
>
|
||||||
|
{showDiff ? (
|
||||||
|
<MonacoDiffEditor
|
||||||
|
height="98%"
|
||||||
|
language={getLanguage(selectedFileExtension)}
|
||||||
|
original={prevFileContent}
|
||||||
|
value={fileContent}
|
||||||
|
editorDidMount={handleDiffEditorDidMount}
|
||||||
|
options={{ readOnly: ctrlPressed }}
|
||||||
|
onChange={(val) => setFileContent(val)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Editor
|
||||||
|
height="98%"
|
||||||
|
language={getLanguage(selectedFileExtension)}
|
||||||
|
value={fileContent}
|
||||||
|
editorDidMount={handleEditorDidMount}
|
||||||
|
options={{ readOnly: ctrlPressed }}
|
||||||
|
onChange={(val) => setFileContent(val)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<TabContext value={tab}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
borderBottom: 1,
|
||||||
|
borderColor: 'divider',
|
||||||
|
position: 'fixed',
|
||||||
|
background: 'white',
|
||||||
|
width: '85%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TabList onChange={handleTabChange} centered>
|
||||||
|
<StyledTab label="Code" value="1" />
|
||||||
|
<StyledTab label="Log" value="2" />
|
||||||
|
<StyledTab
|
||||||
|
label={
|
||||||
|
<Tooltip title="Displays content from the _webout fileref">
|
||||||
|
<Typography>Webout</Typography>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
value="3"
|
||||||
|
/>
|
||||||
|
</TabList>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<StyledTabPanel
|
||||||
|
sx={{ paddingBottom: 0, marginTop: '45px' }}
|
||||||
|
value="1"
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<RunMenu
|
||||||
|
fileContent={fileContent}
|
||||||
|
prevFileContent={prevFileContent}
|
||||||
|
selectedFilePath={selectedFilePath}
|
||||||
|
selectedRunTime={selectedRunTime}
|
||||||
|
runTimes={runTimes}
|
||||||
|
handleChangeRunTime={handleChangeRunTime}
|
||||||
|
handleRunBtnClick={handleRunBtnClick}
|
||||||
|
/>
|
||||||
|
<FileMenu
|
||||||
|
showDiff={showDiff}
|
||||||
|
setShowDiff={setShowDiff}
|
||||||
|
prevFileContent={prevFileContent}
|
||||||
|
currentFileContent={fileContent}
|
||||||
|
selectedFilePath={selectedFilePath}
|
||||||
|
setOpenFilePathInputModal={setOpenFilePathInputModal}
|
||||||
|
saveFile={saveFile}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Paper
|
||||||
|
onKeyUp={handleKeyUp}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
sx={{
|
||||||
|
height: 'calc(100vh - 170px)',
|
||||||
|
padding: '10px',
|
||||||
|
overflow: 'auto',
|
||||||
|
position: 'relative'
|
||||||
|
}}
|
||||||
|
elevation={3}
|
||||||
|
>
|
||||||
|
{showDiff ? (
|
||||||
|
<MonacoDiffEditor
|
||||||
|
height="98%"
|
||||||
|
language={getLanguage(selectedFileExtension)}
|
||||||
|
original={prevFileContent}
|
||||||
|
value={fileContent}
|
||||||
|
editorDidMount={handleDiffEditorDidMount}
|
||||||
|
options={{ readOnly: ctrlPressed }}
|
||||||
|
onChange={(val) => setFileContent(val)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Editor
|
||||||
|
height="98%"
|
||||||
|
language={getLanguage(selectedFileExtension)}
|
||||||
|
value={fileContent}
|
||||||
|
editorDidMount={handleEditorDidMount}
|
||||||
|
options={{ readOnly: ctrlPressed }}
|
||||||
|
onChange={(val) => setFileContent(val)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: -10,
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: '13px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Press CTRL + ENTER to run code
|
||||||
|
</p>
|
||||||
|
</Paper>
|
||||||
|
</StyledTabPanel>
|
||||||
|
<StyledTabPanel value="2">
|
||||||
|
<div style={{ marginTop: '50px' }}>
|
||||||
|
<h2>SAS Log</h2>
|
||||||
|
<pre>{log}</pre>
|
||||||
|
</div>
|
||||||
|
</StyledTabPanel>
|
||||||
|
<StyledTabPanel value="3">
|
||||||
|
<div style={{ marginTop: '50px' }}>
|
||||||
|
<pre>{webout}</pre>
|
||||||
|
</div>
|
||||||
|
</StyledTabPanel>
|
||||||
|
</TabContext>
|
||||||
|
)}
|
||||||
|
<Modal
|
||||||
|
open={openModal}
|
||||||
|
setOpen={setOpenModal}
|
||||||
|
title={modalTitle}
|
||||||
|
payload={modalPayload}
|
||||||
|
/>
|
||||||
|
<BootstrapSnackbar
|
||||||
|
open={openSnackbar}
|
||||||
|
setOpen={setOpenSnackbar}
|
||||||
|
message={snackbarMessage}
|
||||||
|
severity={snackbarSeverity}
|
||||||
|
/>
|
||||||
|
<FilePathInputModal
|
||||||
|
open={openFilePathInputModal}
|
||||||
|
setOpen={setOpenFilePathInputModal}
|
||||||
|
saveFile={handleFilePathInput}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SASjsEditor
|
||||||
|
|
||||||
|
type RunMenuProps = {
|
||||||
|
selectedFilePath: string
|
||||||
|
fileContent: string
|
||||||
|
prevFileContent: string
|
||||||
|
selectedRunTime: string
|
||||||
|
runTimes: string[]
|
||||||
|
handleChangeRunTime: (event: SelectChangeEvent) => void
|
||||||
|
handleRunBtnClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const RunMenu = ({
|
||||||
|
selectedFilePath,
|
||||||
|
fileContent,
|
||||||
|
prevFileContent,
|
||||||
|
selectedRunTime,
|
||||||
|
runTimes,
|
||||||
|
handleChangeRunTime,
|
||||||
|
handleRunBtnClick
|
||||||
|
}: RunMenuProps) => {
|
||||||
|
const launchProgram = () => {
|
||||||
|
window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${selectedFilePath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip title="CTRL+ENTER will also run code">
|
||||||
|
<Button
|
||||||
|
onClick={handleRunBtnClick}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '5px 5px',
|
||||||
|
minWidth: 'unset'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
draggable="false"
|
||||||
|
style={{ width: '25px' }}
|
||||||
|
src="/running-sas.png"
|
||||||
|
></img>
|
||||||
|
<span style={{ fontSize: '12px' }}>RUN</span>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
{selectedFilePath ? (
|
||||||
|
<Box sx={{ marginLeft: '10px' }}>
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
fileContent !== prevFileContent
|
||||||
|
? 'Save file before launching program'
|
||||||
|
: 'Launch program in new window'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<IconButton
|
||||||
|
disabled={fileContent !== prevFileContent}
|
||||||
|
onClick={launchProgram}
|
||||||
|
>
|
||||||
|
<RocketLaunch />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box sx={{ minWidth: '75px', marginLeft: '10px' }}>
|
||||||
|
<FormControl variant="standard">
|
||||||
|
<Select
|
||||||
|
labelId="run-time-select-label"
|
||||||
|
id="run-time-select"
|
||||||
|
value={selectedRunTime}
|
||||||
|
onChange={handleChangeRunTime}
|
||||||
|
>
|
||||||
|
{runTimes.map((runTime) => (
|
||||||
|
<MenuItem key={runTime} value={runTime}>
|
||||||
|
{runTime}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileMenuProps = {
|
||||||
|
showDiff: boolean
|
||||||
|
setShowDiff: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
prevFileContent: string
|
||||||
|
currentFileContent: string
|
||||||
|
selectedFilePath: string
|
||||||
|
setOpenFilePathInputModal: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
saveFile: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileMenu = ({
|
||||||
|
showDiff,
|
||||||
|
setShowDiff,
|
||||||
|
prevFileContent,
|
||||||
|
currentFileContent,
|
||||||
|
selectedFilePath,
|
||||||
|
setOpenFilePathInputModal,
|
||||||
|
saveFile
|
||||||
|
}: FileMenuProps) => {
|
||||||
|
const [anchorEl, setAnchorEl] = useState<
|
||||||
|
(EventTarget & HTMLButtonElement) | null
|
||||||
|
>(null)
|
||||||
|
|
||||||
|
const handleMenu = (
|
||||||
|
event?: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
|
) => {
|
||||||
|
if (event) setAnchorEl(event.currentTarget)
|
||||||
|
else setAnchorEl(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDiffBtnClick = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
setShowDiff(!showDiff)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveAsBtnClick = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
setOpenFilePathInputModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveBtnClick = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
saveFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip title="Save File Menu">
|
||||||
|
<IconButton onClick={handleMenu}>
|
||||||
|
<MoreVert />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Menu
|
||||||
|
id="save-file-menu"
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'center'
|
||||||
|
}}
|
||||||
|
keepMounted
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'center'
|
||||||
|
}}
|
||||||
|
open={!!anchorEl}
|
||||||
|
onClose={() => handleMenu()}
|
||||||
|
>
|
||||||
|
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
onClick={handleDiffBtnClick}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={showDiff ? <Edit /> : <Difference />}
|
||||||
|
>
|
||||||
|
{showDiff ? 'Edit' : 'Diff'}
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
onClick={handleSaveBtnClick}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<Save />}
|
||||||
|
disabled={
|
||||||
|
!selectedFilePath || prevFileContent === currentFileContent
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
onClick={handleSaveAsBtnClick}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<SaveAs />}
|
||||||
|
>
|
||||||
|
Save As
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLanguage = (extension: string) => {
|
||||||
|
if (extension === 'js') return 'javascript'
|
||||||
|
|
||||||
|
if (extension === 'ts') return 'typescript'
|
||||||
|
|
||||||
|
if (extension === 'md' || extension === 'mdx') return 'markdown'
|
||||||
|
|
||||||
|
return extension
|
||||||
|
}
|
||||||
@@ -1,253 +1,99 @@
|
|||||||
import React, { useEffect, useRef, useState, useContext } from 'react'
|
import React, { useState, useEffect, useCallback } from 'react'
|
||||||
|
import { useSearchParams } from 'react-router-dom'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import {
|
import CssBaseline from '@mui/material/CssBaseline'
|
||||||
Backdrop,
|
import Box from '@mui/material/Box'
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
CircularProgress,
|
|
||||||
FormControl,
|
|
||||||
MenuItem,
|
|
||||||
Paper,
|
|
||||||
Select,
|
|
||||||
SelectChangeEvent,
|
|
||||||
Tab,
|
|
||||||
Tooltip
|
|
||||||
} from '@mui/material'
|
|
||||||
import { makeStyles } from '@mui/styles'
|
|
||||||
import Editor, { EditorDidMount } from 'react-monaco-editor'
|
|
||||||
import { useLocation } from 'react-router-dom'
|
|
||||||
import { TabContext, TabList, TabPanel } from '@mui/lab'
|
|
||||||
|
|
||||||
import { AppContext, RunTimeType } from '../../context/appContext'
|
import { TreeNode } from '../../utils/types'
|
||||||
|
|
||||||
const useStyles = makeStyles(() => ({
|
import SideBar from './sideBar'
|
||||||
root: {
|
import SASjsEditor from './editor'
|
||||||
fontSize: '1rem',
|
|
||||||
color: 'gray',
|
|
||||||
'&.Mui-selected': {
|
|
||||||
color: 'black'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
subMenu: {
|
|
||||||
marginTop: '25px',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center'
|
|
||||||
},
|
|
||||||
runButton: {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: '5px 5px',
|
|
||||||
minWidth: 'unset'
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
const Studio = () => {
|
const Studio = () => {
|
||||||
const appContext = useContext(AppContext)
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
const location = useLocation()
|
const [selectedFilePath, setSelectedFilePath] = useState('')
|
||||||
const [fileContent, setFileContent] = useState('')
|
const [directoryData, setDirectoryData] = useState<TreeNode | null>(null)
|
||||||
const [log, setLog] = useState('')
|
|
||||||
const [ctrlPressed, setCtrlPressed] = useState(false)
|
|
||||||
const [webout, setWebout] = useState('')
|
|
||||||
const [tab, setTab] = useState('1')
|
|
||||||
const [runTimes, setRunTimes] = useState<string[]>([])
|
|
||||||
const [selectedRunTime, setSelectedRunTime] = useState('')
|
|
||||||
const [isRunning, setIsRunning] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRunTimes(Object.values(appContext.runTimes))
|
setSelectedFilePath(searchParams.get('filePath') ?? '')
|
||||||
}, [appContext.runTimes])
|
}, [searchParams])
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchDirectoryData = useCallback(() => {
|
||||||
if (runTimes.length) setSelectedRunTime(runTimes[0])
|
|
||||||
}, [runTimes])
|
|
||||||
|
|
||||||
const handleTabChange = (_e: any, newValue: string) => {
|
|
||||||
setTab(newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
const editorRef = useRef(null as any)
|
|
||||||
const handleEditorDidMount: EditorDidMount = (editor) => {
|
|
||||||
editor.focus()
|
|
||||||
editorRef.current = editor
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSelection = () => {
|
|
||||||
const editor = editorRef.current as any
|
|
||||||
const selection = editor?.getModel().getValueInRange(editor?.getSelection())
|
|
||||||
return selection ?? ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRunBtnClick = () => runCode(getSelection() || fileContent)
|
|
||||||
|
|
||||||
const runCode = (code: string) => {
|
|
||||||
setIsRunning(true)
|
|
||||||
axios
|
axios
|
||||||
.post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime })
|
.get(`/SASjsApi/drive/fileTree`)
|
||||||
.then((res: any) => {
|
.then((res: any) => {
|
||||||
const parsedLog = res?.data?.log
|
if (res.data && res.data?.status === 'success') {
|
||||||
.map((logLine: any) => logLine.line)
|
setDirectoryData(res.data.tree)
|
||||||
.join('\n')
|
}
|
||||||
|
})
|
||||||
setLog(parsedLog)
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
setWebout(`${res.data?._webout}`)
|
|
||||||
setTab('2')
|
|
||||||
|
|
||||||
// Scroll to bottom of log
|
|
||||||
window.scrollTo(0, document.body.scrollHeight)
|
|
||||||
})
|
})
|
||||||
.catch((err) => console.log(err))
|
|
||||||
.finally(() => setIsRunning(false))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyDown = (event: any) => {
|
|
||||||
if (event.ctrlKey) {
|
|
||||||
if (event.key === 'v') {
|
|
||||||
setCtrlPressed(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === 'Enter') runCode(getSelection() || fileContent)
|
|
||||||
if (!ctrlPressed) setCtrlPressed(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyUp = (event: any) => {
|
|
||||||
if (!event.ctrlKey && ctrlPressed) setCtrlPressed(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChangeRunTime = (event: SelectChangeEvent) => {
|
|
||||||
setSelectedRunTime(event.target.value as RunTimeType)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const content = localStorage.getItem('fileContent') ?? ''
|
|
||||||
setFileContent(content)
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (fileContent.length) {
|
fetchDirectoryData()
|
||||||
localStorage.setItem('fileContent', fileContent)
|
}, [fetchDirectoryData])
|
||||||
|
|
||||||
|
const handleSelect = (filePath: string, refreshSideBar?: boolean) => {
|
||||||
|
setSearchParams({ filePath })
|
||||||
|
if (refreshSideBar) fetchDirectoryData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeFileFromTree = (path: string) => {
|
||||||
|
if (directoryData) {
|
||||||
|
const newTree = JSON.parse(JSON.stringify(directoryData)) as TreeNode
|
||||||
|
findAndRemoveNode(newTree, newTree, path)
|
||||||
|
setDirectoryData(newTree)
|
||||||
}
|
}
|
||||||
}, [fileContent])
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const findAndRemoveNode = (
|
||||||
const params = new URLSearchParams(location.search)
|
node: TreeNode,
|
||||||
const programPath = params.get('_program')
|
parentNode: TreeNode,
|
||||||
|
path: string
|
||||||
|
) => {
|
||||||
|
if (node.relativePath === path) {
|
||||||
|
removeNodeFromParent(parentNode, path)
|
||||||
|
// reset selected file path and file path query param
|
||||||
|
if (
|
||||||
|
node.relativePath === selectedFilePath ||
|
||||||
|
selectedFilePath.startsWith(node.relativePath)
|
||||||
|
)
|
||||||
|
setSearchParams({})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (Array.isArray(node.children)) {
|
||||||
|
for (let i = 0; i < node.children.length; i++) {
|
||||||
|
if (findAndRemoveNode(node.children[i], node, path)) return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (programPath?.length)
|
const removeNodeFromParent = (parent: TreeNode, path: string) => {
|
||||||
axios
|
const index = parent.children.findIndex(
|
||||||
.get(`/SASjsApi/drive/file?filePath=${programPath}`)
|
(node) => node.relativePath === path
|
||||||
.then((res: any) => setFileContent(res.data.fileContent))
|
)
|
||||||
.catch((err) => console.log(err))
|
if (index !== -1) {
|
||||||
}, [location.search])
|
parent.children.splice(index, 1)
|
||||||
|
}
|
||||||
const classes = useStyles()
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box sx={{ display: 'flex' }}>
|
||||||
onKeyUp={handleKeyUp}
|
<CssBaseline />
|
||||||
onKeyDown={handleKeyDown}
|
<SideBar
|
||||||
sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}
|
selectedFilePath={selectedFilePath}
|
||||||
>
|
directoryData={directoryData}
|
||||||
<TabContext value={tab}>
|
handleSelect={handleSelect}
|
||||||
<Box
|
removeFileFromTree={removeFileFromTree}
|
||||||
sx={{
|
refreshSideBar={fetchDirectoryData}
|
||||||
borderBottom: 1,
|
/>
|
||||||
borderColor: 'divider'
|
<SASjsEditor
|
||||||
}}
|
selectedFilePath={selectedFilePath}
|
||||||
style={{ position: 'fixed', background: 'white', width: '100%' }}
|
setSelectedFilePath={handleSelect}
|
||||||
>
|
/>
|
||||||
<TabList onChange={handleTabChange} centered>
|
|
||||||
<Tab className={classes.root} label="Code" value="1" />
|
|
||||||
<Tab className={classes.root} label="Log" value="2" />
|
|
||||||
<Tooltip title="Displays content from the _webout fileref">
|
|
||||||
<Tab className={classes.root} label="Webout" value="3" />
|
|
||||||
</Tooltip>
|
|
||||||
</TabList>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<TabPanel sx={{ paddingBottom: 0 }} value="1">
|
|
||||||
<Backdrop
|
|
||||||
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
|
||||||
open={isRunning}
|
|
||||||
>
|
|
||||||
<CircularProgress color="inherit" />
|
|
||||||
</Backdrop>
|
|
||||||
<div className={classes.subMenu}>
|
|
||||||
<Tooltip title="CTRL+ENTER will also run SAS code">
|
|
||||||
<Button onClick={handleRunBtnClick} className={classes.runButton}>
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
draggable="false"
|
|
||||||
style={{ width: '25px' }}
|
|
||||||
src="/running-sas.png"
|
|
||||||
></img>
|
|
||||||
<span style={{ fontSize: '12px' }}>RUN</span>
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Box sx={{ minWidth: '75px', marginLeft: '10px' }}>
|
|
||||||
<FormControl variant="standard">
|
|
||||||
<Select
|
|
||||||
labelId="run-time-select-label"
|
|
||||||
id="run-time-select"
|
|
||||||
value={selectedRunTime}
|
|
||||||
onChange={handleChangeRunTime}
|
|
||||||
>
|
|
||||||
{runTimes.map((runTime) => (
|
|
||||||
<MenuItem key={runTime} value={runTime}>
|
|
||||||
{runTime}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</Box>
|
|
||||||
</div>
|
|
||||||
<Paper
|
|
||||||
sx={{
|
|
||||||
height: 'calc(100vh - 170px)',
|
|
||||||
padding: '10px',
|
|
||||||
overflow: 'auto',
|
|
||||||
position: 'relative'
|
|
||||||
}}
|
|
||||||
elevation={3}
|
|
||||||
>
|
|
||||||
<Editor
|
|
||||||
height="98%"
|
|
||||||
language="sas"
|
|
||||||
value={fileContent}
|
|
||||||
editorDidMount={handleEditorDidMount}
|
|
||||||
options={{ readOnly: ctrlPressed }}
|
|
||||||
onChange={(val) => {
|
|
||||||
if (val) setFileContent(val)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: -10,
|
|
||||||
textAlign: 'center',
|
|
||||||
fontSize: '13px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Press CTRL + ENTER to run SAS code
|
|
||||||
</p>
|
|
||||||
</Paper>
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel value="2">
|
|
||||||
<div style={{ marginTop: '50px' }}>
|
|
||||||
<h2>SAS Log</h2>
|
|
||||||
<pre>{log}</pre>
|
|
||||||
</div>
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel value="3">
|
|
||||||
<div style={{ marginTop: '50px' }}>
|
|
||||||
<pre>{webout}</pre>
|
|
||||||
</div>
|
|
||||||
</TabPanel>
|
|
||||||
</TabContext>
|
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
196
web/src/containers/Studio/sideBar.tsx
Normal file
196
web/src/containers/Studio/sideBar.tsx
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import React, { useState, useMemo } from 'react'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { Backdrop, Box, CircularProgress, Drawer, Toolbar } from '@mui/material'
|
||||||
|
|
||||||
|
import TreeView from '../../components/tree'
|
||||||
|
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
|
||||||
|
import Modal from '../../components/modal'
|
||||||
|
import { TreeNode } from '../../utils/types'
|
||||||
|
|
||||||
|
const drawerWidth = '15%'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
selectedFilePath: string
|
||||||
|
directoryData: TreeNode | null
|
||||||
|
handleSelect: (filePath: string) => void
|
||||||
|
removeFileFromTree: (filePath: string) => void
|
||||||
|
refreshSideBar: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const SideBar = ({
|
||||||
|
selectedFilePath,
|
||||||
|
directoryData,
|
||||||
|
handleSelect,
|
||||||
|
removeFileFromTree,
|
||||||
|
refreshSideBar
|
||||||
|
}: Props) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [openModal, setOpenModal] = useState(false)
|
||||||
|
const [modalTitle, setModalTitle] = useState('')
|
||||||
|
const [modalPayload, setModalPayload] = useState('')
|
||||||
|
const [openSnackbar, setOpenSnackbar] = useState(false)
|
||||||
|
const [snackbarMessage, setSnackbarMessage] = useState('')
|
||||||
|
const [snackbarSeverity, setSnackbarSeverity] = useState<AlertSeverityType>(
|
||||||
|
AlertSeverityType.Success
|
||||||
|
)
|
||||||
|
const defaultExpanded = useMemo(() => {
|
||||||
|
const splittedPath = selectedFilePath.split('/')
|
||||||
|
const arr = ['']
|
||||||
|
let nodeId = ''
|
||||||
|
splittedPath.forEach((path) => {
|
||||||
|
if (path !== '') {
|
||||||
|
nodeId += '/' + path
|
||||||
|
arr.push(nodeId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return arr
|
||||||
|
}, [selectedFilePath])
|
||||||
|
|
||||||
|
const deleteNode = (path: string, isFolder: boolean) => {
|
||||||
|
setIsLoading(true)
|
||||||
|
const axiosPromise = axios.delete(
|
||||||
|
`/SASjsApi/drive/${
|
||||||
|
isFolder ? `folder?_folderPath=${path}` : `file?_filePath=${path}`
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
|
||||||
|
axiosPromise
|
||||||
|
.then(() => {
|
||||||
|
removeFileFromTree(path)
|
||||||
|
setSnackbarMessage('Deleted!')
|
||||||
|
setSnackbarSeverity(AlertSeverityType.Success)
|
||||||
|
setOpenSnackbar(true)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setModalTitle('Abort')
|
||||||
|
setModalPayload(
|
||||||
|
typeof err.response.data === 'object'
|
||||||
|
? JSON.stringify(err.response.data)
|
||||||
|
: err.response.data
|
||||||
|
)
|
||||||
|
setOpenModal(true)
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
const addFile = (filePath: string) => {
|
||||||
|
const formData = new FormData()
|
||||||
|
const stringBlob = new Blob([''], { type: 'text/plain' })
|
||||||
|
formData.append('file', stringBlob)
|
||||||
|
formData.append('filePath', filePath)
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
axios
|
||||||
|
.post('/SASjsApi/drive/file', formData)
|
||||||
|
.then(() => {
|
||||||
|
setSnackbarMessage('File added!')
|
||||||
|
setSnackbarSeverity(AlertSeverityType.Success)
|
||||||
|
setOpenSnackbar(true)
|
||||||
|
refreshSideBar()
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setModalTitle('Abort')
|
||||||
|
setModalPayload(
|
||||||
|
typeof err.response.data === 'object'
|
||||||
|
? JSON.stringify(err.response.data)
|
||||||
|
: err.response.data
|
||||||
|
)
|
||||||
|
setOpenModal(true)
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
const addFolder = (folderPath: string) => {
|
||||||
|
setIsLoading(true)
|
||||||
|
axios
|
||||||
|
.post('/SASjsApi/drive/folder', { folderPath })
|
||||||
|
.then(() => {
|
||||||
|
setSnackbarMessage('Folder added!')
|
||||||
|
setSnackbarSeverity(AlertSeverityType.Success)
|
||||||
|
setOpenSnackbar(true)
|
||||||
|
refreshSideBar()
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setModalTitle('Abort')
|
||||||
|
setModalPayload(
|
||||||
|
typeof err.response.data === 'object'
|
||||||
|
? JSON.stringify(err.response.data)
|
||||||
|
: err.response.data
|
||||||
|
)
|
||||||
|
setOpenModal(true)
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
const rename = (oldPath: string, newPath: string) => {
|
||||||
|
setIsLoading(true)
|
||||||
|
axios
|
||||||
|
.post('/SASjsApi/drive/rename', { oldPath, newPath })
|
||||||
|
.then(() => {
|
||||||
|
setSnackbarMessage('Successfully Renamed')
|
||||||
|
setSnackbarSeverity(AlertSeverityType.Success)
|
||||||
|
setOpenSnackbar(true)
|
||||||
|
if (oldPath === selectedFilePath) handleSelect(newPath)
|
||||||
|
else if (selectedFilePath.startsWith(oldPath))
|
||||||
|
handleSelect(selectedFilePath.replace(oldPath, newPath))
|
||||||
|
refreshSideBar()
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setModalTitle('Abort')
|
||||||
|
setModalPayload(
|
||||||
|
typeof err.response.data === 'object'
|
||||||
|
? JSON.stringify(err.response.data)
|
||||||
|
: err.response.data
|
||||||
|
)
|
||||||
|
setOpenModal(true)
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
variant="permanent"
|
||||||
|
sx={{
|
||||||
|
width: drawerWidth,
|
||||||
|
flexShrink: 0,
|
||||||
|
[`& .MuiDrawer-paper`]: { width: drawerWidth, boxSizing: 'border-box' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Backdrop
|
||||||
|
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||||
|
open={isLoading}
|
||||||
|
>
|
||||||
|
<CircularProgress color="inherit" />
|
||||||
|
</Backdrop>
|
||||||
|
<Toolbar />
|
||||||
|
<Box sx={{ overflow: 'auto' }}>
|
||||||
|
{directoryData && (
|
||||||
|
<TreeView
|
||||||
|
node={directoryData}
|
||||||
|
selectedFilePath={selectedFilePath}
|
||||||
|
handleSelect={handleSelect}
|
||||||
|
deleteNode={deleteNode}
|
||||||
|
addFile={addFile}
|
||||||
|
addFolder={addFolder}
|
||||||
|
rename={rename}
|
||||||
|
defaultExpanded={defaultExpanded}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<BootstrapSnackbar
|
||||||
|
open={openSnackbar}
|
||||||
|
setOpen={setOpenSnackbar}
|
||||||
|
message={snackbarMessage}
|
||||||
|
severity={snackbarSeverity}
|
||||||
|
/>
|
||||||
|
<Modal
|
||||||
|
open={openModal}
|
||||||
|
setOpen={setOpenModal}
|
||||||
|
title={modalTitle}
|
||||||
|
payload={modalPayload}
|
||||||
|
/>
|
||||||
|
</Drawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SideBar
|
||||||
@@ -25,3 +25,15 @@ code {
|
|||||||
padding: '5px 10px';
|
padding: '5px 10px';
|
||||||
margin-top: '10px';
|
margin-top: '10px';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tree-item-label {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item-label.selected {
|
||||||
|
background: lightgoldenrodyellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item-label:hover {
|
||||||
|
background: lightgray;
|
||||||
|
}
|
||||||
|
|||||||
2
web/src/utils/hooks/index.ts
Normal file
2
web/src/utils/hooks/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './usePrompt'
|
||||||
|
export * from './useStateWithCallback'
|
||||||
36
web/src/utils/hooks/usePrompt.ts
Normal file
36
web/src/utils/hooks/usePrompt.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { useEffect, useCallback, useContext } from 'react'
|
||||||
|
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom'
|
||||||
|
import { History, Blocker, Transition } from 'history'
|
||||||
|
|
||||||
|
const useBlocker = (blocker: Blocker, when = true) => {
|
||||||
|
const navigator = useContext(NavigationContext).navigator as History
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!when) return
|
||||||
|
|
||||||
|
const unblock = navigator.block((tx: Transition) => {
|
||||||
|
const autoUnblockingTx = {
|
||||||
|
...tx,
|
||||||
|
retry() {
|
||||||
|
unblock()
|
||||||
|
tx.retry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blocker(autoUnblockingTx)
|
||||||
|
})
|
||||||
|
|
||||||
|
return unblock
|
||||||
|
}, [navigator, blocker, when])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePrompt = (message: string, when = true) => {
|
||||||
|
const blocker = useCallback(
|
||||||
|
(tx) => {
|
||||||
|
if (window.confirm(message)) tx.retry()
|
||||||
|
},
|
||||||
|
[message]
|
||||||
|
)
|
||||||
|
|
||||||
|
useBlocker(blocker, when)
|
||||||
|
}
|
||||||
27
web/src/utils/hooks/useStateWithCallback.ts
Normal file
27
web/src/utils/hooks/useStateWithCallback.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { useState, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
export const useStateWithCallback = <T>(
|
||||||
|
initialValue: T
|
||||||
|
): [T, (newValue: T, callback?: () => void) => void] => {
|
||||||
|
const callbackRef = useRef<any>(null)
|
||||||
|
|
||||||
|
const [value, setValue] = useState(initialValue)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof callbackRef.current === 'function') {
|
||||||
|
callbackRef.current()
|
||||||
|
|
||||||
|
callbackRef.current = null
|
||||||
|
}
|
||||||
|
}, [value])
|
||||||
|
|
||||||
|
const setValueWithCallback = (newValue: T, callback?: () => void) => {
|
||||||
|
callbackRef.current = callback
|
||||||
|
|
||||||
|
setValue(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [value, setValueWithCallback]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useStateWithCallback
|
||||||
@@ -30,3 +30,10 @@ export interface RegisterPermissionPayload {
|
|||||||
principalType: string
|
principalType: string
|
||||||
principalId: number
|
principalId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TreeNode {
|
||||||
|
name: string
|
||||||
|
relativePath: string
|
||||||
|
isFolder: boolean
|
||||||
|
children: Array<TreeNode>
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user