mirror of
https://github.com/sasjs/server.git
synced 2025-12-11 19:44:35 +00:00
feat: add basic UI for settings and permissions
This commit is contained in:
@@ -8,6 +8,7 @@ import Header from './components/header'
|
|||||||
import Home from './components/home'
|
import Home from './components/home'
|
||||||
import Drive from './containers/Drive'
|
import Drive from './containers/Drive'
|
||||||
import Studio from './containers/Studio'
|
import Studio from './containers/Studio'
|
||||||
|
import Settings from './containers/Settings'
|
||||||
|
|
||||||
import { AppContext } from './context/appContext'
|
import { AppContext } from './context/appContext'
|
||||||
|
|
||||||
@@ -46,6 +47,9 @@ function App() {
|
|||||||
<Route exact path="/SASjsStudio">
|
<Route exact path="/SASjsStudio">
|
||||||
<Studio />
|
<Studio />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route exact path="/SASjsSettings">
|
||||||
|
<Settings />
|
||||||
|
</Route>
|
||||||
<Route exact path="/SASjsLogon">
|
<Route exact path="/SASjsLogon">
|
||||||
<Login getCodeOnly />
|
<Login getCodeOnly />
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
35
web/src/components/dialogTitle.tsx
Normal file
35
web/src/components/dialogTitle.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React, { Dispatch, SetStateAction } from 'react'
|
||||||
|
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
|
import IconButton from '@mui/material/IconButton'
|
||||||
|
import CloseIcon from '@mui/icons-material/Close'
|
||||||
|
|
||||||
|
export interface DialogTitleProps {
|
||||||
|
id: string
|
||||||
|
children?: React.ReactNode
|
||||||
|
onClose: Dispatch<SetStateAction<boolean>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BootstrapDialogTitle = (props: DialogTitleProps) => {
|
||||||
|
const { children, onClose, ...other } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogTitle sx={{ m: 0, p: 2 }} {...other}>
|
||||||
|
{children}
|
||||||
|
{onClose ? (
|
||||||
|
<IconButton
|
||||||
|
aria-label="close"
|
||||||
|
onClick={() => onClose(false)}
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: 8,
|
||||||
|
top: 8,
|
||||||
|
color: (theme) => theme.palette.grey[500]
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
) : null}
|
||||||
|
</DialogTitle>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useContext } from 'react'
|
import React, { useState, useEffect, useContext } from 'react'
|
||||||
import { Link, useHistory, useLocation } from 'react-router-dom'
|
import { Link, useHistory, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
MenuItem
|
MenuItem
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
||||||
|
import SettingsIcon from '@mui/icons-material/Settings'
|
||||||
|
|
||||||
import Username from './username'
|
import Username from './username'
|
||||||
import { AppContext } from '../context/appContext'
|
import { AppContext } from '../context/appContext'
|
||||||
@@ -29,6 +30,10 @@ const Header = (props: any) => {
|
|||||||
(EventTarget & HTMLButtonElement) | null
|
(EventTarget & HTMLButtonElement) | null
|
||||||
>(null)
|
>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTabValue(pathname)
|
||||||
|
}, [pathname])
|
||||||
|
|
||||||
const handleMenu = (
|
const handleMenu = (
|
||||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
) => {
|
) => {
|
||||||
@@ -132,6 +137,17 @@ const Header = (props: any) => {
|
|||||||
open={!!anchorEl}
|
open={!!anchorEl}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
>
|
>
|
||||||
|
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to="/SASjsSettings"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<SettingsIcon />}
|
||||||
|
>
|
||||||
|
Setting
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
<MenuItem onClick={handleLogout} sx={{ justifyContent: 'center' }}>
|
<MenuItem onClick={handleLogout} sx={{ justifyContent: 'center' }}>
|
||||||
<Button variant="contained" color="primary">
|
<Button variant="contained" color="primary">
|
||||||
Logout
|
Logout
|
||||||
|
|||||||
57
web/src/containers/Settings/index.tsx
Normal file
57
web/src/containers/Settings/index.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import { Box, Paper, Tab, styled } from '@mui/material'
|
||||||
|
import TabContext from '@mui/lab/TabContext'
|
||||||
|
import TabList from '@mui/lab/TabList'
|
||||||
|
import TabPanel from '@mui/lab/TabPanel'
|
||||||
|
|
||||||
|
import Permission from './permission'
|
||||||
|
|
||||||
|
const StyledTab = styled(Tab)({
|
||||||
|
background: 'black',
|
||||||
|
margin: '0 5px 5px 0'
|
||||||
|
})
|
||||||
|
|
||||||
|
const StyledTabpanel = styled(TabPanel)({
|
||||||
|
flexGrow: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const Settings = () => {
|
||||||
|
const [value, setValue] = React.useState('profile')
|
||||||
|
|
||||||
|
const handleChange = (event: React.SyntheticEvent, newValue: string) => {
|
||||||
|
setValue(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
marginTop: '65px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TabContext value={value}>
|
||||||
|
<Box component={Paper} sx={{ margin: '0 5px', height: '92vh' }}>
|
||||||
|
<TabList
|
||||||
|
TabIndicatorProps={{
|
||||||
|
style: {
|
||||||
|
display: 'none'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
orientation="vertical"
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
<StyledTab label="Profile" value="profile" />
|
||||||
|
<StyledTab label="Permission" value="permission" />
|
||||||
|
</TabList>
|
||||||
|
</Box>
|
||||||
|
<StyledTabpanel value="profile">Profile Page</StyledTabpanel>
|
||||||
|
<StyledTabpanel value="permission">
|
||||||
|
<Permission />
|
||||||
|
</StyledTabpanel>
|
||||||
|
</TabContext>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Settings
|
||||||
318
web/src/containers/Settings/permission.tsx
Normal file
318
web/src/containers/Settings/permission.tsx
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
import React, { useState, useEffect, Dispatch, SetStateAction } from 'react'
|
||||||
|
import axios from 'axios'
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Paper,
|
||||||
|
Grid,
|
||||||
|
CircularProgress,
|
||||||
|
IconButton,
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
TextField
|
||||||
|
} from '@mui/material'
|
||||||
|
import Autocomplete from '@mui/material/Autocomplete'
|
||||||
|
|
||||||
|
import FilterListIcon from '@mui/icons-material/FilterList'
|
||||||
|
|
||||||
|
import { styled } from '@mui/material/styles'
|
||||||
|
|
||||||
|
import { BootstrapDialogTitle } from '../../components/dialogTitle'
|
||||||
|
|
||||||
|
interface UserResponse {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
displayName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GroupResponse {
|
||||||
|
groupId: number
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PermissionResponse {
|
||||||
|
permissionId: number
|
||||||
|
uri: string
|
||||||
|
setting: string
|
||||||
|
user?: UserResponse
|
||||||
|
group?: GroupResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
const BootstrapTableCell = styled(TableCell)({
|
||||||
|
textAlign: 'left'
|
||||||
|
})
|
||||||
|
|
||||||
|
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
||||||
|
'& .MuiDialogContent-root': {
|
||||||
|
padding: theme.spacing(2)
|
||||||
|
},
|
||||||
|
'& .MuiDialogActions-root': {
|
||||||
|
padding: theme.spacing(1)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const Permission = () => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [filterModalOpen, setFilterModalOpen] = useState(false)
|
||||||
|
const [uriFilter, setUriFilter] = useState<string[]>([])
|
||||||
|
const [principalFilter, setPrincipalFilter] = useState<string[]>([])
|
||||||
|
const [settingFilter, setSettingFilter] = useState<string[]>([])
|
||||||
|
const [permissions, setPermissions] = useState<PermissionResponse[]>([])
|
||||||
|
const [filteredPermissions, setFilteredPermissions] = useState<
|
||||||
|
PermissionResponse[]
|
||||||
|
>([])
|
||||||
|
const [filterApplied, setFilterApplied] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoading(true)
|
||||||
|
axios
|
||||||
|
.get(`/SASjsApi/permission`)
|
||||||
|
.then((res: any) => {
|
||||||
|
if (res.data?.length > 0) {
|
||||||
|
setPermissions(res.data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* first find the permissions w.r.t each filter type
|
||||||
|
* take intersection of resultant arrays
|
||||||
|
*/
|
||||||
|
const applyFilter = () => {
|
||||||
|
const uriFilteredPermissions =
|
||||||
|
uriFilter.length > 0
|
||||||
|
? permissions.filter((permission) => uriFilter.includes(permission.uri))
|
||||||
|
: permissions
|
||||||
|
const principalFilteredPermissions =
|
||||||
|
principalFilter.length > 0
|
||||||
|
? permissions.filter((permission) => {
|
||||||
|
if (permission.user) {
|
||||||
|
return principalFilter.includes(permission.user.displayName)
|
||||||
|
} else if (permission.group) {
|
||||||
|
return principalFilter.includes(permission.group.name)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
: permissions
|
||||||
|
const settingFilteredPermissions =
|
||||||
|
settingFilter.length > 0
|
||||||
|
? permissions.filter((permission) =>
|
||||||
|
settingFilter.includes(permission.setting)
|
||||||
|
)
|
||||||
|
: permissions
|
||||||
|
|
||||||
|
let filteredArray = uriFilteredPermissions.filter((permission) =>
|
||||||
|
principalFilteredPermissions.some(
|
||||||
|
(item) => item.permissionId === permission.permissionId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
filteredArray = filteredArray.filter((permission) =>
|
||||||
|
settingFilteredPermissions.some(
|
||||||
|
(item) => item.permissionId === permission.permissionId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
setFilteredPermissions(filteredArray)
|
||||||
|
setFilterApplied(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetFilter = () => {
|
||||||
|
setUriFilter([])
|
||||||
|
setPrincipalFilter([])
|
||||||
|
setSettingFilter([])
|
||||||
|
setFilteredPermissions([])
|
||||||
|
setFilterApplied(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return isLoading ? (
|
||||||
|
<CircularProgress
|
||||||
|
style={{ position: 'absolute', left: '50%', top: '50%' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Box className="permissions-page">
|
||||||
|
<Grid container direction="column" spacing={1}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Paper elevation={3}>
|
||||||
|
<IconButton>
|
||||||
|
<FilterListIcon onClick={() => setFilterModalOpen(true)} />
|
||||||
|
</IconButton>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<PermissionTable
|
||||||
|
permissions={filterApplied ? filteredPermissions : permissions}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<FilterModal
|
||||||
|
open={filterModalOpen}
|
||||||
|
handleClose={setFilterModalOpen}
|
||||||
|
permissions={permissions}
|
||||||
|
uriFilter={uriFilter}
|
||||||
|
setUriFilter={setUriFilter}
|
||||||
|
principalFilter={principalFilter}
|
||||||
|
setPrincipalFilter={setPrincipalFilter}
|
||||||
|
settingFilter={settingFilter}
|
||||||
|
setSettingFilter={setSettingFilter}
|
||||||
|
applyFilter={applyFilter}
|
||||||
|
resetFilter={resetFilter}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Permission
|
||||||
|
|
||||||
|
type PermissionTableProps = {
|
||||||
|
permissions: PermissionResponse[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const PermissionTable = ({ permissions }: PermissionTableProps) => {
|
||||||
|
return (
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<Table sx={{ minWidth: 650 }}>
|
||||||
|
<TableHead sx={{ background: 'rgb(0,0,0, 0.3)' }}>
|
||||||
|
<TableRow>
|
||||||
|
<BootstrapTableCell>Uri</BootstrapTableCell>
|
||||||
|
<BootstrapTableCell>Principal</BootstrapTableCell>
|
||||||
|
<BootstrapTableCell>Setting</BootstrapTableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{permissions.map((permission) => (
|
||||||
|
<TableRow>
|
||||||
|
<BootstrapTableCell>{permission.uri}</BootstrapTableCell>
|
||||||
|
<BootstrapTableCell>
|
||||||
|
{displayPrincipal(permission)}
|
||||||
|
</BootstrapTableCell>
|
||||||
|
<BootstrapTableCell>{permission.setting}</BootstrapTableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayPrincipal = (permission: PermissionResponse) => {
|
||||||
|
if (permission.user) {
|
||||||
|
return permission.user?.displayName
|
||||||
|
} else if (permission.group) {
|
||||||
|
return permission.group?.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterModalProps = {
|
||||||
|
open: boolean
|
||||||
|
handleClose: Dispatch<SetStateAction<boolean>>
|
||||||
|
permissions: PermissionResponse[]
|
||||||
|
uriFilter: string[]
|
||||||
|
setUriFilter: Dispatch<SetStateAction<string[]>>
|
||||||
|
principalFilter: string[]
|
||||||
|
setPrincipalFilter: Dispatch<SetStateAction<string[]>>
|
||||||
|
settingFilter: string[]
|
||||||
|
setSettingFilter: Dispatch<SetStateAction<string[]>>
|
||||||
|
applyFilter: () => void
|
||||||
|
resetFilter: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const FilterModal = ({
|
||||||
|
open,
|
||||||
|
handleClose,
|
||||||
|
permissions,
|
||||||
|
uriFilter,
|
||||||
|
setUriFilter,
|
||||||
|
principalFilter,
|
||||||
|
setPrincipalFilter,
|
||||||
|
settingFilter,
|
||||||
|
setSettingFilter,
|
||||||
|
applyFilter,
|
||||||
|
resetFilter
|
||||||
|
}: FilterModalProps) => {
|
||||||
|
const URIs = permissions.map((permission) => permission.uri)
|
||||||
|
const principals = permissions
|
||||||
|
.map((permission) => {
|
||||||
|
if (permission.user) return permission.user.displayName
|
||||||
|
if (permission.group) return permission.group.name
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
.filter((principal) => principal !== '')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BootstrapDialog onClose={handleClose} open={open}>
|
||||||
|
<BootstrapDialogTitle
|
||||||
|
id="permission-filter-dialog-title"
|
||||||
|
onClose={handleClose}
|
||||||
|
>
|
||||||
|
Permission Filter
|
||||||
|
</BootstrapDialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<Grid container spacing={1}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Autocomplete
|
||||||
|
multiple
|
||||||
|
options={URIs}
|
||||||
|
filterSelectedOptions
|
||||||
|
value={uriFilter}
|
||||||
|
onChange={(event: any, newValue: string[]) => {
|
||||||
|
setUriFilter(newValue)
|
||||||
|
}}
|
||||||
|
renderInput={(params) => <TextField {...params} label="URIs" />}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Autocomplete
|
||||||
|
multiple
|
||||||
|
options={principals}
|
||||||
|
filterSelectedOptions
|
||||||
|
value={principalFilter}
|
||||||
|
onChange={(event: any, newValue: string[]) => {
|
||||||
|
setPrincipalFilter(newValue)
|
||||||
|
}}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField {...params} label="Principals" />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Autocomplete
|
||||||
|
multiple
|
||||||
|
options={['Grant', 'Deny']}
|
||||||
|
filterSelectedOptions
|
||||||
|
value={settingFilter}
|
||||||
|
onChange={(event: any, newValue: string[]) => {
|
||||||
|
setSettingFilter(newValue)
|
||||||
|
}}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField {...params} label="Settings" />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="outlined" color="error" onClick={resetFilter}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button variant="outlined" onClick={applyFilter}>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</BootstrapDialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -18,3 +18,10 @@ code {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.permissions-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: '5px 10px';
|
||||||
|
margin-top: '10px';
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user