diff --git a/frontend/src/@types/index.ts b/frontend/src/@types/index.ts new file mode 100644 index 000000000..58c4e6530 --- /dev/null +++ b/frontend/src/@types/index.ts @@ -0,0 +1,12 @@ +export const enum ROLE { + ADMIN = 1, + MANAGER = 10, + OPERATOR = 20, + GUEST_OPERATOR = 30 +} + +export const enum SHARE { + NOSHARE = 0, + ORGANIZATION = 2, + USERS = 1, +} \ No newline at end of file diff --git a/frontend/src/api/Workspace/index.ts b/frontend/src/api/Workspace/index.ts index f0492e756..a5315fc71 100644 --- a/frontend/src/api/Workspace/index.ts +++ b/frontend/src/api/Workspace/index.ts @@ -1,6 +1,7 @@ import axios from 'utils/axios' import qs from 'qs' import { ItemsWorkspace, WorkspaceDataDTO } from 'store/slice/Workspace/WorkspaceType' +import { ListShareDTO } from 'store/slice/Database/DatabaseType'; export type WorkspacePostDataDTO = { name: string; id?: number } @@ -40,3 +41,13 @@ export const exportWorkspaceApi = async (id: number): Promise => { const response = await axios.get(`/workspace/export/${id}`) return response.data } + +export const getListUserShareWorkspaceApi = async (id: number): Promise => { + const response = await axios.get(`/workspace/share/${id}/status`) + return response.data +} + +export const postListUserShareWorkspaceApi = async (id: number, data: {user_ids: number[]}): Promise => { + const response = await axios.post(`/workspace/share/${id}/status`, data) + return response.data +} diff --git a/frontend/src/api/database/index.ts b/frontend/src/api/database/index.ts index a8ab22f65..ab567fc8c 100644 --- a/frontend/src/api/database/index.ts +++ b/frontend/src/api/database/index.ts @@ -1,32 +1,42 @@ -import { DatabaseParams } from 'store/slice/Database/DatabaseType' +import { DatabaseDTO, DatabaseParams, ListShareDTO } from 'store/slice/Database/DatabaseType' import axios from 'utils/axios' import qs from 'qs' -export const getExperimentsPublicApi = async (params: DatabaseParams) => { +export const getExperimentsPublicApi = async (params: DatabaseParams): Promise => { const paramsNew = qs.stringify(params, { indices: false }) const response = await axios.get(`/public/experiments?${paramsNew}`) return response.data } -export const getCellsPublicApi = async (params: DatabaseParams) => { +export const getCellsPublicApi = async (params: DatabaseParams): Promise => { const paramsNew = qs.stringify(params, { indices: false }) const response = await axios.get(`/public/cells?${paramsNew}`) return response.data } -export const getExperimentsApi = async (params: DatabaseParams) => { +export const getExperimentsApi = async (params: DatabaseParams): Promise => { const paramsNew = qs.stringify(params, { indices: false }) const response = await axios.get(`/expdb/experiments?${paramsNew}`) return response.data } -export const getCellsApi = async (params: DatabaseParams) => { +export const getCellsApi = async (params: DatabaseParams): Promise => { const paramsNew = qs.stringify(params, { indices: false }) const response = await axios.get(`/expdb/cells?${paramsNew}`) return response.data } -export const postPublistApi = async (id: number, status: 'on' | 'off') => { +export const postPublistApi = async (id: number, status: 'on' | 'off'): Promise => { const response = await axios.post(`/expdb/experiment/publish/${id}/${status}`) return response.data } + +export const getListUserShareApi = async (id: number): Promise => { + const response = await axios.get(`/expdb/share/${id}/status`) + return response.data +} + +export const postListUserShareApi = async (id: number, data: {share_type: number; user_ids: number[]}): Promise => { + const response = await axios.post(`/expdb/share/${id}/status`, data) + return response.data +} diff --git a/frontend/src/api/users/UsersAdmin.ts b/frontend/src/api/users/UsersAdmin.ts index 2dd65f3ff..d30391da2 100644 --- a/frontend/src/api/users/UsersAdmin.ts +++ b/frontend/src/api/users/UsersAdmin.ts @@ -36,3 +36,8 @@ export const deleteUserApi = async (uid: string): Promise => { const response = await axios.delete(`/admin/users/${uid}`) return response.data } + +export const getListSearchApi = async (data: {keyword: string | null}): Promise => { + const response = await axios.get(`/users/search/share_users${data.keyword ? `?keyword=${data.keyword}` : ''}`) + return response.data +} diff --git a/frontend/src/api/users/UsersApiDTO.ts b/frontend/src/api/users/UsersApiDTO.ts index 416a90d83..13c0d24cd 100644 --- a/frontend/src/api/users/UsersApiDTO.ts +++ b/frontend/src/api/users/UsersApiDTO.ts @@ -1,7 +1,12 @@ export type UserDTO = { - id: number - uid: string + uid?: string email: string + id?: number + name?: string + organization_id?: number + role_id?: number + create_at?: string + update_at?: string } export type AddUserDTO = { diff --git a/frontend/src/components/Database/DatabaseCells.tsx b/frontend/src/components/Database/DatabaseCells.tsx index 229a905ad..31a4a623d 100644 --- a/frontend/src/components/Database/DatabaseCells.tsx +++ b/frontend/src/components/Database/DatabaseCells.tsx @@ -110,9 +110,8 @@ const columns = (handleOpenDialog: (value: ImageUrls[], expId?: string) => void) width={'100%'} height={'100%'} /> - - ) - }, + + )} }, ] diff --git a/frontend/src/components/Database/DatabaseExperiments.tsx b/frontend/src/components/Database/DatabaseExperiments.tsx index 21b3ea247..a4a6509c3 100644 --- a/frontend/src/components/Database/DatabaseExperiments.tsx +++ b/frontend/src/components/Database/DatabaseExperiments.tsx @@ -1,4 +1,4 @@ -import { Box, DialogTitle, FormControl, FormControlLabel, Input, Pagination, Radio, RadioGroup, styled } from '@mui/material' +import { Box, Input, Pagination, styled } from '@mui/material' import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react' import { useNavigate, useSearchParams } from 'react-router-dom' import ContentPasteSearchIcon from '@mui/icons-material/ContentPasteSearch'; @@ -7,7 +7,6 @@ import Dialog from '@mui/material/Dialog' import DialogActions from '@mui/material/DialogActions' import DialogContent from '@mui/material/DialogContent' import DialogContentText from '@mui/material/DialogContentText' -import { DataGrid, GridRenderCellParams, GridRowParams } from '@mui/x-data-grid' import DialogImage from '../common/DialogImage' import SwitchCustom from '../common/SwitchCustom' import { @@ -28,11 +27,15 @@ import { RootState } from '../../store/store' import { getExperimentsDatabase, getExperimentsPublicDatabase, + getListUserShare, postPublist, } from '../../store/slice/Database/DatabaseActions' import Loading from 'components/common/Loading' import { TypeData } from 'store/slice/Database/DatabaseSlice' -import CancelIcon from '@mui/icons-material/Cancel' +import { UserDTO } from 'api/users/UsersApiDTO' +import { isAdminOrManager } from 'store/slice/User/UserSelector' +import { SHARE } from '@types' +import PopupShare from '../PopupShare' export type Data = { id: number @@ -61,13 +64,8 @@ type PopupAttributesProps = { exp_id?: string } -type PopupType = { - open: boolean - handleClose: () => void -} - type DatabaseProps = { - user?: Object + user?: UserDTO cellPath: string } @@ -193,145 +191,6 @@ const columns = ( }, ] -const columnsShare = (handleShareFalse: (parmas: GridRenderCellParams) => void) => [ - { - field: "name", - headerName: "Name", - minWidth: 140, - renderCell: (params: GridRenderCellParams) => ( - {params.row.name} - ), - }, - { - field: "lab", - headerName: "Lab", - minWidth: 280, - renderCell: (params: GridRenderCellParams) => ( - {params.row.email} - ), - }, - { - field: "email", - headerName: "Email", - minWidth: 280, - renderCell: (params: GridRenderCellParams) => ( - {params.row.email} - ), - }, - { - field: "share", - headerName: "", - minWidth: 130, - renderCell: (params: GridRenderCellParams) => { - if(!params.row.share) return "" - return ( - - ) - } - }, -] - -const dataShare = [ - { - id: 1, - name: "User 1", - lab: "Labxxxx", - email: "aaaaa@gmail.com", - share: false - }, - { - id: 2, - name: "User 2", - lab: "Labxxxx", - email: "aaaaa@gmail.com", - share: true - }, - { - id: 3, - name: "User 3", - lab: "Labxxxx", - email: "aaaaa@gmail.com", - share: true - } -] - -const PopupShare = ({open, handleClose}: PopupType) => { - const [value, setValue] = useState("Organization") - const [tableShare, setTableShare] = useState(dataShare) - - const handleShareTrue = (params: GridRowParams) => { - if(params.row.share) return - const index = tableShare.findIndex(item => item.id === params.id) - setTableShare(pre => { - pre[index].share = true - return pre - }) - } - - const handleShareFalse = (params: GridRenderCellParams) => { - const indexSearch = tableShare.findIndex(item => item.id === params.id) - const newData = tableShare.map((item, index) => { - if(index === indexSearch) return {...item, share: false} - return item - }) - setTableShare(newData) - } - - const handleValue = (event: ChangeEvent) => { - setValue((event.target as HTMLInputElement).value); - } - - if(!open) return null; - - return ( - - - Share Database record - Experiment ID: XXXXXX - - - - } label={"Share for Organization"} /> - } label={"Share for Users"} /> - - - - - { - value !== "Organization" ? - <> -

