diff --git a/web/src/components/header.tsx b/web/src/components/header.tsx index 7778016..a34ae1c 100644 --- a/web/src/components/header.tsx +++ b/web/src/components/header.tsx @@ -2,16 +2,18 @@ import React, { useState, useEffect, useContext } from 'react' import { Link, useNavigate, useLocation } from 'react-router-dom' import { + Box, AppBar, Toolbar, Tabs, Tab, Button, Menu, - MenuItem + MenuItem, + IconButton, + Typography } from '@mui/material' -import OpenInNewIcon from '@mui/icons-material/OpenInNew' -import SettingsIcon from '@mui/icons-material/Settings' +import { OpenInNew, Settings, Menu as MenuIcon } from '@mui/icons-material' import Username from './username' import { AppContext } from '../context/appContext' @@ -30,31 +32,38 @@ const Header = (props: any) => { const [tabValue, setTabValue] = useState( validTabs.includes(pathname) ? pathname : '/' ) - const [anchorEl, setAnchorEl] = useState< - (EventTarget & HTMLButtonElement) | null - >(null) + + const [anchorElNav, setAnchorElNav] = React.useState(null) + const [anchorElUser, setAnchorElUser] = React.useState( + null + ) + + const handleOpenNavMenu = (event: React.MouseEvent) => { + setAnchorElNav(event.currentTarget) + } + const handleOpenUserMenu = (event: React.MouseEvent) => { + setAnchorElUser(event.currentTarget) + } + + const handleCloseNavMenu = () => { + setAnchorElNav(null) + } + + const handleCloseUserMenu = () => { + setAnchorElUser(null) + } useEffect(() => { setTabValue(validTabs.includes(pathname) ? pathname : '/') }, [pathname]) - const handleMenu = ( - event: React.MouseEvent - ) => { - setAnchorEl(event.currentTarget) - } - - const handleClose = () => { - setAnchorEl(null) - } - const handleTabChange = (event: React.SyntheticEvent, value: string) => { setTabValue(value) } const handleLogout = () => { if (appContext.logout) { - handleClose() + handleCloseUserMenu() appContext.logout() } } @@ -64,43 +73,129 @@ const Header = (props: any) => { sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }} > - logo { - setTabValue('/') - navigate('/') - }} - /> - - - + logo { + setTabValue('/') + navigate('/') + }} /> - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + logo { + setTabValue('/') + navigate('/') + }} + /> + +
{ > { vertical: 'top', horizontal: 'center' }} - open={!!anchorEl} - onClose={handleClose} + open={!!anchorElUser} + onClose={handleCloseUserMenu} > + {appContext.loggedIn && ( + + + {appContext.displayName || appContext.username} + + + )} + @@ -147,7 +255,7 @@ const Header = (props: any) => { variant="contained" size="large" color="primary" - endIcon={} + endIcon={} > Docs @@ -160,16 +268,21 @@ const Header = (props: any) => { variant="contained" color="primary" size="large" - endIcon={} + endIcon={} > API - - - + {appContext.loggedIn && ( + + + + )}
diff --git a/web/src/components/username.tsx b/web/src/components/username.tsx index d7c4eca..3d465f5 100644 --- a/web/src/components/username.tsx +++ b/web/src/components/username.tsx @@ -20,7 +20,14 @@ const Username = (props: any) => { ) : ( )} - + {props.username} diff --git a/web/src/containers/Settings/addPermissionModal.tsx b/web/src/containers/Settings/addPermissionModal.tsx index 1e3553e..945ed46 100644 --- a/web/src/containers/Settings/addPermissionModal.tsx +++ b/web/src/containers/Settings/addPermissionModal.tsx @@ -32,7 +32,13 @@ const BootstrapDialog = styled(Dialog)(({ theme }) => ({ type AddPermissionModalProps = { open: boolean handleOpen: Dispatch> - addPermission: (addPermissionPayload: RegisterPermissionPayload) => void + addPermission: ( + permissions: RegisterPermissionPayload[], + permissionType: string, + principalType: string, + principal: string, + permissionSetting: string + ) => void } const AddPermissionModal = ({ @@ -42,9 +48,9 @@ const AddPermissionModal = ({ }: AddPermissionModalProps) => { const [paths, setPaths] = useState([]) const [loadingPaths, setLoadingPaths] = useState(false) - const [path, setPath] = useState() + const [selectedPaths, setSelectedPaths] = useState([]) const [permissionType, setPermissionType] = useState('Route') - const [principalType, setPrincipalType] = useState('group') + const [principalType, setPrincipalType] = useState('Group') const [userPrincipal, setUserPrincipal] = useState() const [groupPrincipal, setGroupPrincipal] = useState() const [permissionSetting, setPermissionSetting] = useState('Grant') @@ -72,10 +78,10 @@ const AddPermissionModal = ({ useEffect(() => { setLoadingPrincipals(true) axios - .get(`/SASjsApi/${principalType}`) + .get(`/SASjsApi/${principalType.toLowerCase()}`) .then((res: any) => { if (res.data) { - if (principalType === 'user') { + if (principalType.toLowerCase() === 'user') { const users: UserResponse[] = res.data const nonAdminUsers = users.filter((user) => !user.isAdmin) setUserPrincipals(nonAdminUsers) @@ -93,22 +99,40 @@ const AddPermissionModal = ({ }, [principalType]) const handleAddPermission = () => { - const addPermissionPayload: any = { - path, - type: permissionType, - setting: permissionSetting, - principalType - } - if (principalType === 'user' && userPrincipal) { - addPermissionPayload.principalId = userPrincipal.id - } else if (principalType === 'group' && groupPrincipal) { - addPermissionPayload.principalId = groupPrincipal.groupId - } - addPermission(addPermissionPayload) + const permissions: RegisterPermissionPayload[] = [] + + selectedPaths.forEach((path) => { + const addPermissionPayload: any = { + path, + type: permissionType, + setting: permissionSetting, + principalType: principalType.toLowerCase(), + principalId: + principalType.toLowerCase() === 'user' + ? userPrincipal?.id + : groupPrincipal?.groupId + } + + permissions.push(addPermissionPayload) + }) + + const principal = + principalType.toLowerCase() === 'user' + ? userPrincipal?.username + : groupPrincipal?.name + + addPermission( + permissions, + permissionType, + principalType, + principal!, + permissionSetting + ) } const addButtonDisabled = - !path || (principalType === 'user' ? !userPrincipal : !groupPrincipal) + !selectedPaths.length || + (principalType.toLowerCase() === 'user' ? !userPrincipal : !groupPrincipal) return ( handleOpen(false)} open={open}> @@ -122,17 +146,15 @@ const AddPermissionModal = ({ setPath(newValue)} - renderInput={(params) => - loadingPaths ? ( - - ) : ( - - ) - } + options={paths} + filterSelectedOptions + value={selectedPaths} + onChange={(event: any, newValue: string[]) => { + setSelectedPaths(newValue) + }} + renderInput={(params) => } /> @@ -154,8 +176,7 @@ const AddPermissionModal = ({ option.toUpperCase()} + options={['Group', 'User']} disableClearable value={principalType} onChange={(event: any, newValue: string) => @@ -167,7 +188,7 @@ const AddPermissionModal = ({ /> - {principalType === 'user' ? ( + {principalType.toLowerCase() === 'user' ? ( option.displayName} diff --git a/web/src/containers/Settings/addPermissionResponseModal.tsx b/web/src/containers/Settings/addPermissionResponseModal.tsx new file mode 100644 index 0000000..4096ebc --- /dev/null +++ b/web/src/containers/Settings/addPermissionResponseModal.tsx @@ -0,0 +1,120 @@ +import React from 'react' + +import { Typography, DialogContent } from '@mui/material' + +import { BootstrapDialog } from '../../components/modal' +import { BootstrapDialogTitle } from '../../components/dialogTitle' +import { PermissionResponse } from '../../utils/types' + +export interface PermissionResponsePayload { + permissionType: string + principalType: string + principal: string + permissionSetting: string + existingPermissions: PermissionResponse[] + newAddedPermissions: PermissionResponse[] + updatedPermissions: PermissionResponse[] + errorPaths: string[] +} + +type Props = { + open: boolean + setOpen: React.Dispatch> + payload: PermissionResponsePayload +} + +const PermissionResponseModal = ({ open, setOpen, payload }: Props) => { + const newAddedPermissionsLength = payload.newAddedPermissions.length + const updatedPermissionsLength = payload.updatedPermissions.length + const existingPermissionsLength = payload.existingPermissions.length + const appliedPermissionsLength = + newAddedPermissionsLength + updatedPermissionsLength + + return ( +
+ setOpen(false)} open={open}> + + Permission Response + + + + {`${appliedPermissionsLength} "${payload.permissionSetting}", "${ + payload.permissionType + }", "${payload.principalType}", "${payload.principal}" ${ + appliedPermissionsLength > 1 ? 'Rules' : 'Rule' + }`}{' '} + Applied: + + + {newAddedPermissionsLength > 0 && ( + <> + + {`${newAddedPermissionsLength} ${ + newAddedPermissionsLength > 1 ? 'Rules' : 'Rule' + }`}{' '} + Added: + +
    + {payload.newAddedPermissions.map((permission, index) => ( +
  • {permission.path}
  • + ))} +
+ + )} + + {updatedPermissionsLength > 0 && ( + <> + + {` ${updatedPermissionsLength} ${ + updatedPermissionsLength > 1 ? 'Rules' : 'Rule' + }`}{' '} + Updated: + +
    + {payload.updatedPermissions.map((permission, index) => ( +
  • {permission.path}
  • + ))} +
+ + )} + + {existingPermissionsLength > 0 && ( + <> + + {`${existingPermissionsLength} ${ + existingPermissionsLength > 1 ? 'Rules' : 'Rule' + }`}{' '} + Unchanged: + +
    + {payload.existingPermissions.map((permission, index) => ( +
  • {permission.path}
  • + ))} +
+ + )} + + {payload.errorPaths.length > 0 && ( + <> + + Errors occurred for following paths: + +
    + {payload.errorPaths.map((path, index) => ( +
  • + {path} +
  • + ))} +
+ + )} +
+
+
+ ) +} + +export default PermissionResponseModal diff --git a/web/src/containers/Settings/index.tsx b/web/src/containers/Settings/index.tsx index a93d400..0032549 100644 --- a/web/src/containers/Settings/index.tsx +++ b/web/src/containers/Settings/index.tsx @@ -31,11 +31,20 @@ const Settings = () => { - + { > {appContext.mode === ModeType.Server && ( - + )} diff --git a/web/src/containers/Settings/permission.tsx b/web/src/containers/Settings/permission.tsx index 3238d69..dc9f3af 100644 --- a/web/src/containers/Settings/permission.tsx +++ b/web/src/containers/Settings/permission.tsx @@ -27,6 +27,9 @@ import { styled } from '@mui/material/styles' import Modal from '../../components/modal' import PermissionFilterModal from './permissionFilterModal' import AddPermissionModal from './addPermissionModal' +import PermissionResponseModal, { + PermissionResponsePayload +} from './addPermissionResponseModal' import UpdatePermissionModal from './updatePermissionModal' import DeleteConfirmationModal from '../../components/deleteConfirmationModal' import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar' @@ -36,12 +39,23 @@ import { PermissionResponse, RegisterPermissionPayload } from '../../utils/types' +import { + findExistingPermission, + findUpdatingPermission +} from '../../utils/helper' + import { AppContext } from '../../context/appContext' const BootstrapTableCell = styled(TableCell)({ textAlign: 'left' }) +const BootstrapGridItem = styled(Grid)({ + '&.MuiGrid-item': { + maxWidth: '100%' + } +}) + export enum PrincipalType { User = 'User', Group = 'Group' @@ -59,6 +73,20 @@ const Permission = () => { AlertSeverityType.Success ) const [addPermissionModalOpen, setAddPermissionModalOpen] = useState(false) + const [openPermissionResponseModal, setOpenPermissionResponseModal] = + useState(false) + const [permissionResponsePayload, setPermissionResponsePayload] = + useState({ + permissionType: '', + principalType: '', + principal: '', + permissionSetting: '', + existingPermissions: [], + newAddedPermissions: [], + updatedPermissions: [], + errorPaths: [] + }) + const [updatePermissionModalOpen, setUpdatePermissionModalOpen] = useState(false) const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] = @@ -181,29 +209,77 @@ const Permission = () => { setFilterApplied(false) } - const addPermission = (addPermissionPayload: RegisterPermissionPayload) => { + const addPermission = async ( + permissionsToAdd: RegisterPermissionPayload[], + permissionType: string, + principalType: string, + principal: string, + permissionSetting: string + ) => { setAddPermissionModalOpen(false) setIsLoading(true) - axios - .post('/SASjsApi/permission', addPermissionPayload) - .then((res: any) => { - fetchPermissions() - setSnackbarMessage('Permission added!') - 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 newAddedPermissions: PermissionResponse[] = [] + const updatedPermissions: PermissionResponse[] = [] + const errorPaths: string[] = [] + + const existingPermissions: PermissionResponse[] = [] + const updatingPermissions: PermissionResponse[] = [] + const newPermissions: RegisterPermissionPayload[] = [] + + permissionsToAdd.forEach((permission) => { + const existingPermission = findExistingPermission(permissions, permission) + if (existingPermission) { + existingPermissions.push(existingPermission) + return + } + + const updatingPermission = findUpdatingPermission(permissions, permission) + if (updatingPermission) { + updatingPermissions.push(updatingPermission) + return + } + + newPermissions.push(permission) + }) + + for (const permission of newPermissions) { + await axios + .post('/SASjsApi/permission', permission) + .then((res) => { + newAddedPermissions.push(res.data) + }) + .catch((error) => { + errorPaths.push(permission.path) + }) + } + + for (const permission of updatingPermissions) { + await axios + .patch(`/SASjsApi/permission/${permission.permissionId}`, { + setting: permission.setting === 'Grant' ? 'Deny' : 'Grant' + }) + .then((res) => { + updatedPermissions.push(res.data) + }) + .catch((error) => { + errorPaths.push(permission.path) + }) + } + + fetchPermissions() + setIsLoading(false) + setPermissionResponsePayload({ + permissionType, + principalType, + principal, + permissionSetting, + existingPermissions, + updatedPermissions, + newAddedPermissions, + errorPaths + }) + setOpenPermissionResponseModal(true) } const handleUpdatePermissionClick = (permission: PermissionResponse) => { @@ -280,11 +356,11 @@ const Permission = () => { ) : ( - + - - setFilterModalOpen(true)} /> + setFilterModalOpen(true)}> + {appContext.isAdmin && ( @@ -299,14 +375,14 @@ const Permission = () => { )} - - + + - + { handleOpen={setAddPermissionModalOpen} addPermission={addPermission} /> + { Group Members - {group.users.map((user) => ( - + {group.users.map((user, index) => ( + {user.username} ))} diff --git a/web/src/containers/Settings/permissionFilterModal.tsx b/web/src/containers/Settings/permissionFilterModal.tsx index 44e104e..7fe64fb 100644 --- a/web/src/containers/Settings/permissionFilterModal.tsx +++ b/web/src/containers/Settings/permissionFilterModal.tsx @@ -92,7 +92,7 @@ const PermissionFilterModal = ({ onChange={(event: any, newValue: string[]) => { setPathFilter(newValue) }} - renderInput={(params) => } + renderInput={(params) => } />
diff --git a/web/src/containers/Studio/editor.tsx b/web/src/containers/Studio/editor.tsx index cc8f76e..f0ba49a 100644 --- a/web/src/containers/Studio/editor.tsx +++ b/web/src/containers/Studio/editor.tsx @@ -353,9 +353,7 @@ const SASjsEditor = ({ sx={{ borderBottom: 1, borderColor: 'divider', - position: 'fixed', - background: 'white', - width: '85%' + background: 'white' }} > @@ -372,10 +370,7 @@ const SASjsEditor = ({ - + -
+

SAS Log

{log}
-
+
{webout}
diff --git a/web/src/containers/Studio/sideBar.tsx b/web/src/containers/Studio/sideBar.tsx index 65af0bc..6f53393 100644 --- a/web/src/containers/Studio/sideBar.tsx +++ b/web/src/containers/Studio/sideBar.tsx @@ -1,6 +1,15 @@ import React, { useState, useMemo } from 'react' import axios from 'axios' -import { Backdrop, Box, CircularProgress, Drawer, Toolbar } from '@mui/material' +import { + Backdrop, + Box, + Paper, + CircularProgress, + Drawer, + Toolbar, + IconButton +} from '@mui/material' +import { FolderOpen } from '@mui/icons-material' import TreeView from '../../components/tree' import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar' @@ -33,6 +42,17 @@ const SideBar = ({ const [snackbarSeverity, setSnackbarSeverity] = useState( AlertSeverityType.Success ) + const [mobileOpen, setMobileOpen] = React.useState(false) + + const handleDrawerToggle = () => { + setMobileOpen(!mobileOpen) + } + + const handleFileSelect = (filePath: string) => { + setMobileOpen(false) + handleSelect(filePath) + } + const defaultExpanded = useMemo(() => { const splittedPath = selectedFilePath.split('/') const arr = [''] @@ -147,15 +167,8 @@ const SideBar = ({ .finally(() => setIsLoading(false)) } - return ( - + const drawer = ( +
theme.zIndex.drawer + 1 }} open={isLoading} @@ -168,7 +181,7 @@ const SideBar = ({ - +
+ ) + + return ( + <> + + + + + + + {drawer} + + + {drawer} + + ) } diff --git a/web/src/utils/helper.ts b/web/src/utils/helper.ts new file mode 100644 index 0000000..27f6ae9 --- /dev/null +++ b/web/src/utils/helper.ts @@ -0,0 +1,59 @@ +import { PermissionResponse, RegisterPermissionPayload } from './types' + +export const findExistingPermission = ( + existingPermissions: PermissionResponse[], + newPermission: RegisterPermissionPayload +) => { + for (const permission of existingPermissions) { + if ( + permission.user?.id === newPermission.principalId && + hasSameCombination(permission, newPermission) + ) + return permission + + if ( + permission.group?.groupId === newPermission.principalId && + hasSameCombination(permission, newPermission) + ) + return permission + } + + return null +} + +export const findUpdatingPermission = ( + existingPermissions: PermissionResponse[], + newPermission: RegisterPermissionPayload +) => { + for (const permission of existingPermissions) { + if ( + permission.user?.id === newPermission.principalId && + hasDifferentSetting(permission, newPermission) + ) + return permission + + if ( + permission.group?.groupId === newPermission.principalId && + hasDifferentSetting(permission, newPermission) + ) + return permission + } + + return null +} + +const hasSameCombination = ( + existingPermission: PermissionResponse, + newPermission: RegisterPermissionPayload +) => + existingPermission.path === newPermission.path && + existingPermission.type === newPermission.type && + existingPermission.setting === newPermission.setting + +const hasDifferentSetting = ( + existingPermission: PermissionResponse, + newPermission: RegisterPermissionPayload +) => + existingPermission.path === newPermission.path && + existingPermission.type === newPermission.type && + existingPermission.setting !== newPermission.setting