Permitted users

- - - : null - } -
- - - - -
-
- ) -} - const PopupAttributes = ({ data, open, @@ -367,13 +226,22 @@ const PopupAttributes = ({ ) } const DatabaseExperiments = ({ user, cellPath }: DatabaseProps) => { + const type: keyof TypeData = user ? 'private' : 'public' + const adminOrManager = useSelector(isAdminOrManager) + const { data: dataExperiments, loading } = useSelector( + (state: RootState) => ({ + data: state[DATABASE_SLICE_NAME].data[type], + loading: state[DATABASE_SLICE_NAME].loading, + }), + ) - const [openShare, setOpenShare] = useState(false) + const [openShare, setOpenShare] = useState<{open: boolean, id?: number}>({open: false}) const [dataDialog, setDataDialog] = useState<{ - type: string + type?: string data?: string | string[] expId?: string nameCol?: string + shareType?: number }>({ type: '', data: undefined, @@ -383,12 +251,10 @@ const DatabaseExperiments = ({ user, cellPath }: DatabaseProps) => { const dispatch = useDispatch() const navigate = useNavigate() - const type: keyof TypeData = user ? 'private' : 'public' - const { data: dataExperiments, loading } = useSelector( - (state: RootState) => ({ - data: state[DATABASE_SLICE_NAME].data[type], - loading: state[DATABASE_SLICE_NAME].loading, - }), + const { dataShare } = useSelector( + (state: RootState) => ({ + dataShare: state[DATABASE_SLICE_NAME].listShare, + }), ) const pagiFilter = useCallback( @@ -434,6 +300,12 @@ const DatabaseExperiments = ({ user, cellPath }: DatabaseProps) => { //eslint-disable-next-line }, [dataParams, user, dataParamsFilter]) + useEffect(() => { + if(!openShare.open || !openShare.id) return + dispatch(getListUserShare({id: openShare.id})) + //eslint-disable-next-line + }, [openShare]) + const handleOpenDialog = (data: ImageUrls[] | ImageUrls, expId?: string, graphTitle?: string) => { let newData: string | (string[]) = [] if(Array.isArray(data)) { @@ -454,8 +326,9 @@ const DatabaseExperiments = ({ user, cellPath }: DatabaseProps) => { setDataDialog((pre) => ({ ...pre, data: event.target.value })) } - const handleOpenShare = () => { - setOpenShare(true) + const handleOpenShare = (expId?: string, value?: number, id?: number) => { + setDataDialog({expId: expId, shareType: value}) + setOpenShare({open: true, id: id}) } const getParamsData = () => { @@ -553,11 +426,17 @@ const DatabaseExperiments = ({ user, cellPath }: DatabaseProps) => { width: 160, sortable: false, filterable: false, - renderCell: (params: { row: DatabaseType }) => ( - - - - ), + renderCell: (params: { value: number, row: DatabaseType }) => { + const { value, row } = params + return ( + handleOpenShare(row.experiment_id, value, row.id)} + > + + + ) + } }, { field: 'publish_status', @@ -592,7 +471,7 @@ const DatabaseExperiments = ({ user, cellPath }: DatabaseProps) => { { data={dataDialog.data} open={dataDialog.type === 'attribute'} handleClose={handleCloseDialog} - role={!!user} + role={!!adminOrManager} /> {loading ? : null} - setOpenShare(false)} - /> + {openShare.open && openShare.id ? + { + if(isSubmit) fetchApi(); + setOpenShare({...openShare, open: false})} + } + /> : null + } ) } @@ -682,13 +569,4 @@ const Content = styled('textarea')(() => ({ height: 'fit-content', })) -const DialogCustom = styled(Dialog)(({ theme }) => ({ - "& .MuiDialog-container": { - "& .MuiPaper-root": { - width: "70%", - maxWidth: "890px", - }, - }, -})) - export default DatabaseExperiments diff --git a/frontend/src/components/PopupShare.tsx b/frontend/src/components/PopupShare.tsx new file mode 100644 index 000000000..72bdec3ce --- /dev/null +++ b/frontend/src/components/PopupShare.tsx @@ -0,0 +1,315 @@ +import { + Box, Button, + Dialog, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, Input, Radio, RadioGroup, styled +} from "@mui/material"; +import { DataGrid, GridRenderCellParams } from "@mui/x-data-grid"; +import { SHARE } from "../@types"; +import { ChangeEvent, MouseEvent as MouseEventReact, useCallback, useEffect, useRef, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { postListUserShare } from "../store/slice/Database/DatabaseActions"; +import CancelIcon from '@mui/icons-material/Cancel' +import { postListUserShareWorkspaces } from "store/slice/Workspace/WorkspacesActions"; +import { selectListSearch, selectListSearchLoading } from "../store/slice/User/UserSelector"; +import { getListSearch } from "../store/slice/User/UserActions"; +import Loading from "./common/Loading"; +import { UserDTO } from "../api/users/UsersApiDTO"; +import CheckIcon from '@mui/icons-material/Check'; +import { resetUserSearch } from "../store/slice/User/UserSlice"; + +type PopupType = { + open: boolean + id: number + handleClose: (v: boolean) => void + isWorkspace?: boolean + title?: string + data?: { + expId: string + shareType: number + } + usersShare?: { + share_type?: number + users: UserDTO[] + } +} + +type TableSearch = { + usersSuggest: UserDTO[] + onClose: () => void + handleAddListUser: (user: UserDTO) => void + stateUserShare: UserDTO[] +} + +const TableListSearch = ({usersSuggest, onClose, handleAddListUser, stateUserShare}: TableSearch) => { + + const ref = useRef(null) + + useEffect(() => { + window.addEventListener('mousedown', onMouseDown) + return () => { + window.removeEventListener('mousedown', onMouseDown) + } + //eslint-disable-next-line + }, []) + + const onMouseDown = (event: MouseEvent) => { + if(ref.current?.contains((event as any).target) || (event as any).target.id === 'inputSearch') return; + onClose?.() + } + + return ( + console.log(123)}> + + {usersSuggest.map(item => { + const isSelected = stateUserShare.some(i => i.id === item.id) + return ( + handleAddListUser(item)} style={{ + cursor: isSelected ? 'not-allowed' : 'pointer' + }} + > + {`${item.name} (${item.email})`} + {isSelected ? : null} + + ) + })} + + + ) +} +const PopupShare = ({open, handleClose, data, usersShare, id, isWorkspace, title}: PopupType) => { + const [shareType, setShareType] = useState(data?.shareType || 0) + const usersSuggest = useSelector(selectListSearch) + const loading = useSelector(selectListSearchLoading) + const [textSearch, setTextSearch] = useState('') + const [stateUserShare, setStateUserShare] = useState(usersShare || undefined) + const dispatch = useDispatch() + let timeout = useRef() + + useEffect(() => { + if(usersShare) { + // setUserIdsSelected(usersShare.users.map(user => user.id)); + setStateUserShare(usersShare) + } + }, [usersShare]) + + useEffect(() => { + + }, [data]) + + useEffect(() => { + if(timeout.current) clearTimeout(timeout.current) + if(!textSearch) { + dispatch(resetUserSearch()) + return + } + timeout.current = setTimeout(() => { + dispatch(getListSearch({keyword: textSearch})) + }, 300) + //eslint-disable-next-line + }, [textSearch]) + + const handleShareFalse = (e: any, params: GridRenderCellParams) => { + e.preventDefault() + e.stopPropagation() + if(!stateUserShare) return + const indexCheck = stateUserShare.users.findIndex(user => user.id === params.id) + const newStateUserShare = stateUserShare.users.filter((user, index) => index !== indexCheck) + setStateUserShare({...setStateUserShare, users: newStateUserShare}) + } + + const handleValue = (event: ChangeEvent) => { + setShareType(Number((event.target as HTMLInputElement).value)); + } + + const columnsShare = useCallback((handleShareFalse: (e: MouseEventReact, parmas: GridRenderCellParams) => void) => [ + { + field: "name", + headerName: "Name", + minWidth: 140, + renderCell: (params: GridRenderCellParams) => ( + {params.row.name} + ), + }, + { + field: "email", + headerName: "Email", + minWidth: 280, + renderCell: (params: GridRenderCellParams) => ( + {params.row.email} + ), + }, + { + field: "share", + headerName: "", + filterable: false, + sortable: false, + minWidth: 130, + renderCell: (params: GridRenderCellParams) => { + if(!params.row.share) return '' + return ( + + ) + } + }, + //eslint-disable-next-line + ], [JSON.stringify(stateUserShare?.users)]) + + const handleOke = async () => { + if(!stateUserShare) return + let newUserIds = stateUserShare.users.map(user => user.id) + let newType = shareType + if(!isWorkspace) { + if(shareType === SHARE.ORGANIZATION) { + newUserIds = [] + } + else if(shareType === SHARE.USERS && newUserIds.length < 1) { + newType = 0 + } + else if(newUserIds.length > 0) newType = SHARE.USERS + await dispatch(postListUserShare({id, data: {user_ids: newUserIds as number[], share_type: newType }})) + } else { + await dispatch(postListUserShareWorkspaces({id, data: {user_ids: newUserIds as number[]}})) + } + handleClose(true); + } + + const handleSearch = (event: ChangeEvent) => { + setTextSearch(event.target.value) + } + + const handleCloseSearch = () => { + setTextSearch('') + dispatch(resetUserSearch()) + } + + const handleAddListUser = (user: any) => { + if(!usersSuggest || !stateUserShare) return + if(!stateUserShare.users.find(item => item.id === user.id)) { + setStateUserShare({...stateUserShare, users: [...stateUserShare.users, user]}) + } + } + + const handleClosePopup = (event: any) => { + if(event.key === 'Escape') { + handleClose(false) + } + } + + if(!data || !usersShare) return null; + + return ( + + + {title || "Share Database record"} + {isWorkspace ? null : Experiment ID: {data.expId}} + {isWorkspace ? null : ( + + + + } label={"Share for Organization"}/> + } label={"Share for Users"}/> + + + + )} + + { + (shareType !== SHARE.ORGANIZATION || isWorkspace) ? + <> + + + { + textSearch && usersSuggest ? + : null + } + +

Permitted users

+ { + stateUserShare && + ({...user, share: true}))} + columns={columnsShare(handleShareFalse)} + hideFooterPagination + /> + } + + : null + } +
+ + + + +
+ { + loading ? : null + } +
+ ) +} + +const DialogCustom = styled(Dialog)(({ theme }) => ({ + "& .MuiDialog-container": { + "& .MuiPaper-root": { + width: "70%", + maxWidth: "890px", + }, + }, +})) + +const TableListSearchWrapper = styled(Box)(({ theme }) => ({ + position: 'absolute', + background: "#fff", + zIndex: 100, + width: "60%", + boxShadow: '0 6px 16px 0 rgba(0,0,0,.08), 0 3px 6px -4px rgba(0,0,0,.12), 0 9px 28px 8px rgba(0,0,0,.05)', + borderBottomLeftRadius: 8, + borderBottomRightRadius: 8, + maxHeight: 200, + overflow: 'auto' +})) + +const UlCustom = styled('ul')(({ theme }) => ({ + listStyle: 'none', + padding: 0, + margin: 0, +})) + +const LiCustom = styled('li')(({ theme }) => ({ + padding: theme.spacing(1, 2), + fontSize: 14, + cursor: "pointer", + display: 'flex', + justifyContent: "space-between", + alignItems: 'center', + "&:hover": { + backgroundColor: 'rgba(0,0,0,.04)' + } +})) + +export default PopupShare diff --git a/frontend/src/pages/Account/index.tsx b/frontend/src/pages/Account/index.tsx index 38f2eb457..951d3305e 100644 --- a/frontend/src/pages/Account/index.tsx +++ b/frontend/src/pages/Account/index.tsx @@ -7,9 +7,10 @@ import { useState } from 'react' import { useNavigate } from "react-router-dom"; import { updateMePasswordApi } from 'api/users/UsersMe' import { deleteMe } from 'store/slice/User/UserActions' -import { selectCurrentUser } from 'store/slice/User/UserSelector' +import {isAdmin, selectCurrentUser} from 'store/slice/User/UserSelector' const Account = () => { const user = useSelector(selectCurrentUser) + const admin = useSelector(isAdmin) const dispatch = useDispatch() const navigate = useNavigate() const [isDeleteConfirmModalOpen, setIsDeleteConfirmModalOpen] = useState(false) @@ -86,7 +87,10 @@ const Account = () => { Change Password - Delete Account + { + admin ? + Delete Account : null + } { isLoading && diff --git a/frontend/src/pages/Dashboard/index.tsx b/frontend/src/pages/Dashboard/index.tsx index fc421e091..64f493172 100644 --- a/frontend/src/pages/Dashboard/index.tsx +++ b/frontend/src/pages/Dashboard/index.tsx @@ -3,8 +3,10 @@ import { Box, styled, Typography } from '@mui/material' import StorageIcon from '@mui/icons-material/Storage' import AccountCircleIcon from '@mui/icons-material/AccountCircle' import AnalyticsIcon from '@mui/icons-material/Analytics' - +import { useSelector } from 'react-redux' +import { isAdmin } from 'store/slice/User/UserSelector' const Dashboard = () => { + const admin = useSelector(isAdmin) return (

Dashboard

@@ -26,14 +28,17 @@ const Dashboard = () => { - - - - - Account - - - + { + admin ? + + + + + Account + + + : null + }
diff --git a/frontend/src/pages/Login/index.tsx b/frontend/src/pages/Login/index.tsx index 9f87c6416..59a4bb1cc 100644 --- a/frontend/src/pages/Login/index.tsx +++ b/frontend/src/pages/Login/index.tsx @@ -1,6 +1,6 @@ import { Box, Stack, styled, Typography } from '@mui/material' import { useDispatch } from 'react-redux' -import { login } from 'store/slice/User/UserActions' +import {getMe, login} from 'store/slice/User/UserActions' import { AppDispatch } from 'store/store' import { ChangeEvent, FormEvent, useState } from 'react' import { Link, useNavigate } from 'react-router-dom' @@ -27,7 +27,8 @@ const Login = () => { setIsLoading(true) dispatch(login(values)) .unwrap() - .then((_) => { + .then(async (_) => { + await dispatch(getMe()) navigate('/console') }) .catch((_) => { diff --git a/frontend/src/pages/Workspace/index.tsx b/frontend/src/pages/Workspace/index.tsx index 69c113a61..0ef3100c8 100644 --- a/frontend/src/pages/Workspace/index.tsx +++ b/frontend/src/pages/Workspace/index.tsx @@ -10,7 +10,7 @@ import { Input, Pagination, } from '@mui/material' -import { GridRenderCellParams, GridRowParams, DataGrid } from '@mui/x-data-grid' +import { GridRenderCellParams } from '@mui/x-data-grid' import { DataGridPro, GridEventListener, @@ -19,26 +19,30 @@ import { GridRowModes, } from '@mui/x-data-grid-pro' import { Link, useSearchParams } from 'react-router-dom' -import { selectCurrentUser } from 'store/slice/User/UserSelector' import Loading from '../../components/common/Loading' import { selectIsLoadingWorkspaceList, selectWorkspaceData, + selectWorkspaceListUserShare, } from 'store/slice/Workspace/WorkspaceSelector' -import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt' -import CancelIcon from '@mui/icons-material/Cancel' import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react' -import EditIcon from '@mui/icons-material/Edit' -import PeopleOutlineIcon from '@mui/icons-material/PeopleOutline' import { delWorkspace, exportWorkspace, + getListUserShareWorkSpaces, getWorkspaceList, importWorkspace, postWorkspace, putWorkspace, } from 'store/slice/Workspace/WorkspacesActions' +import PopupShare from 'components/PopupShare' import moment from 'moment' +import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt'; +import GroupsIcon from '@mui/icons-material/Groups'; +import EditIcon from '@mui/icons-material/Edit'; +import { selectCurrentUser } from 'store/slice/User/UserSelector' +import { UserDTO } from 'api/users/UsersApiDTO' +import { isMe } from 'utils/checkRole' type PopupType = { open: boolean @@ -51,11 +55,12 @@ type PopupType = { error?: string } + const columns = ( - handleOpenPopupShare: () => void, + handleOpenPopupShare: (id: number) => void, handleOpenPopupDel: (id: number) => void, handleDownload: (id: number) => void, - user?: { id: number }, + user?: UserDTO, onEdit?: (id: number) => void, ) => [ { @@ -96,11 +101,11 @@ const columns = ( > {value} - {row.user?.id === user?.id && ( + {isMe(user, row?.user?.id) ? ( onEdit?.(row.id)}> - )} + ) : null} ) }, @@ -116,7 +121,7 @@ const columns = ( ) => ( {params.value?.name} - {params.value.id !== user?.id ? : ''} + {!isMe(user, params.value.id) ? : ''} ), }, @@ -169,11 +174,11 @@ const columns = ( filterable: false, // todo enable when api complete sortable: false, // todo enable when api complete renderCell: (params: GridRenderCellParams) => - params.row?.user?.id === user?.id && ( - - + isMe(user, params.row?.user?.id) ? ( + handleOpenPopupShare(params.row.id)}> + - ), + ): null }, { field: 'delete', @@ -182,122 +187,14 @@ const columns = ( filterable: false, // todo enable when api complete sortable: false, // todo enable when api complete renderCell: (params: GridRenderCellParams) => - params.row?.user_id === user?.id && ( - handleOpenPopupDel(params.row.id)}> - Del - - ), - }, -] - -const columnsShare = ( - handleShareFalse: (parmas: GridRenderCellParams) => void, -) => [ - { - field: 'name', - headerName: 'Name', - minWidth: 140, - renderCell: (params: GridRenderCellParams) => ( - {params.row.name} - ), - }, - { - field: 'lab', - headerName: 'Lab', - minWidth: 280, - renderCell: (params: GridRenderCellParams) => ( - {params.row.email} - ), - }, - { - field: 'email', - headerName: 'Email', - minWidth: 280, - renderCell: (params: GridRenderCellParams) => ( - {params.row.email} - ), - }, - { - field: 'share', - headerName: '', - minWidth: 130, - renderCell: (params: GridRenderCellParams) => { - if (!params.row.share) return '' - return ( - - ) - }, - }, -] - -const dataShare = [ - { - id: 1, - name: 'User 1', - lab: 'Labxxxx', - email: 'aaaaa@gmail.com', - share: false, - }, - { - id: 2, - name: 'User 2', - lab: 'Labxxxx', - email: 'aaaaa@gmail.com', - share: true, - }, - { - id: 3, - name: 'User 3', - lab: 'Labxxxx', - email: 'aaaaa@gmail.com', - share: true, + isMe(user, params.row?.user?.id) ? ( + handleOpenPopupDel(params.row.id)}> + Del + + ) : null }, ] -const PopupShare = ({ open, handleClose }: PopupType) => { - const [tableShare, setTableShare] = useState(dataShare) - const handleShareTrue = (params: GridRowParams) => { - if (params.row.share) return - const index = tableShare.findIndex((item) => item.id === params.id) - setTableShare((pre) => { - pre[index].share = true - return pre - }) - } - - const handleShareFalse = (params: GridRenderCellParams) => { - const indexSearch = tableShare.findIndex((item) => item.id === params.id) - const newData = tableShare.map((item, index) => { - if (index === indexSearch) return { ...item, share: false } - return item - }) - setTableShare(newData) - } - - return ( - - - Share Workspace - Permitted users - - - - - - - - - - ) -} - const PopupNew = ({ open, handleClose, @@ -334,8 +231,8 @@ const PopupNew = ({ ) } -const PopupDelete = ({ open, handleClose, handleOkDel }: PopupType) => { - if (!handleOkDel) return null +const PopupDelete = ({open, handleClose, handleOkDel}: PopupType) => { + if(!open) return null return ( @@ -352,9 +249,15 @@ const PopupDelete = ({ open, handleClose, handleOkDel }: PopupType) => { const Workspaces = () => { const dispatch = useDispatch() const loading = useSelector(selectIsLoadingWorkspaceList) + const listUserShare = useSelector(selectWorkspaceListUserShare) const data = useSelector(selectWorkspaceData) const user = useSelector(selectCurrentUser) - const [open, setOpen] = useState({ share: false, del: false, new: false }) + const [open, setOpen] = useState({ + share: false, + del: false, + new: false, + shareId: 0, + }) const [idDel, setIdDel] = useState() const [newWorkspace, setNewWorkSpace] = useState() const [error, setError] = useState('') @@ -378,10 +281,16 @@ const Workspaces = () => { //eslint-disable-next-line }, [dataParams]) - const handleOpenPopupShare = () => { - setOpen({ ...open, share: true }) + const handleOpenPopupShare = (shareId: number) => { + setOpen({ ...open, share: true, shareId }) } + useEffect(() => { + if (!open.share || !open.shareId) return + dispatch(getListUserShareWorkSpaces({ id: open.shareId })) + //eslint-disable-next-line + }, [open.share, open.shareId]) + const handleClosePopupShare = () => { setOpen({ ...open, share: false }) } @@ -515,46 +424,64 @@ const Workspaces = () => { New - - params.row.user?.id === user?.id} - onProcessRowUpdateError={onProcessRowUpdateError} - onRowEditStop={onRowEditStop} - processRowUpdate={processRowUpdate as any} - hideFooter={true} - /> - + { + user ? + + isMe(user, params.row.user?.id)} + onProcessRowUpdateError={onProcessRowUpdateError} + onRowEditStop={onRowEditStop} + processRowUpdate={processRowUpdate as any} + hideFooter={true} + /> + : null + } - + {open.share ? ( + { + if (_isSubmit) { + dispatch(getWorkspaceList(dataParams)) + } + handleClosePopupShare() + }} + id={open.shareId} + data={{ expId: '', shareType: 0 }} + /> + ) : null} ({ }, })) -const DialogCustom = styled(Dialog)(({ theme }) => ({ - '& .MuiDialog-container': { - '& .MuiPaper-root': { - width: '70%', - maxWidth: '890px', - }, - }, -})) - const ButtonIcon = styled('button')(({ theme }) => ({ minWidth: '32px', minHeight: '32px', diff --git a/frontend/src/store/slice/Database/DatabaseActions.ts b/frontend/src/store/slice/Database/DatabaseActions.ts index e4aa1b316..d986a0126 100644 --- a/frontend/src/store/slice/Database/DatabaseActions.ts +++ b/frontend/src/store/slice/Database/DatabaseActions.ts @@ -3,12 +3,15 @@ import { DATABASE_SLICE_NAME, DatabaseDTO, DatabaseParams, + ListShareDTO, } from './DatabaseType' import { getCellsApi, getCellsPublicApi, getExperimentsApi, getExperimentsPublicApi, + getListUserShareApi, + postListUserShareApi, postPublistApi, } from 'api/database' @@ -68,8 +71,8 @@ export const getCellsPublicDatabase = createAsyncThunk< }) export const postPublist = createAsyncThunk< - DatabaseDTO, - {id: number, status: "on" | "off"} + boolean, + { id: number; status: 'on' | 'off' } >(`${DATABASE_SLICE_NAME}/postPublist`, async (params, thunkAPI) => { const { rejectWithValue } = thunkAPI try { @@ -80,3 +83,31 @@ export const postPublist = createAsyncThunk< } }) +export const getListUserShare = createAsyncThunk( + `${DATABASE_SLICE_NAME}/getListUserShare`, + async (params, thunkAPI) => { + const { rejectWithValue } = thunkAPI + try { + const response = await getListUserShareApi(params.id) + return response + } catch (e) { + return rejectWithValue(e) + } + }, +) + +export const postListUserShare = createAsyncThunk< + boolean, + { + id: number + data: { share_type: number; user_ids: number[] } + } +>(`${DATABASE_SLICE_NAME}/postListUserShare`, async (params, thunkAPI) => { + const { rejectWithValue } = thunkAPI + try { + const response = await postListUserShareApi(params.id, params.data) + return response + } catch (e) { + return rejectWithValue(e) + } +}) diff --git a/frontend/src/store/slice/Database/DatabaseSlice.ts b/frontend/src/store/slice/Database/DatabaseSlice.ts index 54b51ed77..e70eec3b4 100644 --- a/frontend/src/store/slice/Database/DatabaseSlice.ts +++ b/frontend/src/store/slice/Database/DatabaseSlice.ts @@ -4,8 +4,10 @@ import { getCellsDatabase, getExperimentsPublicDatabase, getCellsPublicDatabase, + getListUserShare, + postListUserShare, } from './DatabaseActions' -import { DATABASE_SLICE_NAME, DatabaseDTO } from './DatabaseType' +import { DATABASE_SLICE_NAME, DatabaseDTO, ListShare } from './DatabaseType' const initData = { offset: 0, @@ -26,6 +28,10 @@ export const initialState: { data: TypeData loading: boolean type: 'experiment' | 'cell' + listShare?: { + share_type: number + users: ListShare[] + } } = { data: { public: initData, @@ -33,6 +39,7 @@ export const initialState: { }, loading: false, type: 'experiment', + listShare: undefined } export const databaseSlice = createSlice({ @@ -69,6 +76,13 @@ export const databaseSlice = createSlice({ } state.loading = true }) + .addCase(getListUserShare.pending, (state, action) => { + state.listShare = undefined + state.loading = true + }) + .addCase(postListUserShare.pending, (state) => { + state.loading = true + }) .addMatcher( isAnyOf( getCellsDatabase.fulfilled, @@ -89,12 +103,24 @@ export const databaseSlice = createSlice({ state.loading = false }, ) + .addMatcher( + isAnyOf( + getListUserShare.fulfilled, + ), + (state, action) => { + state.listShare = action.payload + state.loading = false + }, + ) .addMatcher( isAnyOf( getExperimentsDatabase.rejected, getCellsDatabase.rejected, getExperimentsPublicDatabase.rejected, getCellsPublicDatabase.rejected, + getListUserShare.rejected, + postListUserShare.rejected, + postListUserShare.fulfilled ), (state) => { state.loading = false diff --git a/frontend/src/store/slice/Database/DatabaseType.ts b/frontend/src/store/slice/Database/DatabaseType.ts index 6115db0f2..598f6a9ae 100644 --- a/frontend/src/store/slice/Database/DatabaseType.ts +++ b/frontend/src/store/slice/Database/DatabaseType.ts @@ -41,4 +41,17 @@ export type DatabaseDTO = { items: DatabaseType[] } +export type ListShareDTO = { + share_type: number + users: ListShare[] +} + +export type ListShare = { + id: number, + name: string + email: string + created_at: string + updated_at: string +} + export type DatabaseParams = { [key: string]: number | string | string[] | undefined } diff --git a/frontend/src/store/slice/User/UserActions.ts b/frontend/src/store/slice/User/UserActions.ts index 1f1dd4077..787c5459c 100644 --- a/frontend/src/store/slice/User/UserActions.ts +++ b/frontend/src/store/slice/User/UserActions.ts @@ -1,8 +1,9 @@ import { createAsyncThunk } from '@reduxjs/toolkit' import { USER_SLICE_NAME } from './UserType' import { deleteMeApi, getMeApi, updateMeApi } from 'api/users/UsersMe' -import { UpdateUserDTO } from 'api/users/UsersApiDTO' +import {UpdateUserDTO, UserDTO} from 'api/users/UsersApiDTO' import { LoginDTO, loginApi } from 'api/auth/Auth' +import {getListSearchApi} from "../../../api/users/UsersAdmin"; export const login = createAsyncThunk( `${USER_SLICE_NAME}/login`, @@ -51,3 +52,18 @@ export const deleteMe = createAsyncThunk( } }, ) + +export const getListSearch = createAsyncThunk< + UserDTO[], + {keyword: string | null} +>( + `${USER_SLICE_NAME}/getListSearch`, + async (params, thunkAPI) => { + try { + const responseData = await getListSearchApi(params) + return responseData + } catch (e) { + return thunkAPI.rejectWithValue(e) + } + }, +) diff --git a/frontend/src/store/slice/User/UserSelector.ts b/frontend/src/store/slice/User/UserSelector.ts index fa17bf27e..513cfea4e 100644 --- a/frontend/src/store/slice/User/UserSelector.ts +++ b/frontend/src/store/slice/User/UserSelector.ts @@ -1,3 +1,4 @@ +import { ROLE } from '@types' import { RootState } from 'store/store' export const selectCurrentUser = (state: RootState) => state.user.currentUser @@ -5,3 +6,13 @@ export const selectCurrentUserUid = (state: RootState) => selectCurrentUser(state)?.uid export const selectCurrentUserEmail = (state: RootState) => selectCurrentUser(state)?.email +export const selectListSearch = (state: RootState) => state.user.listUserSearch +export const selectListSearchLoading = (state: RootState) => state.user.loading + +export const isAdmin = (state: RootState) => { + return state.user && ROLE.ADMIN === state.user.currentUser?.role_id +} + +export const isAdminOrManager = (state: RootState) => { + return [ROLE.ADMIN, ROLE.MANAGER].includes(state.user.currentUser?.role_id as number) +} diff --git a/frontend/src/store/slice/User/UserSlice.ts b/frontend/src/store/slice/User/UserSlice.ts index f0a210b14..ff01f7a7c 100644 --- a/frontend/src/store/slice/User/UserSlice.ts +++ b/frontend/src/store/slice/User/UserSlice.ts @@ -1,7 +1,7 @@ import { createSlice, isAnyOf } from '@reduxjs/toolkit' import { USER_SLICE_NAME } from './UserType' import { User } from './UserType' -import { deleteMe, getMe, login, updateMe } from './UserActions' +import {deleteMe, getListSearch, getMe, login, updateMe} from './UserActions' import { removeExToken, removeToken, @@ -10,7 +10,11 @@ import { saveToken, } from 'utils/auth/AuthUtils' -const initialState: User = { currentUser: undefined } +const initialState: User = { + currentUser: undefined, + listUserSearch: undefined, + loading: false +} export const userSlice = createSlice({ name: USER_SLICE_NAME, @@ -21,6 +25,9 @@ export const userSlice = createSlice({ removeExToken() state = initialState }, + resetUserSearch: (state) => { + state.listUserSearch = [] + } }, extraReducers: (builder) => { builder @@ -35,6 +42,16 @@ export const userSlice = createSlice({ .addCase(updateMe.fulfilled, (state, action) => { state.currentUser = action.payload }) + .addCase(getListSearch.pending, (state, action) => { + state.loading = true + }) + .addCase(getListSearch.fulfilled, (state, action) => { + state.loading = false + state.listUserSearch = action.payload + }) + .addCase(getListSearch.rejected, (state) => { + state.loading = false + }) .addMatcher( isAnyOf(login.rejected, getMe.rejected, deleteMe.fulfilled), (state) => { @@ -46,5 +63,5 @@ export const userSlice = createSlice({ }, }) -export const { logout } = userSlice.actions +export const { logout, resetUserSearch } = userSlice.actions export default userSlice.reducer diff --git a/frontend/src/store/slice/User/UserType.ts b/frontend/src/store/slice/User/UserType.ts index 9deed628e..9f3cc5d2f 100644 --- a/frontend/src/store/slice/User/UserType.ts +++ b/frontend/src/store/slice/User/UserType.ts @@ -4,4 +4,6 @@ export const USER_SLICE_NAME = 'user' export type User = { currentUser?: UserDTO + listUserSearch?: UserDTO[] + loading: boolean } diff --git a/frontend/src/store/slice/Workspace/WorkspaceSelector.ts b/frontend/src/store/slice/Workspace/WorkspaceSelector.ts index 6f011300c..fd5d9020b 100644 --- a/frontend/src/store/slice/Workspace/WorkspaceSelector.ts +++ b/frontend/src/store/slice/Workspace/WorkspaceSelector.ts @@ -2,6 +2,7 @@ import { RootState } from 'store/store' export const selectWorkspace = (state: RootState) => state.workspace export const selectWorkspaceData = (state: RootState) => state.workspace.workspace +export const selectWorkspaceListUserShare = (state: RootState) => state.workspace.listUserShare export const selectActiveTab = (state: RootState) => state.workspace.currentWorkspace.selectedTab diff --git a/frontend/src/store/slice/Workspace/WorkspaceSlice.ts b/frontend/src/store/slice/Workspace/WorkspaceSlice.ts index 59c7c254d..40b2603d1 100644 --- a/frontend/src/store/slice/Workspace/WorkspaceSlice.ts +++ b/frontend/src/store/slice/Workspace/WorkspaceSlice.ts @@ -3,7 +3,9 @@ import { WORKSPACE_SLICE_NAME, Workspace } from './WorkspaceType' import { importExperimentByUid } from '../Experiments/ExperimentsActions' import { delWorkspace, + getListUserShareWorkSpaces, getWorkspaceList, + postListUserShareWorkspaces, postWorkspace, putWorkspace, } from './WorkspacesActions' @@ -19,6 +21,7 @@ const initialState: Workspace = { offset: 0, }, loading: false, + listUserShare: undefined } export const workspaceSlice = createSlice({ @@ -46,6 +49,10 @@ export const workspaceSlice = createSlice({ state.workspace = action.payload state.loading = false }) + .addCase(getListUserShareWorkSpaces.fulfilled, (state, action) => { + state.listUserShare = action.payload + state.loading = false + }) .addMatcher( isAnyOf( getWorkspaceList.rejected, @@ -55,6 +62,8 @@ export const workspaceSlice = createSlice({ putWorkspace.rejected, delWorkspace.fulfilled, delWorkspace.rejected, + getListUserShareWorkSpaces.rejected, + postListUserShareWorkspaces.rejected ), (state) => { state.loading = false @@ -66,6 +75,8 @@ export const workspaceSlice = createSlice({ postWorkspace.pending, putWorkspace.pending, delWorkspace.pending, + getListUserShareWorkSpaces.pending, + postListUserShareWorkspaces.pending ), (state) => { state.loading = true diff --git a/frontend/src/store/slice/Workspace/WorkspaceType.ts b/frontend/src/store/slice/Workspace/WorkspaceType.ts index ceb42ee03..5d6e1d74a 100644 --- a/frontend/src/store/slice/Workspace/WorkspaceType.ts +++ b/frontend/src/store/slice/Workspace/WorkspaceType.ts @@ -1,3 +1,5 @@ +import {UserDTO} from "../../../api/users/UsersApiDTO"; + export const WORKSPACE_SLICE_NAME = 'workspace' export type ItemsWorkspace = { @@ -28,6 +30,19 @@ export type Workspace = { selectedTab: number } loading: boolean + listUserShare?: ListUserShareWorkspaceDTO +} + +export type ListUserShareWorkSpace = { + id: number, + name: string + email: string + created_at: string + updated_at: string +} + +export type ListUserShareWorkspaceDTO = { + users: UserDTO[] } export type WorkspaceParams = { [key: string]: string | undefined | number | string[] | object } diff --git a/frontend/src/store/slice/Workspace/WorkspacesActions.ts b/frontend/src/store/slice/Workspace/WorkspacesActions.ts index 9e5632c7a..9664db43a 100644 --- a/frontend/src/store/slice/Workspace/WorkspacesActions.ts +++ b/frontend/src/store/slice/Workspace/WorkspacesActions.ts @@ -7,7 +7,10 @@ import { importWorkspaceApi, postWorkspaceApi, putWorkspaceApi, + getListUserShareWorkspaceApi, + postListUserShareWorkspaceApi, } from 'api/Workspace' +import { ListShareDTO } from '../Database/DatabaseType' import { ItemsWorkspace, WorkspaceDataDTO, @@ -94,3 +97,32 @@ export const exportWorkspace = createAsyncThunk( } }, ) + +export const getListUserShareWorkSpaces = createAsyncThunk< + ListShareDTO, + {id: number} +>(`${WORKSPACE_SLICE_NAME}/getListUserShareWorkSpaces`, async (params, thunkAPI) => { + const { rejectWithValue } = thunkAPI + try { + const response = await getListUserShareWorkspaceApi(params.id) + return response + } catch (e) { + return rejectWithValue(e) + } +}) + +export const postListUserShareWorkspaces = createAsyncThunk< + boolean, + { + id: number + data: {user_ids: number[]} + } +>(`${WORKSPACE_SLICE_NAME}/postListUserShareWorkspaces`, async (params, thunkAPI) => { + const { rejectWithValue } = thunkAPI + try { + const response = await postListUserShareWorkspaceApi(params.id, params.data) + return response + } catch (e) { + return rejectWithValue(e) + } +}) diff --git a/frontend/src/utils/checkRole.ts b/frontend/src/utils/checkRole.ts new file mode 100644 index 000000000..a98b4c85d --- /dev/null +++ b/frontend/src/utils/checkRole.ts @@ -0,0 +1,5 @@ +import { UserDTO } from "api/users/UsersApiDTO" + +export const isMe = (user?: UserDTO, idUserWorkSpace?: number) => { + return !!(user && idUserWorkSpace && user.id === idUserWorkSpace) +} diff --git a/studio/alembic/versions/965cb1594a40_create_foreign_key_for_user_id.py b/studio/alembic/versions/965cb1594a40_create_foreign_key_for_user_id.py new file mode 100644 index 000000000..e589015c5 --- /dev/null +++ b/studio/alembic/versions/965cb1594a40_create_foreign_key_for_user_id.py @@ -0,0 +1,32 @@ +"""create foreignkey for user_id + +Revision ID: 965cb1594a40 +Revises: 73d4d7abcd35 +Create Date: 2023-08-01 18:27:33.077883 + +""" +from alembic import op + +# revision identifiers, used by Alembic. +revision = "965cb1594a40" +down_revision = "73d4d7abcd35" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.execute( + "ALTER TABLE workspaces " "MODIFY COLUMN user_id BIGINT UNSIGNED NOT NULL;" + ) + op.drop_index("ix_workspaces_user_id", table_name="workspaces") + op.create_foreign_key("user", "workspaces", "users", ["user_id"], ["id"]) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint("user", "workspaces", type_="foreignkey") + op.create_index("ix_workspaces_user_id", "workspaces", ["user_id"], unique=False) + op.execute("ALTER TABLE workspaces " "MODIFY COLUMN user_id INTEGER NOT NULL;") + # ### end Alembic commands ### diff --git a/studio/app/common/core/auth/auth_dependencies.py b/studio/app/common/core/auth/auth_dependencies.py index 624af9cfa..a4c2b00c4 100644 --- a/studio/app/common/core/auth/auth_dependencies.py +++ b/studio/app/common/core/auth/auth_dependencies.py @@ -62,3 +62,13 @@ async def get_admin_user(current_user: User = Depends(get_current_user)): status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient privileges", ) + + +async def get_admin_data_user(current_user: User = Depends(get_current_user)): + if current_user.is_admin_data: + return current_user + else: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Insufficient privileges", + ) diff --git a/studio/app/common/core/users/crud_users.py b/studio/app/common/core/users/crud_users.py index 6d7545494..8ff15b812 100644 --- a/studio/app/common/core/users/crud_users.py +++ b/studio/app/common/core/users/crud_users.py @@ -54,18 +54,17 @@ async def get_user(db: Session, user_id: int, organization_id: int) -> User: async def list_user(db: Session, organization_id: int): try: - users = paginate( - db, - query=select(UserModel).filter( - UserModel.active.is_(True), - UserModel.organization_id == organization_id, - ), - ) - for user in users.items: - role = ( - db.query(UserRoleModel).filter(UserRoleModel.user_id == user.id).first() + query = ( + select( + UserRoleModel.role_id, + *UserModel.__table__.columns, ) - user.__dict__["role_id"] = role.role_id if role else None + .join(UserRoleModel, UserRoleModel.user_id == UserModel.id) + .filter( + UserModel.active.is_(True), UserModel.organization_id == organization_id + ) + ) + users = paginate(db, query=query, unique=False) return users except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @@ -114,10 +113,7 @@ async def update_user( setattr(user_db, key, value) if role_id is not None: await set_role(db, user_id=user_db.id, role_id=role_id, auto_commit=False) - role = ( - db.query(UserRoleModel).filter(UserRoleModel.user_id == user_db.id).first() - ) - user_db.__dict__["role_id"] = role.role_id if role else None + user_db.__dict__["role_id"] = role_id firebase_auth.update_user(user_db.uid, email=data.email) db.commit() return User.from_orm(user_db) diff --git a/studio/app/common/models/user.py b/studio/app/common/models/user.py index 63c71c54b..6a96c72ef 100644 --- a/studio/app/common/models/user.py +++ b/studio/app/common/models/user.py @@ -1,8 +1,16 @@ from datetime import datetime -from typing import Dict, Optional +from typing import Dict, List, Optional from sqlalchemy.sql.functions import current_timestamp -from sqlmodel import JSON, Column, Field, Integer, String, UniqueConstraint +from sqlmodel import ( + JSON, + Column, + Field, + Integer, + Relationship, + String, + UniqueConstraint, +) from studio.app.common.models.base import Base, TimestampMixin @@ -18,6 +26,10 @@ class User(Base, TimestampMixin, table=True): attributes: Optional[Dict] = Field(default={}, sa_column=Column(JSON)) active: bool = Field(nullable=False) + workspace: List["Workspace"] = Relationship( # noqa: F821 + back_populates="user", sa_relationship_kwargs={"uselist": True} + ) + class Organization(Base, table=True): __tablename__ = "organization" diff --git a/studio/app/common/models/workspace.py b/studio/app/common/models/workspace.py index 39781eb0f..358bb999f 100644 --- a/studio/app/common/models/workspace.py +++ b/studio/app/common/models/workspace.py @@ -1,8 +1,17 @@ from datetime import datetime from typing import Optional +from sqlalchemy.dialects.mysql import BIGINT from sqlalchemy.sql.functions import current_timestamp -from sqlmodel import Column, Field, Integer, String, UniqueConstraint +from sqlmodel import ( + Column, + Field, + ForeignKey, + Integer, + Relationship, + String, + UniqueConstraint, +) from studio.app.common.models.base import Base, TimestampMixin @@ -11,8 +20,15 @@ class Workspace(Base, TimestampMixin, table=True): __tablename__ = "workspaces" name: str = Field(sa_column=Column(String(100), nullable=False)) - user_id: int = Field(nullable=False, index=True) + user_id: int = Field( + sa_column=Column( + BIGINT(unsigned=True), ForeignKey("users.id", name="user"), nullable=False + ), + ) deleted: bool = Field(nullable=False) + user: Optional["User"] = Relationship( # noqa: F821 + back_populates="workspace", + ) class WorkspacesShareUser(Base, table=True): diff --git a/studio/app/common/routers/workspace.py b/studio/app/common/routers/workspace.py index d96783ceb..7d3279e4f 100644 --- a/studio/app/common/routers/workspace.py +++ b/studio/app/common/routers/workspace.py @@ -15,7 +15,7 @@ WorkspacesSetting, WorkspaceUpdate, ) -from studio.app.optinist.schemas.base import SortDirection, SortOptions +from studio.app.optinist.schemas.base import SortOptions router = APIRouter(tags=["Workspace"]) @@ -32,7 +32,7 @@ def search_workspaces( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): - sort_column = getattr(common_model.Workspace, sortOptions.sort[0] or "id") + sa_sort_list = sortOptions.get_sa_sort_list(sa_table=common_model.Workspace) query = ( select(common_model.Workspace) .outerjoin( @@ -47,15 +47,9 @@ def search_workspaces( ), ) .group_by(common_model.Workspace.id) - .order_by( - sort_column.desc() - if sortOptions.sort[1] == SortDirection.desc - else sort_column.asc() - ) + .order_by(*sa_sort_list) ) data = paginate(db, query) - for ws in data.items: - ws.__dict__["user"] = db.query(common_model.User).get(ws.user_id) return data diff --git a/studio/app/common/schemas/workspace.py b/studio/app/common/schemas/workspace.py index 574cb5ba7..a3df2daba 100644 --- a/studio/app/common/schemas/workspace.py +++ b/studio/app/common/schemas/workspace.py @@ -9,7 +9,6 @@ class Workspace(BaseModel): id: Optional[int] name: str - user_id: Optional[int] user: Optional[UserInfo] created_at: Optional[datetime] updated_at: Optional[datetime] diff --git a/studio/app/optinist/routers/expdb.py b/studio/app/optinist/routers/expdb.py index f7336822a..32072e07b 100644 --- a/studio/app/optinist/routers/expdb.py +++ b/studio/app/optinist/routers/expdb.py @@ -6,7 +6,7 @@ from studio.app.common import models as common_model from studio.app.common.core.auth.auth_dependencies import ( - get_admin_user, + get_admin_data_user, get_current_user, ) from studio.app.common.db.database import get_db @@ -345,7 +345,7 @@ async def publish_db_experiment( id: int, flag: PublishFlags, db: Session = Depends(get_db), - current_admin_user: User = Depends(get_admin_user), + current_admin_user: User = Depends(get_admin_data_user), ): exp = ( db.query(optinist_model.Experiment) @@ -382,7 +382,7 @@ async def publish_db_experiment( def get_experiment_database_share_status( id: int, db: Session = Depends(get_db), - current_admin_user: User = Depends(get_admin_user), + current_admin_user: User = Depends(get_admin_data_user), ): exp = ( db.query(optinist_model.Experiment) @@ -409,6 +409,9 @@ def get_experiment_database_share_status( db.query(common_model.User) .join( optinist_model.ExperimentShareUser, + optinist_model.ExperimentShareUser.user_id == common_model.User.id, + ) + .filter( optinist_model.ExperimentShareUser.experiment_uid == id, ) .all() @@ -428,7 +431,7 @@ def update_experiment_database_share_status( id: int, data: ExpDbExperimentSharePostStatus, db: Session = Depends(get_db), - current_admin_user: User = Depends(get_admin_user), + current_admin_user: User = Depends(get_admin_data_user), ): exp = ( db.query(optinist_model.Experiment)