diff --git a/packages/client/src/components/app/ChangeAccessModal.tsx b/packages/client/src/components/app/ChangeAccessModal.tsx index 1798173b77..973ef2e8ed 100644 --- a/packages/client/src/components/app/ChangeAccessModal.tsx +++ b/packages/client/src/components/app/ChangeAccessModal.tsx @@ -11,7 +11,8 @@ import { import { Control, Controller } from 'react-hook-form'; import { HdrAuto, Edit, Visibility } from '@mui/icons-material'; import { AppDetailsFormTypes } from './app-details.utility'; -import { PERMISSION_DESCRIPTION_MAP } from '../settings/member-permissions.constants'; + +import { PERMISSION_DESCRIPTION_MAP } from '@/constants'; import { useRootStore } from '@/hooks'; const StyledContentBox = styled(Stack)(({ theme }) => ({ @@ -56,7 +57,7 @@ interface ChangeAccessModalProps { export const ChangeAccessModal = (props: ChangeAccessModalProps) => { const { open, onClose, control, getValues } = props; - const permissionDescriptions = PERMISSION_DESCRIPTION_MAP['app']; + const permissionDescriptions = PERMISSION_DESCRIPTION_MAP['APP']; const { monolithStore } = useRootStore(); const notification = useNotification(); diff --git a/packages/client/src/components/engine/EditEngineDetails.tsx b/packages/client/src/components/engine/EditEngineDetails.tsx index 5920533c6d..35bce2a0f2 100644 --- a/packages/client/src/components/engine/EditEngineDetails.tsx +++ b/packages/client/src/components/engine/EditEngineDetails.tsx @@ -20,9 +20,6 @@ const StyledEditorContainer = styled('div')(({ theme }) => ({ })); interface EditEngineDetailsProps { - /** Type of Engine */ - type: string; - /** Track if the edit is open */ open: boolean; @@ -37,7 +34,7 @@ interface EditEngineDetailsProps { * Wrap the Engine routes and provide styling/functionality */ export const EditEngineDetails = observer((props: EditEngineDetailsProps) => { - const { open = false, type, onClose = () => null, values = {} } = props; + const { open = false, onClose = () => null, values = {} } = props; // get the notification const notification = useNotification(); @@ -54,7 +51,7 @@ export const EditEngineDetails = observer((props: EditEngineDetailsProps) => { ); // get the engine information - const { id } = useEngine(); + const { id, type } = useEngine(); // track the options const [filterOptions, setFilterOptions] = useState< diff --git a/packages/client/src/components/engine/EngineAccessButton.tsx b/packages/client/src/components/engine/EngineAccessButton.tsx index b36f984a8e..9ce8fbd426 100644 --- a/packages/client/src/components/engine/EngineAccessButton.tsx +++ b/packages/client/src/components/engine/EngineAccessButton.tsx @@ -13,25 +13,17 @@ import { TextArea, } from '@semoss/ui'; import { EditRounded, RemoveRedEyeRounded, Add } from '@mui/icons-material'; -import { PERMISSION_DESCRIPTION_MAP } from '@/components/settings/member-permissions.constants'; import { Role } from '@/types'; import { useRootStore, useEngine } from '@/hooks'; +import { PERMISSION_DESCRIPTION_MAP } from '@/constants'; const StyledCard = styled(Card)({ borderRadius: '12px', }); -interface EngineAccessButtonProps { - /** - * Model, Vector, Storage, Database, Function - */ - name: string; -} - -export const EngineAccessButton = (props: EngineAccessButtonProps) => { - const { name } = props; - const { id, role } = useEngine(); +export const EngineAccessButton = () => { + const { id, type, role } = useEngine(); const { monolithStore } = useRootStore(); const notification = useNotification(); @@ -155,9 +147,8 @@ export const EngineAccessButton = (props: EngineAccessButtonProps) => { subheader={ { - PERMISSION_DESCRIPTION_MAP[ - name.toLowerCase() - ]?.author + PERMISSION_DESCRIPTION_MAP[type] + .author } } @@ -198,9 +189,8 @@ export const EngineAccessButton = (props: EngineAccessButtonProps) => { subheader={ { - PERMISSION_DESCRIPTION_MAP[ - name.toLowerCase() - ]?.editor + PERMISSION_DESCRIPTION_MAP[type] + .editor } } @@ -241,9 +231,8 @@ export const EngineAccessButton = (props: EngineAccessButtonProps) => { subheader={ { - PERMISSION_DESCRIPTION_MAP[ - name.toLowerCase() - ]?.readonly + PERMISSION_DESCRIPTION_MAP[type] + .readonly } } diff --git a/packages/client/src/components/engine/EngineShell.tsx b/packages/client/src/components/engine/EngineShell.tsx index 2df94c636b..3d28bc36f5 100644 --- a/packages/client/src/components/engine/EngineShell.tsx +++ b/packages/client/src/components/engine/EngineShell.tsx @@ -156,7 +156,7 @@ export const EngineShell = (props: EngineShellProps) => {   - + {role === 'OWNER' && ( + + + + ); +}; diff --git a/packages/client/src/components/settings/MembersAddOverlayUser.tsx b/packages/client/src/components/settings/MembersAddOverlayUser.tsx new file mode 100644 index 0000000000..e2d75e9645 --- /dev/null +++ b/packages/client/src/components/settings/MembersAddOverlayUser.tsx @@ -0,0 +1,137 @@ +import { styled, Avatar, Typography, Stack } from '@semoss/ui'; + +const StyledUser = styled(Stack)(({ theme }) => ({ + paddingTop: theme.spacing(1), + paddingRight: theme.spacing(2), + paddingBottom: theme.spacing(1), + paddingLeft: theme.spacing(2), +})); + +const StyledAvatar = styled(Avatar)({ + height: '32px', + width: '32px', +}); + +// TODO:Refactor when Typography coloris updated +const StyledPrimaryText = styled(Typography)(({ theme }) => ({ + color: theme.palette.text.primary, +})); + +const StyledSecondaryText = styled(Typography)(({ theme }) => ({ + color: theme.palette.text.secondary, +})); + +interface MembersAddOverlayUserProps { + /** + * Name of the user + */ + name: string; + + /** + * Name of the user + */ + id: string; + + /** + * Email of the user + */ + email: string; + + /** + * Type of the user + */ + type: string; + + /** + * Optional action to render + */ + action?: React.ReactNode; +} + +/** + * @name extractInitials + * + * Extract a initials for a string + * + * @param str + */ +const extractInitials = (str: string): string => { + if (str.length < 1) { + return ''; + } + + return str.split(' ').reduce((prev, curr) => { + return prev + (curr[0] || ''); + }, ''); +}; + +export const MembersAddOverlayUser = (props: MembersAddOverlayUserProps) => { + const { name, id, email, type, action } = props; + + const initials = extractInitials(name); + + return ( + + {initials} + + + {name || <> } + + + + + User ID: + + + {id || <> } + + + + + Email: + + + {email || <> } + + + + + Type: + + + {type || <> } + + + + + {action} + + ); +}; diff --git a/packages/client/src/components/settings/MembersDeleteOverlay.tsx b/packages/client/src/components/settings/MembersDeleteOverlay.tsx new file mode 100644 index 0000000000..d812e43b21 --- /dev/null +++ b/packages/client/src/components/settings/MembersDeleteOverlay.tsx @@ -0,0 +1,142 @@ +import { Button, Modal, useNotification } from '@semoss/ui'; +import { AxiosResponse } from 'axios'; + +import { ALL_TYPES } from '@/types'; +import { useRootStore, useSettings } from '@/hooks'; + +import { SETTINGS_PROVISIONED_USER } from './settings.types'; + +interface MembersDeleteOverlayProps { + /** + * Type of engine + */ + type: ALL_TYPES; + + /** + * ID of the app or engine being edited + */ + id: string; + + /** + * Members + */ + members: SETTINGS_PROVISIONED_USER[]; + + /** + * Track if the model is open or close + */ + open: boolean; + + /** + * Called on close + * + * @returns - method that is called onClose + */ + onClose: (success: boolean) => void; +} + +export const MembersDeleteOverlay = (props: MembersDeleteOverlayProps) => { + const { + type, + id, + members = [], + open = false, + onClose = () => null, + } = props; + + const { monolithStore } = useRootStore(); + const notification = useNotification(); + const { adminMode } = useSettings(); + + /** + * @name deleteSelectedMembers + * @param members - members that will be deleted + * + * delete the selected members from the app or engine + */ + const deleteSelectedMembers = async ( + members: SETTINGS_PROVISIONED_USER[], + ) => { + let success = false; + + try { + // construct requests for post data + const requests = members.map((m) => { + return m.id; + }); + + let response: AxiosResponse<{ success: boolean }> | null = null; + if ( + type === 'DATABASE' || + type === 'STORAGE' || + type === 'MODEL' || + type === 'VECTOR' || + type === 'FUNCTION' + ) { + response = await monolithStore.removeEngineUserPermissions( + adminMode, + id, + requests, + ); + } else if (type === 'APP') { + response = await monolithStore.removeProjectUserPermissions( + adminMode, + id, + requests, + ); + } + + if (!response) { + return; + } + + // ignore if there is no response + if (response.data.success) { + notification.add({ + color: 'success', + message: `Successfully removed ${ + requests.length > 1 ? 'members' : 'member' + }`, + }); + + success = true; + } else { + notification.add({ + color: 'error', + message: `Error changing user permissions`, + }); + } + } catch (e) { + notification.add({ + color: 'error', + message: String(e), + }); + } finally { + // close the overlay + onClose(success); + } + }; + + return ( + + Are you sure? + + Would you like to delete all selected members + + + + + + + ); +}; diff --git a/packages/client/src/components/settings/MembersTable.tsx b/packages/client/src/components/settings/MembersTable.tsx index a08f469db9..cbfcdfa64b 100644 --- a/packages/client/src/components/settings/MembersTable.tsx +++ b/packages/client/src/components/settings/MembersTable.tsx @@ -1,5 +1,4 @@ -import { useEffect, useMemo, useState, useRef } from 'react'; -import { useForm, useFieldArray } from 'react-hook-form'; +import { useEffect, useMemo, useState, useRef, useLayoutEffect } from 'react'; import { styled, Button, @@ -8,44 +7,22 @@ import { IconButton, AvatarGroup, Avatar, - Modal, RadioGroup, Typography, - Autocomplete, - Card, - Box, - Chip, - Icon, - Link, Search, - Select, Stack, useNotification, } from '@semoss/ui'; -import { - Delete, - EditRounded, - RemoveRedEyeRounded, - ClearRounded, -} from '@mui/icons-material'; +import { Delete } from '@mui/icons-material'; import { AxiosResponse } from 'axios'; -import { useRootStore, useAPI, useSettings } from '@/hooks'; +import { ALL_TYPES } from '@/types'; +import { useRootStore, useAPI, useSettings, useDebounceValue } from '@/hooks'; import { LoadingScreen } from '@/components/ui'; -import { SETTINGS_MODE, SETTINGS_ROLE } from './settings.types'; - -import { PERMISSION_DESCRIPTION_MAP } from './member-permissions.constants'; -import { TextField } from '@mui/material'; +import { SETTINGS_PROVISIONED_USER } from './settings.types'; -const colors = [ - '#22A4FF', - '#FA3F20', - '#FA3F20', - '#FF9800', - '#FF9800', - '#22A4FF', - '#4CAF50', -]; +import { MembersDeleteOverlay } from './MembersDeleteOverlay'; +import { MembersAddOverlay } from './MembersAddOverlay'; const UserInfoTableCell = styled(Table.Cell)({ display: 'flex', @@ -84,7 +61,17 @@ const StyledTableContainer = styled(Table.Container)(({ theme }) => ({ border: `1px solid ${theme.palette.secondary.border}`, })); -const StyledMemberTable = styled(Table)({ backgroundColor: 'white' }); +const StyledMemberLoading = styled('div')(({ theme }) => ({ + position: 'relative', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: '160px', +})); + +const StyledMemberTable = styled(Table)({ + backgroundColor: 'white', +}); const StyledTableTitleContainer = styled('div')({ display: 'flex', @@ -158,25 +145,15 @@ const StyledAddMemberContainer = styled('div')({ gap: '10px', }); -const StyledNoMembersContainer = styled('div')(({ theme }) => ({ - width: '100%', - borderRadius: '12px', - border: `1px solid ${theme.palette.secondary.border}`, -})); - -const StyledNoMembersDiv = styled('div')({ +const StyledNoMembersDiv = styled('div')(({ theme }) => ({ width: '100%', height: '503px', display: 'flex', flexDirection: 'column', - gap: '1rem', + gap: theme.spacing(1), justifyContent: 'center', alignItems: 'center', -}); - -const StyledCard = styled(Card)({ - borderRadius: '12px', -}); +})); const StyledTableCell = styled(Table.Cell)({ paddingLeft: '16px', @@ -186,75 +163,6 @@ const StyledCheckbox = styled(Checkbox)({ paddingBottom: '0px', }); -const StyledModalContentText = styled(Modal.ContentText)({ - display: 'flex', - flexDirection: 'column', - gap: '.5rem', - marginTop: '12px', -}); - -const StyledCardHeader = styled(Card.Header)({ - color: '#000', - width: '100%', -}); - -const StyledIconButton = styled(IconButton)({ - marginTop: '16px', - color: 'rgba(0, 0, 0, 0.7)', - marginRight: '24px', -}); - -const StyledOuterBox = styled('div', { - shouldForwardProp: (prop) => prop !== 'userLength', -})<{ - userLength: number; -}>(({ userLength }) => ({ - maxHeight: userLength > 2 ? '300px' : 'auto', - overflow: 'auto', - transition: 'max-height 0.3s ease', -})); - -const StyledFlexBox = styled(Box, { - shouldForwardProp: (prop) => prop !== 'index', -})<{ - index: number; -}>(({ index }) => ({ - display: 'flex', - justifyContent: 'left', - alignItems: 'center', - backgroundColor: index % 2 !== 0 ? 'rgba(0, 0, 0, 0.03)' : 'transparent', -})); - -const StyledCenterBox = styled(Box)({ - display: 'flex', - justifyContent: 'center', - marginTop: '6px', - marginLeft: '8px', - marginRight: '8px', -}); - -const StyledAvatarBox = styled(Box)({ - display: 'flex', - height: '80px', - width: '80px', - justifyContent: 'center', - alignItems: 'center', - border: '0.5px solid rgba(0, 0, 0, 0.05)', - borderRadius: '50%', -}); - -const StyledUserAvatar = styled(Avatar, { - shouldForwardProp: (prop) => prop !== 'userColor', -})<{ - userColor: string; -}>(({ userColor }) => ({ - display: 'flex', - width: '60px', - height: '60px', - fontSize: '24px', - backgroundColor: userColor, -})); - // maps for permissions, const permissionMapper = { 1: 'Author', // BE: 'DISPLAY' @@ -268,162 +176,114 @@ const permissionMapper = { 'Read-Only': 'READ_ONLY', // DISPLAY: BE }; -// Members Table -interface Member { - id: string; - name: string; - type: string; - EMAIL: string; - SELECTED: boolean; - permission: string; - OG_PERMISSION?: string; - CONFIRM_DELETE?: boolean; -} - interface MembersTableProps { /** - * Type of setting - */ - mode: SETTINGS_MODE; - - /** - * Id of the setting + * Id of the engine */ id: string; /** - * Name for the item of the setting + * Type of the engine */ - name: string; + type: ALL_TYPES; /** - * Condensed view + * Called when permissions are changed */ - condensed?: boolean; - - refreshPermission?: () => void; + onChange?: () => void; } export const MembersTable = (props: MembersTableProps) => { - const { - mode, - id, - name, - condensed, - refreshPermission = () => console.log('pass refresh function'), - } = props; + const { id, type, onChange = () => null } = props; const { monolithStore } = useRootStore(); const notification = useNotification(); const { adminMode } = useSettings(); /** Member Table State */ - const [membersCount, setMembersCount] = useState(0); - const [filteredMembersCount, setFilteredMembersCount] = useState(0); - const [membersPage, setMembersPage] = useState(1); - const [limit, setLimit] = useState(5); - const [selectedMembers, setSelectedMembers] = useState([]); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + const [search, setSearch] = useState(''); + const [permissionFilter, setPermissionFilter] = useState(''); + const [selectedMembers, setSelectedMembers] = useState< + SETTINGS_PROVISIONED_USER[] + >([]); + + // debounce the input + const debouncedSearch = useDebounceValue(search); /** Delete Member */ const [deleteMembersModal, setDeleteMembersModal] = useState(false); - const [deleteMemberModal, setDeleteMemberModal] = useState(false); - const [userToDelete, setUserToDelete] = useState(); + const [pendingDeletedMembers, setPendingDeletedMembers] = useState< + SETTINGS_PROVISIONED_USER[] + >([]); /** Add Member State */ const [addMembersModal, setAddMembersModal] = useState(false); - const [nonCredentialedUsers, setNonCredentialedUsers] = useState([]); - const [selectedNonCredentialedUsers, setSelectedNonCredentialedUsers] = - useState([]); - const [addMemberRole, setAddMemberRole] = useState(); const memberSearchRef = useRef(undefined); - const didMount = useRef(false); - - const { control, watch, setValue } = useForm<{ - MEMBERS: Member[]; - - SEARCH_FILTER: string; - ACCESS_FILTER: string; - }>({ - defaultValues: { - // Members Table - MEMBERS: [], - // Filters for Members table - SEARCH_FILTER: '', - ACCESS_FILTER: '', - }, - }); - - const { remove: memberRemove } = useFieldArray({ - control, - name: 'MEMBERS', - }); - - const searchFilter = watch('SEARCH_FILTER'); - const permissionFilter = watch('ACCESS_FILTER'); - const verifiedMembers = watch('MEMBERS'); // get the api const getMembersApi: Parameters[0] = - mode === 'engine' + type === 'DATABASE' || + type === 'STORAGE' || + type === 'MODEL' || + type === 'VECTOR' || + type === 'FUNCTION' ? [ 'getEngineUsers', adminMode, id, - searchFilter ? searchFilter : undefined, + debouncedSearch ? debouncedSearch : undefined, permissionMapper[permissionFilter], - membersPage * limit - limit, // offset - limit, + (page + 1) * rowsPerPage - rowsPerPage, // offset + rowsPerPage, // limit ] - : mode === 'app' + : type === 'APP' ? [ 'getProjectUsers', adminMode, id, - searchFilter ? searchFilter : undefined, + debouncedSearch ? debouncedSearch : undefined, permissionMapper[permissionFilter], - membersPage * limit - limit, // offset - limit, + (page + 1) * rowsPerPage - rowsPerPage, // offset + rowsPerPage, // limit ] : null; const getMembers = useAPI(getMembersApi); /** - * @name useEffect - * @desc - sets members in react hook form - */ + * When + **/ useEffect(() => { if (getMembers.status !== 'SUCCESS' || !getMembers.data) { return; } - const members = []; - - getMembers.data['members'].forEach((mem) => { - members.push(mem); - }); + setPage(0); + setSelectedMembers([]); - setValue('MEMBERS', members); - - if (!didMount.current) { - // set total members - setMembersCount(getMembers.data['totalMembers']); - didMount.current = true; - } + // select the member when done mounting + memberSearchRef.current?.focus(); + }, [getMembers.status, getMembers.data]); - // Needed for total pages on pagination - setFilteredMembersCount(getMembers.data['totalMembers']); + // useLayoutEffect(() => { + // if (getMembers.status !== 'SUCCESS' || !getMembers.data) { + // return; + // } - memberSearchRef.current?.focus(); - return () => { - setValue('MEMBERS', []); - setSelectedMembers([]); - }; - }, [getMembers.status, getMembers.data, searchFilter, permissionFilter]); + // // select the member when done mounting + // memberSearchRef.current?.focus(); + // }, [getMembers.status, getMembers.data]); - /** MEMBER TABLE FUNCTIONS */ + /** + * Update the selected users + * @param members + * @param quickUpdate + * @returns + */ const updateSelectedUsers = async (members, quickUpdate) => { try { // construct requests for post data @@ -444,13 +304,19 @@ export const MembersTable = (props: MembersTableProps) => { } let response: AxiosResponse<{ success: boolean }> | null = null; - if (mode === 'engine') { + if ( + type === 'DATABASE' || + type === 'STORAGE' || + type === 'MODEL' || + type === 'VECTOR' || + type === 'FUNCTION' + ) { response = await monolithStore.editEngineUserPermissions( adminMode, id, requests, ); - } else if (mode === 'app') { + } else if (type === 'APP') { response = await monolithStore.editProjectUserPermissions( adminMode, id, @@ -469,7 +335,10 @@ export const MembersTable = (props: MembersTableProps) => { message: 'Succesfully updated user permissions', }); - refreshPermission(); + // refresh the members + getMembers.refresh(); + + onChange(); } else { notification.add({ color: 'error', @@ -481,245 +350,62 @@ export const MembersTable = (props: MembersTableProps) => { color: 'error', message: String(e), }); - } finally { - // refresh the members - getMembers.refresh(); } }; /** - * @name deleteSelectedMembers - * @param members + * Open the delete modal + * + * @param members - members that will be deleted */ - const deleteSelectedMembers = async (members: Member[]) => { - try { - // construct requests for post data - const requests = members.map((m, i) => { - return m.id; - }); - - if (requests.length === 0) { - notification.add({ - color: 'warning', - message: `No permissions to change`, - }); - - return; - } - - let response: AxiosResponse<{ success: boolean }> | null = null; - if (mode === 'engine') { - response = await monolithStore.removeEngineUserPermissions( - adminMode, - id, - requests, - ); - } else if (mode === 'app') { - response = await monolithStore.removeProjectUserPermissions( - adminMode, - id, - requests, - ); - } - - if (!response) { - return; - } - - // ignore if there is no response - if (response.data.success) { - if ( - verifiedMembers.length === requests.length && - membersPage !== 1 && - membersPage !== filteredMembersCount / limit - ) { - setMembersPage(membersPage - 1); - } - - // get index of members in order to remove - const indexesToRemove = []; - requests.forEach((mem) => { - verifiedMembers.find((m, i) => { - if (mem === m.id) indexesToRemove.push(i); - }); - }); - - // remove indexes from react hook form - memberRemove(indexesToRemove); - - const newMemberCount = membersCount - requests.length; - setMembersCount(newMemberCount); - - // Clean selected Members in state - if (!userToDelete) { - setSelectedMembers([]); - setDeleteMembersModal(false); - } else { - // Quick Delete one member - const filteredSelectedMembers = selectedMembers.filter( - // find the single member that is being deleted and remove from selected members - (m) => m.id !== userToDelete.id, - ); - - // set new selected members - setSelectedMembers(filteredSelectedMembers); - setDeleteMemberModal(false); - } - - notification.add({ - color: 'success', - message: `Successfully removed ${ - requests.length > 1 ? 'members' : 'member' - }`, - }); - } else { - notification.add({ - color: 'error', - message: `Error changing user permissions`, - }); - } - } catch (e) { + const openDeleteMembersModal = (members: SETTINGS_PROVISIONED_USER[]) => { + // notify if no members + if (members.length === 0) { notification.add({ - color: 'error', - message: String(e), + color: 'warning', + message: `No permissions to change`, }); - } finally { - // refresh the members - getMembers.refresh(); - } - }; - /** ADD MEMBER FUNCTIONS */ - /** - * @name getUsersNoCreds - * @desc Gets all users without credentials - */ - const getUsersNoCreds = async () => { - try { - let response: AxiosResponse[]> | null = - null; - if (mode === 'engine') { - // possibly add more db table columns / keys here to get id type for display under username - response = await monolithStore.getEngineUsersNoCredentials( - adminMode, - id, - ); - } else if (mode === 'app') { - response = await monolithStore.getProjectUsersNoCredentials( - adminMode, - id, - ); - } else { - return; - } + return; + } - // ignore if there is no response - if (response.data) { - const users = response.data.map((val) => { - return { - ...val, - color: colors[ - Math.floor(Math.random() * colors.length) - ], - }; - }); + // set the pending members + setPendingDeletedMembers(members); - setNonCredentialedUsers(users); - setAddMembersModal(true); - } - } catch (e) { - notification.add({ - color: 'error', - message: String(e), - }); - } + // close the model + setDeleteMembersModal(true); }; /** - * @name submitNonCredUsers + * Open the add modal */ - const submitNonCredUsers = async () => { - try { - // construct requests for post data - const requests = selectedNonCredentialedUsers.map((m) => { - return { - userid: m.id, - permission: permissionMapper[addMemberRole], - }; - }); - - if (requests.length === 0) { - notification.add({ - color: 'warning', - message: `No permissions to change`, - }); - - return; - } - - let response: AxiosResponse<{ success: boolean }> | null = null; - if (mode === 'engine') { - response = await monolithStore.addEngineUserPermissions( - adminMode, - id, - requests, - ); - } else if (mode === 'app') { - response = await monolithStore.addProjectUserPermissions( - adminMode, - id, - requests, - ); - } - - if (!response) { - return; - } - - // ignore if there is no response - if (response.data.success) { - setMembersCount( - membersCount + selectedNonCredentialedUsers.length, - ); - setAddMembersModal(false); - setSelectedNonCredentialedUsers([]); - setAddMemberRole(undefined); - - notification.add({ - color: 'success', - message: 'Successfully added member permissions', - }); - } else { - notification.add({ - color: 'error', - message: `Error changing user permissions`, - }); - } - } catch (e) { - setAddMembersModal(false); - setSelectedNonCredentialedUsers([]); - setAddMemberRole(undefined); - - notification.add({ - color: 'error', - message: String(e), - }); - } finally { - // refresh the members - getMembers.refresh(); - } + const openAddMembersModal = () => { + // close the model + setAddMembersModal(true); }; - /** HELPERS */ + // track if the page is loading + const isLoading = + getMembers.status === 'INITIAL' || getMembers.status === 'LOADING'; + const renderedMembers = + getMembers.status === 'SUCCESS' ? getMembers.data['members'] : []; + const totalMembers = + getMembers.status === 'SUCCESS' ? getMembers.data['totalMembers'] : 0; + const hasMembers = + getMembers.status === 'SUCCESS' && getMembers.data['totalMembers'] > 0; + + // Avatars rendered const Avatars = useMemo(() => { - if (!verifiedMembers.length) return []; + if (!renderedMembers.length) { + return []; + } let i = 0; const avatarList = []; - while (i < 5 && i < verifiedMembers.length) { + while (i < 5 && i < renderedMembers.length) { avatarList.push( - {verifiedMembers[i].name.charAt(0).toUpperCase()} + {(renderedMembers[i].name || ' ').charAt(0).toUpperCase()} , ); @@ -727,880 +413,354 @@ export const MembersTable = (props: MembersTableProps) => { } return avatarList; - }, [filteredMembersCount, verifiedMembers.length]); - - const paginationOptions = { - membersPageCounts: [5], - }; - - membersCount > 9 && paginationOptions.membersPageCounts.push(10); - membersCount > 19 && paginationOptions.membersPageCounts.push(20); - - /** END OF HELPERS */ - - /** LOADING */ - if (getMembers.status !== 'SUCCESS' && !didMount.current) { - return ; - } + }, [renderedMembers.length]); return ( - {!condensed ? ( - - {membersCount > 0 ? ( - - - - - Members - - - - - {Avatars.length > 0 ? ( - - - {Avatars.map((el) => { - return el; - })} - - - ) : null} - - - - {filteredMembersCount} Members - - - - - - {/* - - - - */} - - - { - setValue( - 'SEARCH_FILTER', - e.target.value, - ); - }} - /> - - - - {selectedMembers.length > 0 && ( - - )} - - - - - - - - - - 0 - } - onChange={() => { - if ( - selectedMembers.length !== - verifiedMembers.length - ) { - setSelectedMembers( - verifiedMembers, - ); - } else { - setSelectedMembers([]); - } - }} - /> - - - Name - - - Permission - - - Permission Date - - - Action - - - - - {verifiedMembers.map((x, i) => { - const user = verifiedMembers[i]; - - let isSelected = false; - - if (user) { - isSelected = selectedMembers.some( - (value) => { - return value.id === user.id; - }, - ); - } - if (user) { - return ( - - - { - if ( - isSelected - ) { - const selMembers = - []; - selectedMembers.forEach( - (u) => { - if ( - u.id !== - user.id - ) - selMembers.push( - u, - ); - }, - ); - setSelectedMembers( - selMembers, - ); - } else { - setSelectedMembers( - [ - ...selectedMembers, - user, - ], - ); - } - }} - /> - - - - - {user.name[0].toUpperCase()} - - - - - {user.name} - - - {`${user.type} ID: ${user.id}`} - - - - - { - updateSelectedUsers( - [user], - permissionMapper[ - e.target - .value - ], - ); - }} - > - - - - - - - Not Available - - - { - // set user - setUserToDelete( - user, - ); - // open modal - setDeleteMemberModal( - true, - ); - }} - > - - - - - ); - } else { - return ( - - - - - - - - ); - } - })} - - - - { - setMembersPage(v + 1); - setSelectedMembers([]); - }} - page={membersPage - 1} - rowsPerPage={5} - count={filteredMembersCount} - /> - - - - - ) : ( - - - - - Members + {Avatars.map((el) => { + return el; + })} + + + ) : null} + + + + {totalMembers} Members - - - - - No members present - + + + + + + { + setSearch(e.target.value); + }} + /> + + + + {selectedMembers.length > 0 && ( - - - )} - - ) : ( - <> -
- { - setValue('SEARCH_FILTER', e.target.value); - }} - /> - -
- - - - Name - Permission - {/* Action */} - - - - {verifiedMembers.map((x, i) => { - const user = verifiedMembers[i]; - - let isSelected = false; - - if (user) { - isSelected = selectedMembers.some( - (value) => { - return value.id === user.id; - }, - ); - } - if (user) { - return ( - - {/* leaving this in case we want to add separate columns for name, id, login type */} - {/* + + + + + + {isLoading ? ( + + + + + + ) : ( + <> + {hasMembers ? ( + + + + - {user.name} - */} - - - - - {user.name[0].toUpperCase()} - - - - {user.name} - - {`${user.type} ID: ${user.id}`} - - - - - - - {/* { - updateSelectedUsers( - [user], - permissionMapper[ - e.target.value - ], - ); - }} - sx={{ - flexWrap: 'nowrap', - WebkitFlexWrap: 'nowrap', - }} - > - - - - */} - {/* - { - // set user - setUserToDelete(user); - // open modal - setDeleteMemberModal(true); - }} - > - - - */} + + Name + + + Permission + + + Permission Date + + +   + - ); - } - })} - - - - { - setMembersPage(v + 1); - setSelectedMembers([]); - }} - page={membersPage - 1} - rowsPerPage={5} - count={filteredMembersCount} - /> - - - - - )} - - Are you sure? - - Would you like to delete all selected members - - - - - - - - - Are you sure? - - - - {userToDelete && ( - - This will remove {userToDelete.name} - - )} - - - - - - - - - - Add Members - - - - ` +${selectedNonCredentialedUsers.length - 2}` - } - value={[...selectedNonCredentialedUsers]} - getOptionLabel={(option: any) => { - return `${option.name}`; - }} - isOptionEqualToValue={(option, value) => { - return option.name === value.name; - }} - onChange={(event, newValue: any) => { - setSelectedNonCredentialedUsers([...newValue]); - }} - renderInput={(params) => ( - - )} - /> - - {selectedNonCredentialedUsers.map((user, idx) => ( - - - - - {user.name[0].toUpperCase() + - (user.name.indexOf(' ') > -1 - ? user.name[ - user.name.indexOf( - ' ', - ) + 1 - ].toUpperCase() - : '')} - - - - - {user.name} - - } - subheader={ - - - {`User ID: `} - - - {`• `} - - {`Email: `} - - {user.email} - - - - } - action={ - { - const filtered = - selectedNonCredentialedUsers.filter( - (val) => - val.id !== - user.id, - ); - setSelectedNonCredentialedUsers( - filtered, + + + {renderedMembers.map((x, i) => { + const user = renderedMembers[i]; + + let isSelected = false; + + if (user) { + isSelected = + selectedMembers.some( + (value) => { + return ( + value.id === + user.id + ); + }, ); - }} - > - - - } - /> - - ))} - - - - Permissions - - - { - const val = e.target.value; - if (val) { - setAddMemberRole(val as SETTINGS_ROLE); - } - }} - > - - - - - A - - Author - - } - sx={{ color: '#000' }} - subheader={ - - {PERMISSION_DESCRIPTION_MAP[ - name.toLowerCase() - ]?.author || - `Error: update key in test-editor.constants to "${name}"`} - - } - action={ - - } - /> - - - - - - - Editor - - } - sx={{ color: '#000' }} - subheader={ - - {PERMISSION_DESCRIPTION_MAP[ - name.toLowerCase() - ]?.editor || - `Error: update key in test-editor.constants to "${name}"`} - - } - action={ - } - /> - - - - - - - Read-Only - - } - sx={{ color: '#000' }} - subheader={ - - {PERMISSION_DESCRIPTION_MAP[ - name.toLowerCase() - ]?.readonly || - `Error: update key in test-editor.constants to "${name}"`} - - } - action={ - + + if (user) { + return ( + + + { + if ( + isSelected + ) { + const selMembers = + []; + selectedMembers.forEach( + ( + u, + ) => { + if ( + u.id !== + user.id + ) + selMembers.push( + u, + ); + }, + ); + setSelectedMembers( + selMembers, + ); + } else { + setSelectedMembers( + [ + ...selectedMembers, + user, + ], + ); + } + }} + /> + + + + + {user.name[0].toUpperCase()} + + + + + {user.name} + + + {`${user.type} ID: ${user.id}`} + + + + + { + updateSelectedUsers( + [user], + permissionMapper[ + e + .target + .value + ], + ); + }} + > + + + + + + + Not Available + + + { + openDeleteMembersModal( + [user], + ); + }} + > + + + + + ); } - /> - -
- - - - - - - - - + + return null; + })} + + + + { + setPage(v); + setSelectedMembers([]); + }} + page={page} + rowsPerPage={rowsPerPage} + rowsPerPageOptions={[5, 10, 20]} + onRowsPerPageChange={(e) => { + // set the new limit + setRowsPerPage( + parseInt( + e.target.value, + 10, + ), + ); + }} + count={totalMembers} + /> + + + + ) : ( + + + No members + + + + )} + + )} + + + { + // clear out the deleted members + setPendingDeletedMembers([]); + + // close the model + setDeleteMembersModal(false); + + // refresh if successful + if (success) { + // trigger the update + onChange(); + + // refresh + getMembers.refresh(); + } + }} + /> + { + // clear out the deleted members + setAddMembersModal(false); + + // refresh if successful + if (success) { + // trigger the update + onChange(); + + getMembers.refresh(); + } + }} + /> ); }; diff --git a/packages/client/src/components/settings/PendingMembersTable.tsx b/packages/client/src/components/settings/PendingMembersTable.tsx index 0a86248aa4..9f8679b205 100644 --- a/packages/client/src/components/settings/PendingMembersTable.tsx +++ b/packages/client/src/components/settings/PendingMembersTable.tsx @@ -1,5 +1,4 @@ import { useEffect, useState } from 'react'; -import { useForm, useFieldArray } from 'react-hook-form'; import { styled, Button, @@ -17,8 +16,18 @@ import { Add, Check, Close, ExpandLess, ExpandMore } from '@mui/icons-material'; import { AxiosResponse } from 'axios'; import { useRootStore, usePixel, useSettings } from '@/hooks'; +import { ALL_TYPES } from '@/types'; +import { LoadingScreen } from '@/components/ui'; -import { SETTINGS_ROLE, SETTINGS_MODE } from './settings.types'; +import { SETTINGS_ROLE, SETTINGS_PENDING_USER } from './settings.types'; + +const StyledMemberLoading = styled('div')(({ theme }) => ({ + position: 'relative', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: '160px', +})); const StyledMemberContent = styled('div')({ display: 'flex', @@ -129,124 +138,81 @@ const permissionMapper = { 'Read-Only': 'READ_ONLY', // DISPLAY: BE }; -// Pending Members Table -interface PendingMember { - ID: string; - NAME: string; - EMAIL: string; - PERMISSION: SETTINGS_ROLE; - // Requester Info - REQUEST_TIMESTAMP: string; - REQUEST_TYPE: string; - REQUEST_USERID: string; -} - const StyledNoPendingReqs = styled('div')(({ theme }) => ({ width: '100%', - height: theme.spacing(6), + height: '503px', display: 'flex', + flexDirection: 'column', + gap: theme.spacing(1), justifyContent: 'center', alignItems: 'center', - backgroundColor: 'white', })); interface PendingMemberTableProps { /** - * Mode of setting + * Id of the engine */ - mode: SETTINGS_MODE; + id: string; /** - * Id of the setting + * Type of the engine */ - id: string; + type: ALL_TYPES; + + /** + * Called when permissions are changed + */ + onChange?: () => void; } export const PendingMembersTable = (props: PendingMemberTableProps) => { - const { mode, id } = props; + const { id, type, onChange = () => null } = props; const { monolithStore } = useRootStore(); const notification = useNotification(); const { adminMode } = useSettings(); - const [selectedPending, setSelectedPending] = useState([]); + const [renderedMembers, setRenderedMembers] = useState< + SETTINGS_PENDING_USER[] + >([]); + const [selectedMembers, setSelectedMembers] = useState< + Record + >({}); const [openTable, setOpenTable] = useState(false); - const { control, watch, setValue } = useForm<{ - PENDING_MEMBERS: PendingMember[]; - }>({ - defaultValues: { - // Members Table - PENDING_MEMBERS: [], - }, - }); - - const { remove: pendingMemberRemove } = useFieldArray({ - control, - name: 'PENDING_MEMBERS', - }); - const pendingMembers = watch('PENDING_MEMBERS'); - - useEffect(() => { - if (pendingMembers.length) { - setOpenTable(true); - } - }, [pendingMembers]); - const pendingUserAccessPixel = - mode === 'engine' + type === 'DATABASE' || + type === 'STORAGE' || + type === 'MODEL' || + type === 'VECTOR' || + type === 'FUNCTION' ? `GetEngineUserAccessRequest(engine='${id}');` - : mode === 'app' + : type === 'APP' ? `GetProjectUserAccessRequest(project='${id}')` : ''; // Pending Member Requests Pixel call - const pendingUserAccess = usePixel< - { - ENGINEID: string; - ID: string; - PERMISSION: number; - REQUEST_TIMESTAMP: string; - REQUEST_TYPE: string; - REQUEST_USERID: string; - }[] - >(pendingUserAccessPixel); + const pendingUserAccess = usePixel( + pendingUserAccessPixel, + ); - /** - * @name useEffect - * @desc - sets pending members in react hook form - */ + // track if the page is loading + const isLoading = + pendingUserAccess.status === 'INITIAL' || + pendingUserAccess.status === 'LOADING'; + + // set the rendered users useEffect(() => { - // pixel call to get pending members - if (pendingUserAccess.status !== 'SUCCESS' || !pendingUserAccess.data) { + if (pendingUserAccess.status !== 'SUCCESS') { return; } - const newPendingMembers = []; + const updatedMembers = pendingUserAccess.data.map((m) => ({ + ...m, + PERMISSION: permissionMapper[m.PERMISSION], // comes in as 1,2,3 -> map to Author, Edit, Read-only + })); - pendingUserAccess.data.forEach((mem) => { - newPendingMembers.push({ - ...mem, - PERMISSION: permissionMapper[mem.PERMISSION], // comes in as 1,2,3 -> map to Author, Edit, Read-only - }); - }); - - // set new members with the Pending Members in react hook form - setValue('PENDING_MEMBERS', newPendingMembers); - - // notify user for pending members - if (newPendingMembers.length) { - let message = - newPendingMembers.length === 1 - ? `1 member has ` - : `${newPendingMembers.length} members have `; - - message += `requested access`; - } - - return () => { - // TODO - }; + setRenderedMembers(updatedMembers); }, [pendingUserAccess.status, pendingUserAccess.data]); /** API Functions */ @@ -255,10 +221,7 @@ export const PendingMembersTable = (props: PendingMemberTableProps) => { * @param members - members to pass to approve api call * @description Approve list of Pending Members */ - const approvePendingMembers = async ( - members: PendingMember[], - quickActionFlag?: boolean, // quick approve button - ) => { + const approvePendingMembers = async (members: SETTINGS_PENDING_USER[]) => { try { // construct requests for post data const requests = members.map((mem, i) => { @@ -279,13 +242,19 @@ export const PendingMembersTable = (props: PendingMemberTableProps) => { } let response: AxiosResponse<{ success: boolean }> | null = null; - if (mode === 'engine') { + if ( + type === 'DATABASE' || + type === 'STORAGE' || + type === 'MODEL' || + type === 'VECTOR' || + type === 'FUNCTION' + ) { response = await monolithStore.approveEngineUserAccessRequest( adminMode, id, requests, ); - } else if (mode === 'app') { + } else if (type === 'APP') { response = await monolithStore.approveProjectUserAccessRequest( adminMode, id, @@ -298,40 +267,24 @@ export const PendingMembersTable = (props: PendingMemberTableProps) => { return; } - // if (response.success) { - // get index of pending members in order to remove - const indexesToRemove = []; - requests.forEach((mem) => { - pendingMembers.find((m, i) => { - if (mem.userid === m.REQUEST_USERID) - indexesToRemove.push(i); - }); - }); - - // remove indexes - pendingMemberRemove(indexesToRemove); + if (response.data.success) { + const updatedMembers = { + ...selectedMembers, + } as Record; - if (!quickActionFlag) { - // remove from selected pending members - setSelectedPending([]); - } else { - let indexToRemoveFromSelected; - // remove from selected - selectedPending.find((m, i) => { - if (m.ID !== requests[0].requestid) { - indexToRemoveFromSelected = i; + for (const m of members) { + if (updatedMembers[m.ID]) { + delete updatedMembers[m.ID]; } - }); + } + setSelectedMembers(updatedMembers); - const filteredArr = selectedPending.splice( - indexToRemoveFromSelected, - 1, - ); + // refresh the data + pendingUserAccess.refresh(); - setSelectedPending(filteredArr); - } + // trigger onChange + onChange(); - if (response.data.success) { notification.add({ color: 'success', message: 'Succesfully approved user permissions', @@ -356,10 +309,7 @@ export const PendingMembersTable = (props: PendingMemberTableProps) => { * @param quickActionFlag - quick deny button on table * @description Deny Selected Pending Members */ - const denyPendingMembers = async ( - members: PendingMember[], - quickActionFlag?: boolean, - ) => { + const denyPendingMembers = async (members: SETTINGS_PENDING_USER[]) => { try { // construct requests for post data const requests = members.map((m) => { @@ -376,13 +326,19 @@ export const PendingMembersTable = (props: PendingMemberTableProps) => { } let response: AxiosResponse<{ success: boolean }> | null = null; - if (mode === 'engine') { + if ( + type === 'DATABASE' || + type === 'STORAGE' || + type === 'MODEL' || + type === 'VECTOR' || + type === 'FUNCTION' + ) { response = await monolithStore.denyEngineUserAccessRequest( adminMode, id, requests, ); - } else if (mode === 'app') { + } else if (type === 'APP') { response = await monolithStore.denyProjectUserAccessRequest( adminMode, id, @@ -395,42 +351,24 @@ export const PendingMembersTable = (props: PendingMemberTableProps) => { return; } - // get index of pending members in order to remove - const indexesToRemove = []; - requests.forEach((mem) => { - pendingMembers.find((m, i) => { - if (mem === m.ID) indexesToRemove.push(i); - }); - }); - - // remove indexes from react hook form - pendingMemberRemove(indexesToRemove); + if (response.data.success) { + const updatedMembers = { + ...selectedMembers, + } as Record; - if (!quickActionFlag) { - setSelectedPending([]); - // close modal - // setDenySelectedModal(false); - } else { - // remove from selected pending members - let indexToRemoveFromSelected = 0; - // remove from selected - selectedPending.find((m, i) => { - if (m.ID !== requests[0]) { - indexToRemoveFromSelected = i; + for (const m of members) { + if (updatedMembers[m.ID]) { + delete updatedMembers[m.ID]; } - }); + } + setSelectedMembers(updatedMembers); - const filteredArr = selectedPending.splice( - indexToRemoveFromSelected, - 1, - ); + // refresh the data + pendingUserAccess.refresh(); - setSelectedPending(filteredArr); - // close modal - // setDenySelectedModal(false); - } + // trigger onChange + onChange(); - if (response.data.success) { notification.add({ color: 'success', message: 'Succesfully denied user permissions', @@ -452,38 +390,26 @@ export const PendingMembersTable = (props: PendingMemberTableProps) => { /** HELPERS */ /** * @name updatePendingMemberPermission - * @param mem + * @param member * @param value * @desc Updates pending member permission in radiogroup */ const updatePendingMemberPermission = ( - mem: PendingMember, + member: SETTINGS_PENDING_USER, role: SETTINGS_ROLE, ) => { - const updatedPendingMems = pendingMembers.map((user) => { - if (user.REQUEST_USERID === mem.REQUEST_USERID) { + const updatedRenderedMembers = renderedMembers.map((m) => { + if (member.ID === m.ID) { return { - ...user, + ...m, PERMISSION: role, }; - } else { - return user; } - }); - const updateSelectedPendingMems = selectedPending.map((user) => { - if (user.REQUEST_USERID === mem.REQUEST_USERID) { - return { - ...user, - PERMISSION: role, - }; - } else { - return user; - } + return m; }); - setSelectedPending(updateSelectedPendingMems); - setValue('PENDING_MEMBERS', updatedPendingMems); + setRenderedMembers(updatedRenderedMembers); }; return ( @@ -501,9 +427,9 @@ export const PendingMembersTable = (props: PendingMemberTableProps) => { - {pendingMembers.length < 2 - ? `${pendingMembers.length} pending request` - : `${pendingMembers.length} pending requests`} + {renderedMembers.length < 2 + ? `${renderedMembers.length} pending request` + : `${renderedMembers.length} pending requests`} @@ -521,17 +447,20 @@ export const PendingMembersTable = (props: PendingMemberTableProps) => { - {selectedPending.length > 0 && ( + {Object.keys(selectedMembers).length > 0 && ( <> + + + + + ); +}; diff --git a/packages/client/src/components/settings/UserTable.tsx b/packages/client/src/components/settings/UserTable.tsx new file mode 100644 index 0000000000..fb058bf9af --- /dev/null +++ b/packages/client/src/components/settings/UserTable.tsx @@ -0,0 +1,520 @@ +import { useMemo, useRef, useState } from 'react'; +import { Delete, Edit } from '@mui/icons-material'; +import { + styled, + useNotification, + Button, + Checkbox, + Typography, + AvatarGroup, + Avatar, + Table, + IconButton, + Search, +} from '@semoss/ui'; +import { useRootStore, useAPI, useSettings, useDebounceValue } from '@/hooks'; +import { LoadingScreen } from '@/components/ui'; +import { UserAddOverlay } from './UserAddOverlay'; +import { UserTableUser } from './UserTableUser'; + +const StyledMemberContent = styled('div')({ + display: 'flex', + width: '100%', + flexDirection: 'column', + alignItems: 'flex-start', + gap: '25px', + flexShrink: '0', +}); + +const StyledMemberInnerContent = styled('div')({ + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + gap: '20px', + alignSelf: 'stretch', +}); + +const StyledTableContainer = styled(Table.Container)(({ theme }) => ({ + borderRadius: '12px', + border: `1px solid ${theme.palette.secondary.border}`, +})); + +const StyledMemberLoading = styled('div')(({ theme }) => ({ + position: 'relative', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: '160px', +})); + +const StyledMemberTable = styled(Table)({ + backgroundColor: 'white', +}); + +const StyledTableTitleContainer = styled('div')({ + display: 'flex', + alignItems: 'center', + alignSelf: 'stretch', + boxShadow: '0px -1px 0px 0px rgba(0, 0, 0, 0.12) inset', + backgroundColor: 'white', +}); + +const StyledTableTitleDiv = styled('div')({ + display: 'flex', + padding: '12px 24px 12px 16px', + alignItems: 'center', + gap: '10px', +}); + +const StyledTableTitleMemberContainer = styled('div')({ + display: 'flex', + alignItems: 'flex-start', + flex: '1 0 0', +}); + +const StyledAvatarGroupContainer = styled('div')({ + display: 'flex', + width: '130px', + height: '56px', + padding: '10px 16px', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + gap: '10px', +}); + +const StyledTableTitleMemberCountContainer = styled('div')({ + display: 'flex', + height: '56px', + padding: '6px 16px 6px 8px', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + gap: '10px', +}); + +const StyledTableTitleMemberCount = styled('div')({ + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', +}); + +const StyledSearchButtonContainer = styled('div')({ + display: 'flex', + alignItems: 'center', + // gap: '10px', +}); + +const StyledAddMemberContainer = styled('div')({ + display: 'flex', + padding: '10px 24px 10px 8px', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + gap: '10px', +}); + +const StyledNoUsersDiv = styled('div')(({ theme }) => ({ + width: '100%', + height: '503px', + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(1), + justifyContent: 'center', + alignItems: 'center', +})); + +interface User { + id: string; + type: string; + name?: string; + admin?: boolean; + publisher?: boolean; + exporter?: boolean; + email?: string; + phone?: string; + phoneextension?: string; + countrycode?: string; + username?: string; +} + +interface UserTableProps { + /** + * Called users are changed + */ + onChange?: () => void; +} + +export const UserTable = (props: UserTableProps) => { + const { onChange = () => null } = props; + + const { adminMode } = useSettings(); + const { monolithStore } = useRootStore(); + const notification = useNotification(); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + const [search, setSearch] = useState(''); + + // debounce the input + const debouncedSearch = useDebounceValue(search); + + /** Add User State */ + const [addModalOpen, setAddModalOpen] = useState(false); + const [addModalUser, setAddModalUser] = useState(null); + + const userSearchRef = useRef(undefined); + + const getUsers = useAPI([ + 'getAllUsers', + adminMode, + debouncedSearch ? debouncedSearch : '', + (page + 1) * rowsPerPage - rowsPerPage, // offset + rowsPerPage, // limit + ]); + + // track if the page is loading + const isLoading = + getUsers.status === 'INITIAL' || getUsers.status === 'LOADING'; + const renderedUsers = getUsers.status === 'SUCCESS' ? getUsers.data : []; + const totalUsers = getUsers.status === 'SUCCESS' ? 0 : 0; + const hasUsers = getUsers.status === 'SUCCESS' && getUsers.data.length > 0; + /** + * Update a user + * @param user - user to update + */ + const updateUser = async (user: User) => { + try { + const response = await monolithStore.editMemberInfo( + adminMode, + user, + ); + + if (!response) { + return; + } + + // ignore if there is no response + if (response.data) { + notification.add({ + color: 'success', + message: 'Succesfully updated user', + }); + + onChange(); + + // refresh the users + getUsers.refresh(); + } else { + notification.add({ + color: 'error', + message: `Error changing user`, + }); + } + } catch (e) { + notification.add({ + color: 'error', + message: String(e), + }); + } + }; + + /** + * Delate a user info + * @param user - user to update + */ + const deleteUser = async (user: User) => { + try { + const response = await monolithStore.deleteMember( + adminMode, + user.id, + user.type, + ); + + if (!response) { + return; + } + + // ignore if there is no response + if (response.data) { + notification.add({ + color: 'success', + message: 'Succesfully deleting user', + }); + + onChange(); + + // refresh the users + getUsers.refresh(); + } else { + notification.add({ + color: 'error', + message: `Error deleting user`, + }); + } + } catch (e) { + notification.add({ + color: 'error', + message: String(e), + }); + } + }; + + // Avatars rendered + const Avatars = useMemo(() => { + if (!renderedUsers.length) { + return []; + } + + let i = 0; + const avatarList = []; + while (i < 5 && i < renderedUsers.length) { + avatarList.push( + + {(renderedUsers[i].name || ' ').charAt(0).toUpperCase()} + , + ); + + i++; + } + + return avatarList; + }, [renderedUsers.length]); + + return ( + + + + + + Users + + + {Avatars.length > 0 ? ( + + + {Avatars.map((el) => { + return el; + })} + + + ) : null} + + + + {totalUsers} Users + + + + + + + { + setSearch(e.target.value); + }} + /> + + + + + + + + {isLoading ? ( + + + + + + ) : ( + <> + {hasUsers ? ( + + + + + User + + + Role + + +   + + + + + {renderedUsers.map((user) => { + if (user) { + return ( + + + + + + { + updateUser({ + ...user, + publisher: + !user.publisher, + }); + }} + /> + { + updateUser({ + ...user, + exporter: + !user.exporter, + }); + }} + /> + { + updateUser({ + ...user, + admin: !user.admin, + }); + }} + /> + + + { + setAddModalOpen( + true, + ); + + setAddModalUser( + user, + ); + }} + > + + + { + deleteUser( + user, + ); + }} + > + + + + + ); + } + + return null; + })} + + + + { + setPage(v); + }} + page={page} + rowsPerPage={rowsPerPage} + rowsPerPageOptions={[5, 10, 20]} + onRowsPerPageChange={(e) => { + // set the new limit + setRowsPerPage( + parseInt( + e.target.value, + 10, + ), + ); + }} + count={totalUsers} + /> + + + + ) : ( + + + No users + + + + )} + + )} + + + + { + // close it + setAddModalOpen(false); + + // de-select the user + setAddModalUser(null); + + // refresh if successful + if (success) { + // trigger the update + onChange(); + + getUsers.refresh(); + } + }} + /> + + ); +}; diff --git a/packages/client/src/components/settings/UserTableUser.tsx b/packages/client/src/components/settings/UserTableUser.tsx new file mode 100644 index 0000000000..725c2a3ff2 --- /dev/null +++ b/packages/client/src/components/settings/UserTableUser.tsx @@ -0,0 +1,137 @@ +import { styled, Avatar, Typography, Stack } from '@semoss/ui'; + +const StyledUser = styled(Stack)(({ theme }) => ({ + paddingTop: theme.spacing(1), + paddingRight: theme.spacing(2), + paddingBottom: theme.spacing(1), + paddingLeft: theme.spacing(2), +})); + +const StyledAvatar = styled(Avatar)({ + height: '32px', + width: '32px', +}); + +// TODO:Refactor when Typography coloris updated +const StyledPrimaryText = styled(Typography)(({ theme }) => ({ + color: theme.palette.text.primary, +})); + +const StyledSecondaryText = styled(Typography)(({ theme }) => ({ + color: theme.palette.text.secondary, +})); + +interface UserTableUserProps { + /** + * Name of the user + */ + name: string; + + /** + * Name of the user + */ + id: string; + + /** + * Email of the user + */ + email: string; + + /** + * Type of the user + */ + type: string; + + /** + * Optional action to render + */ + action?: React.ReactNode; +} + +/** + * @name extractInitials + * + * Extract a initials for a string + * + * @param str + */ +const extractInitials = (str: string): string => { + if (str.length < 1) { + return ''; + } + + return str.split(' ').reduce((prev, curr) => { + return prev + (curr[0] || ''); + }, ''); +}; + +export const UserTableUser = (props: UserTableUserProps) => { + const { name, id, email, type, action } = props; + + const initials = extractInitials(name); + + return ( + + {initials} + + + {name || <> } + + + + + User ID: + + + {id || <> } + + + + + Email: + + + {email || <> } + + + + + Type: + + + {type || <> } + + + + + {action} + + ); +}; diff --git a/packages/client/src/components/settings/index.ts b/packages/client/src/components/settings/index.ts index 51561ab63b..3ddd316f81 100644 --- a/packages/client/src/components/settings/index.ts +++ b/packages/client/src/components/settings/index.ts @@ -1,5 +1,5 @@ -export * from './PendingMembersTable'; -export * from './MembersTable'; +export * from './settings.types'; + export * from './TeamMembersTable'; export * from './TeamProjectsTable'; export * from './TeamEnginesTable'; @@ -7,3 +7,6 @@ export * from './SettingsTiles'; export * from './UpdateSMSS'; export * from './FileTable'; export * from './EngineQASidebar'; +export * from './MembersTable'; +export * from './PendingMembersTable'; +export * from './UserTable'; diff --git a/packages/client/src/components/settings/member-permissions.constants.ts b/packages/client/src/components/settings/member-permissions.constants.ts deleted file mode 100644 index d3fab06e2e..0000000000 --- a/packages/client/src/components/settings/member-permissions.constants.ts +++ /dev/null @@ -1,36 +0,0 @@ -export const PERMISSION_DESCRIPTION_MAP: Record< - string, - Record -> = { - function: { - author: 'Ability to hide or delete the function, provision other authors, and all editor permissions', - editor: 'Ability to edit the function code, provision other users as editors and read-only users, and all read-only permissions', - readonly: 'Ability to execute the function', - }, - app: { - author: 'Ability to hide or delete the data app, provision other authors and all editor permissions', - editor: 'Ability to edit the data app code, provision other users as editors and read only users, and all read-only permissions', - readonly: - 'Ability to view the data app. User still requires permission to all dependent databases, models, remote storage, vector databases, etc', - }, - model: { - author: 'Ability to edit the model connection details, set the model as discoverable, delete the model, provision other authors, and all editor permissions', - editor: 'Ability to edit the model details, provision other users as editors and read-only users, and all read-only permissions', - readonly: 'Ability to run the model', - }, - storage: { - author: 'Ability to hide or delete the remote storage, provision other authors, and all editor permissions', - editor: 'Ability to push and delete files from the remote storage, and all read-only permissions', - readonly: 'Ability to view and pull files from the remote storage', - }, - database: { - author: 'Ability to edit the database connection details, set the database as discoverable, delete the database, provision other authors, and all editor permissions', - editor: 'Ability to edit the database structure, provision other users as editors and read-only users, and all read-only permissions', - readonly: 'Ability to query and read data from the database', - }, - vector: { - author: 'Ability to hide or delete the vector database, provision other authors, and all editor permissions', - editor: 'Ability to add and remove files from the vector database, and all read-only permissions', - readonly: 'Ability to query against the vector database', - }, -}; diff --git a/packages/client/src/components/settings/settings.types.ts b/packages/client/src/components/settings/settings.types.ts index 1860628180..61161363c4 100644 --- a/packages/client/src/components/settings/settings.types.ts +++ b/packages/client/src/components/settings/settings.types.ts @@ -1,3 +1,23 @@ export type SETTINGS_ROLE = 'Author' | 'Editor' | 'Read-Only'; -export type SETTINGS_MODE = 'engine' | 'app'; +export type SETTINGS_PROVISIONED_USER = { + id: string; + name: string; + type: string; + email: string; + permission: string; + permission_granted_by: string; + permission_granted_by_type: string; + date_added: string; +}; + +export type SETTINGS_PENDING_USER = { + ID: string; + NAME: string; + EMAIL: string; + PERMISSION: string; + // Requester Info + REQUEST_TIMESTAMP: string; + REQUEST_TYPE: string; + REQUEST_USERID: string; +}; diff --git a/packages/client/src/components/ui/LoadingScreen/LoadingScreen.tsx b/packages/client/src/components/ui/LoadingScreen/LoadingScreen.tsx index 6f8a4278da..625b8f43a3 100644 --- a/packages/client/src/components/ui/LoadingScreen/LoadingScreen.tsx +++ b/packages/client/src/components/ui/LoadingScreen/LoadingScreen.tsx @@ -4,12 +4,15 @@ import { Backdrop, CircularProgress, Typography, Stack } from '@semoss/ui'; import { LoadingScreenContext } from './LoadingScreenContext'; export interface LoadingScreenProps { + /** Relative loading screen */ + relative?: boolean; + /** Content to overlay the Loading Screen on */ children: React.ReactNode; } export const LoadingScreen = (props: LoadingScreenProps): JSX.Element => { - const { children } = props; + const { relative = false, children } = props; // when the count is > 0 it is loading const [count, setCount] = useState(0); @@ -73,6 +76,7 @@ export const LoadingScreen = (props: LoadingScreenProps): JSX.Element => { // zIndex: (theme) => // Math.max.apply(Math, Object.values(theme.zIndex)) + 1, zIndex: 1501, + position: relative ? 'relative' : undefined, }} > { {workspace.role === 'OWNER' ? ( { navigate('/settings/app'); @@ -89,15 +89,14 @@ export const SettingsView = () => { {view === 'CURRENT' && ( console.log('TODO')} + onChange={() => console.log('TODO')} /> )} {view === 'PENDING' && ( )} diff --git a/packages/client/src/constants.ts b/packages/client/src/constants.ts index 7d85ba4a56..eac5454ad6 100644 --- a/packages/client/src/constants.ts +++ b/packages/client/src/constants.ts @@ -1,3 +1,4 @@ +import { ALL_TYPES } from './types'; import Logo from '@/assets/logo.svg'; export const THEME_TITLE = process.env.THEME_TITLE; @@ -8,3 +9,40 @@ export const THEME = { name: THEME_TITLE || 'SEMOSS', logo: Logo, }; + +export const PERMISSION_DESCRIPTION_MAP: Record< + ALL_TYPES, + Record +> = { + APP: { + author: 'Ability to hide or delete the data app, provision other authors and all editor permissions', + editor: 'Ability to edit the data app code, provision other users as editors and read only users, and all read-only permissions', + readonly: + 'Ability to view the data app. User still requires permission to all dependent databases, models, remote storage, vector databases, etc', + }, + FUNCTION: { + author: 'Ability to hide or delete the function, provision other authors, and all editor permissions', + editor: 'Ability to edit the function code, provision other users as editors and read-only users, and all read-only permissions', + readonly: 'Ability to execute the function', + }, + MODEL: { + author: 'Ability to edit the model connection details, set the model as discoverable, delete the model, provision other authors, and all editor permissions', + editor: 'Ability to edit the model details, provision other users as editors and read-only users, and all read-only permissions', + readonly: 'Ability to run the model', + }, + STORAGE: { + author: 'Ability to hide or delete the remote storage, provision other authors, and all editor permissions', + editor: 'Ability to push and delete files from the remote storage, and all read-only permissions', + readonly: 'Ability to view and pull files from the remote storage', + }, + DATABASE: { + author: 'Ability to edit the database connection details, set the database as discoverable, delete the database, provision other authors, and all editor permissions', + editor: 'Ability to edit the database structure, provision other users as editors and read-only users, and all read-only permissions', + readonly: 'Ability to query and read data from the database', + }, + VECTOR: { + author: 'Ability to hide or delete the vector database, provision other authors, and all editor permissions', + editor: 'Ability to add and remove files from the vector database, and all read-only permissions', + readonly: 'Ability to query against the vector database', + }, +}; diff --git a/packages/client/src/hooks/index.ts b/packages/client/src/hooks/index.ts index e2091af033..ed6ccd2bce 100644 --- a/packages/client/src/hooks/index.ts +++ b/packages/client/src/hooks/index.ts @@ -13,6 +13,7 @@ import { useStepper } from './useStepper'; import { useWorkspace } from './useWorkspace'; import { useLLMComparison } from './useLLMCompare'; import { useDebounce } from './useDebounce'; +import { useDebounceValue } from './useDebounceValue'; export { useAPI, @@ -30,4 +31,5 @@ export { useWorkspace, useLLMComparison, useDebounce, + useDebounceValue, }; diff --git a/packages/client/src/hooks/useDebounceValue.ts b/packages/client/src/hooks/useDebounceValue.ts new file mode 100644 index 0000000000..b0c11b06bd --- /dev/null +++ b/packages/client/src/hooks/useDebounceValue.ts @@ -0,0 +1,25 @@ +import { useEffect, useRef, useState } from 'react'; + +/** + * Debouncea + * @param value - new value + * @param delay - delay timer + * @returns debounced value + */ +export const useDebounceValue = (value: T, delay = 500) => { + const [debouncedValue, setDebouncedValue] = useState(undefined); + const timerRef = useRef>(); + + useEffect(() => { + // waittill it is set + timerRef.current = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(timerRef.current); + }; + }, [value, delay]); + + return debouncedValue; +}; diff --git a/packages/client/src/pages/app/AppDetailPage.tsx b/packages/client/src/pages/app/AppDetailPage.tsx index c3731dfb03..2ca0b78a1f 100644 --- a/packages/client/src/pages/app/AppDetailPage.tsx +++ b/packages/client/src/pages/app/AppDetailPage.tsx @@ -717,9 +717,9 @@ export const AppDetailPage = () => { }} > { navigate('/settings/app'); @@ -743,14 +743,13 @@ export const AppDetailPage = () => { > { + onChange={() => { fetchUserSpecificData(); }} /> diff --git a/packages/client/src/pages/engine/EngineSettingsPage.tsx b/packages/client/src/pages/engine/EngineSettingsPage.tsx index 0e5b39329e..791f7ab4c1 100644 --- a/packages/client/src/pages/engine/EngineSettingsPage.tsx +++ b/packages/client/src/pages/engine/EngineSettingsPage.tsx @@ -30,16 +30,16 @@ export const EngineSettingsPage = () => { > { navigate(`/engine/${type.toLowerCase()}`); }} /> - - + + ); diff --git a/packages/client/src/pages/engine/EngineSmssPage.tsx b/packages/client/src/pages/engine/EngineSmssPage.tsx index 20ca92fe21..7b6c2c23ce 100644 --- a/packages/client/src/pages/engine/EngineSmssPage.tsx +++ b/packages/client/src/pages/engine/EngineSmssPage.tsx @@ -13,7 +13,7 @@ const StyledContainer = styled('div')(({ theme }) => ({ })); export const EngineSmssPage = () => { - const { id } = useEngine(); + const { id, type } = useEngine(); return ( { }} > - + ); diff --git a/packages/client/src/pages/settings/AppSettingsDetailPage.tsx b/packages/client/src/pages/settings/AppSettingsDetailPage.tsx index 2f96bb303c..2e3153abeb 100644 --- a/packages/client/src/pages/settings/AppSettingsDetailPage.tsx +++ b/packages/client/src/pages/settings/AppSettingsDetailPage.tsx @@ -81,7 +81,7 @@ export const AppSettingsDetailPage = () => { {permission === 'OWNER' ? ( { {view === 'CURRENT' && ( - getUserEnginePermission.refresh() - } + type={'APP'} + onChange={() => getUserEnginePermission.refresh()} /> )} {view === 'PENDING' && ( - + )} {view === 'APP' && } diff --git a/packages/client/src/pages/settings/EngineSettingsDetailPage.tsx b/packages/client/src/pages/settings/EngineSettingsDetailPage.tsx index 3db3da0db4..2553296d73 100644 --- a/packages/client/src/pages/settings/EngineSettingsDetailPage.tsx +++ b/packages/client/src/pages/settings/EngineSettingsDetailPage.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { styled, ToggleTabsGroup } from '@semoss/ui'; -import { ENGINE_TYPES, Role } from '@/types'; +import { Role, ALL_TYPES } from '@/types'; import { useSettings, useAPI } from '@/hooks'; import { SettingsTiles, @@ -36,7 +36,7 @@ type VIEW = 'CURRENT' | 'PENDING'; */ interface EngineSettingsDetailPageProps { /** Type of the page to render */ - type: ENGINE_TYPES; + type: ALL_TYPES; } export const EngineSettingsDetailPage = ( @@ -44,11 +44,6 @@ export const EngineSettingsDetailPage = ( ) => { const { type } = props; - // get a pretty name - const name = type - .replace(/^[-_]*(.)/, (_, c) => c.toUpperCase()) - .replace(/[-_]+(.)/g, (_, c) => ' ' + c.toUpperCase()); - const { id } = useParams(); const navigate = useNavigate(); const { adminMode } = useSettings(); @@ -98,8 +93,8 @@ export const EngineSettingsDetailPage = ( {permission === 'OWNER' ? ( { navigate('..', { relative: 'path' }); @@ -120,21 +115,16 @@ export const EngineSettingsDetailPage = ( {view === 'CURRENT' && ( - getUserEnginePermission.refresh() - } + onChange={() => getUserEnginePermission.refresh()} /> )} {view === 'PENDING' && ( - + )} - {permission === 'OWNER' ? ( - - ) : null} + {permission === 'OWNER' ? : null} ); }; diff --git a/packages/client/src/pages/settings/EngineSettingsIndexPage.tsx b/packages/client/src/pages/settings/EngineSettingsIndexPage.tsx index b81cb82a8a..90da322fe6 100644 --- a/packages/client/src/pages/settings/EngineSettingsIndexPage.tsx +++ b/packages/client/src/pages/settings/EngineSettingsIndexPage.tsx @@ -1,9 +1,6 @@ import { useNavigate } from 'react-router-dom'; import { useEffect, useState, useRef, useReducer } from 'react'; -import { ENGINE_TYPES } from '@/types'; -import { useRootStore, usePixel, useAPI, useSettings } from '@/hooks'; - import { Grid, Search, @@ -23,6 +20,8 @@ import { FormatListBulletedOutlined, } from '@mui/icons-material'; +import { ALL_TYPES } from '@/types'; +import { useRootStore, usePixel, useAPI, useSettings } from '@/hooks'; import { EngineLandscapeCard, EngineTileCard } from '@/components/engine'; import { removeUnderscores } from '@/utility'; @@ -96,7 +95,7 @@ const reducer = (state, action) => { */ interface EngineSettingsIndexPageProps { /** Type of the page to render */ - type: ENGINE_TYPES; + type: ALL_TYPES; } export const EngineSettingsIndexPage = ( diff --git a/packages/client/src/pages/settings/MemberSettingsPage.tsx b/packages/client/src/pages/settings/MemberSettingsPage.tsx index cf5ca6c572..14431bb048 100644 --- a/packages/client/src/pages/settings/MemberSettingsPage.tsx +++ b/packages/client/src/pages/settings/MemberSettingsPage.tsx @@ -1,1362 +1,13 @@ -import { useEffect, useState } from 'react'; -import { - Delete, - Add, - Edit, - Close, - LocalPoliceRounded, - CloudUploadRounded, - DownloadForOfflineRounded, -} from '@mui/icons-material'; -import { - styled, - useNotification, - List, - Modal, - Button, - Checkbox, - Typography, - AvatarGroup, - Avatar, - Table, - IconButton, - Stack, - TextField, - Select, - Switch, -} from '@semoss/ui'; -import { useForm, useFormState, Controller } from 'react-hook-form'; -import { useNavigate } from 'react-router-dom'; -import { useRootStore, useAPI, useSettings } from '@/hooks'; -import { LoadingScreen } from '@/components/ui'; - -const StyledContainer = styled('div')({ - width: '100%', -}); - -const StyledEnd = styled('div')(({ theme }) => ({ - display: 'flex', - justifyContent: 'space-between', - paddingBottom: theme.spacing(1), -})); - -const StyledMemberTypography = styled(Typography)(({ theme }) => ({ - paddingTop: theme.spacing(1), - color: theme.palette.grey['500'], -})); - -const StyledTitle = styled('div')({ - display: 'flex', -}); - -const StyledModal = styled('div')({ - width: '1000px', - height: '854px', - padding: '16px', -}); - -const StyledListItem = styled(List.Item)({ - padding: '4px 0', -}); - -const StyledList = styled(List)({ - padding: 0, -}); - -const StyledModalTitle = styled(Modal.Title)({ - padding: '24px', -}); - -const StyledModalContent = styled(Modal.Content)({ - paddingTop: '4px', -}); - -const StyledStack = styled(Stack)({ - marginTop: '4px', -}); - -const StyledAddMemberButton = styled(Button)({ - textAlign: 'right', -}); - -const StyledCountryCodeExt = styled(TextField)({ - width: '168px', -}); - -const StyledPhoneNumber = styled(TextField)({ - width: '550px', -}); - -const StyledCredentials = styled(Typography)({ - padding: '8px 0px', -}); - -const StyledPermissions = styled(Typography)({ - padding: '25px 0', -}); - -const StyledPaddingTopStack = styled(Stack)({ - paddingTop: '4px', -}); - -const StyledMembers = styled(Typography)({ - marginRight: '16px', -}); - -const StyledAvatarGroup = styled(AvatarGroup)({ - marginRight: '8px', -}); - -const StyledAvatar = styled(Avatar)({ - height: 32, - width: 32, -}); - -interface Member { - admin: boolean; - email: string; - id: string; - name: string; - type: string; - username: string; - password: string; - phone: string; - publisher: boolean; - exporter: boolean; - phoneextension: string; - countrycode: string; -} - -interface PendingMember { - admin: boolean; - username: string; - email: string; - countrycode: string; - phone: string; - phoneextension: string; - id: string; - name: string; - type: string; - publisher: boolean; - exporter: boolean; -} - -interface EditUserForm { - id: string; - username: string; - name: string; - password: string; - email: string; - phone: string; - phoneextension: string; - countrycode: string; - admin: boolean; - exporter: boolean; - publisher: boolean; - type: string; -} - -const capitalize = (input: string) => { - return input.charAt(0).toUpperCase() + input.slice(1); -}; - -const passwordValidate = (password: string) => { - if (!password) { - return false; - } - if (!password.match(/[a-z]/g)) { - return false; - } - - if (!password.match(/[A-Z]/g)) { - return false; - } - - if (!password.match(/[0-9]/g)) { - return false; - } - - if (!password.match(/[!@#$%^&*]/g)) { - return false; - } - - return true; -}; - -const numberValidate = (number: string) => { - if (!number) { - return false; - } - if (!number.match(/^[()-.\s0-9]{8,}$/)) { - return false; - } - - return true; -}; +import { Navigate } from 'react-router-dom'; +import { useSettings } from '@/hooks'; +import { UserTable } from '@/components/settings'; export const MemberSettingsPage = () => { const { adminMode } = useSettings(); - const { configStore, monolithStore } = useRootStore(); - const notification = useNotification(); - const navigate = useNavigate(); if (!adminMode) { - navigate('/settings'); + return ; } - const [members, setMembers] = useState([]); - const [addMemberModal, setAddMemberModal] = useState(false); - const [memberInfoModal, setMemberInfoModal] = useState(false); - const [activeMember, setActiveMember] = useState(null); - const [page, setPage] = useState(0); - - const { - control, - reset, - handleSubmit, - getValues, - watch, - formState: { errors }, - } = useForm<{ - // edit existing member fields - id: string; - username: string; - name: string; - password: string; - email: string; - phone: string; - phoneextension: string; - countrycode: string; - admin: boolean; - exporter: boolean; - publisher: boolean; - type: string; - // add pending member fields - }>({ - defaultValues: { - id: activeMember?.id, - username: activeMember?.username, - name: activeMember?.name, - email: activeMember?.email, - phone: activeMember?.phone, - phoneextension: activeMember?.phoneextension, - countrycode: activeMember?.countrycode, - admin: activeMember?.admin, - exporter: activeMember?.exporter, - publisher: activeMember?.exporter, - type: activeMember?.type, - }, - }); - - const type = watch('type', ''); - - const { dirtyFields } = useFormState({ - control, - }); - - const providers = configStore.store.config.providers.map((val) => - capitalize(val), - ); - - const updateMemberInfo = (member: Member) => { - monolithStore['editMemberInfo'](adminMode, member) - .then(() => { - const message = - 'You have successfully updated user information'; - notification.add({ - color: 'success', - message: message, - }); - getMembers.refresh(); - }) - .catch((error) => { - notification.add({ - color: 'error', - message: error, - }); - }); - }; - - const updateActiveMember = handleSubmit((data) => { - setMemberInfoModal(false); - monolithStore['editMemberInfo'](adminMode, data) - .then(() => { - const message = `You have successfully updated user information`; - notification.add({ - color: 'success', - message: message, - }); - getMembers.refresh(); - }) - .catch((error) => { - notification.add({ - color: 'error', - message: error, - }); - }); - }); - - const deleteActiveMember = (member: Member) => { - monolithStore['deleteMember'](adminMode, member.id, member.type).then( - () => { - setActiveMember(null); - getMembers.refresh(); - }, - ); - }; - - const createUser = handleSubmit((data: EditUserForm) => { - monolithStore['createUser'](adminMode, data).then((resp) => { - if (resp.data) { - const message = `You have successfully added new user(s)`; - notification.add({ - color: 'success', - message: message, - }); - getMembers.refresh(); - const newMember = members.find((m) => { - m.id == data.id; - }); - setActiveMember(newMember); - setAddMemberModal(false); - } - }); - }); - - const getMembers = useAPI(['getAllUsers', adminMode]); - - // TODO: Remove this useEffect. It is unnecessary. - useEffect(() => { - // REST call to get all apps - if (getMembers.status !== 'SUCCESS' || !getMembers.data) { - return; - } - - // flush into a non optional object - const updated: Member[] = getMembers.data.map((m) => ({ - admin: false, - email: '', - name: '', - username: '', - password: '', - phone: '', - publisher: false, - exporter: false, - phoneextension: '', - countrycode: '', - ...m, - })); - - setMembers(updated); - - () => { - console.warn('Cleaning up getMembers'); - setMembers([]); - }; - }, [getMembers.status, getMembers.data]); - // show a loading screen when getProjects is pending - if (getMembers.status !== 'SUCCESS') { - return ( - - ); - } - - const buildMemberModal = () => { - return ( - - - - Edit Member - - { - setActiveMember(null); - setMemberInfoModal(false); - }} - > - - - - {activeMember && ( -
- - - Details - - - -
- - { - return ( - - field.onChange( - e.target.value, - ) - } - > - ); - }} - /> - { - return ( - - field.onChange( - e.target.value, - ) - } - type="email" - > - ); - }} - /> - - - { - return ( - - field.onChange( - e.target.value, - ) - } - > - ); - }} - /> - - numberValidate(value), - pattern: /^[()-.\s0-9]{8,}$/, - }} - render={({ field }) => { - return ( - - - field.onChange( - e.target - .value, - ) - } - > - {errors.phone && ( - - Note: Number - should be - written in - XXX-XXX-XXXX - format - - )} - - ); - }} - /> - { - return ( - - field.onChange( - e.target.value, - ) - } - > - ); - }} - /> - - - - Credentials - - - { - return ( - - ); - }} - /> - - { - return ( - { - field.onChange( - e.target.value, - ); - }} - > - ); - }} - /> - - { - return ( - { - field.onChange( - e.target.value, - ); - }} - > - ); - }} - /> - - {type === 'Native' && ( - <> - - passwordValidate(value), - }} - render={({ field }) => { - return ( - { - field.onChange( - e.target - .value, - ); - }} - > - ); - }} - /> - - {dirtyFields.password && ( - - Note: Password must have one - letter, one capital, one - number, one special - character, and be a minimum - of 8 characters. - - )} - - )} - - - Permissions - - - - { - return ( - - field.onChange( - !field.value, - ) - } - /> - ); - }} - /> - } - > - - - - Admin} - secondary="All-Access pass to app information" - /> - - - { - return ( - - field.onChange( - !field.value, - ) - } - /> - ); - }} - /> - } - > - - - - Publisher - } - secondary="Anyone on the platform can access" - /> - - - { - return ( - - field.onChange( - !field.value, - ) - } - /> - ); - }} - /> - } - > - - - - Exporter - } - secondary="Anyone on the platform can access" - /> - - - -
-
- - - - -
- )} -
- ); - }; - - const buildNewMemberModal = () => { - return ( - - - - Add Member - - { - reset(); - setAddMemberModal(false); - }} - > - - - -
- - - Details - - - -
- - { - return ( - - field.onChange( - e.target.value, - ) - } - > - ); - }} - /> - { - return ( - - field.onChange( - e.target.value, - ) - } - type="email" - > - ); - }} - /> - - - { - return ( - - field.onChange( - e.target.value, - ) - } - > - ); - }} - /> - - numberValidate(value), - pattern: /^[()-.\s0-9]{8,}$/, - }} - render={({ field }) => { - return ( - - - field.onChange( - e.target.value, - ) - } - > - {errors.phone && ( - - Note: Number should - be written in - XXX-XXX-XXXX format - - )} - - ); - }} - /> - { - return ( - - field.onChange( - e.target.value, - ) - } - > - ); - }} - /> - - - - Credentials - - { - return ( - - ); - }} - /> - - { - return ( - { - field.onChange( - e.target.value, - ); - }} - > - ); - }} - /> - - { - return ( - { - field.onChange( - e.target.value, - ); - }} - > - ); - }} - /> - {type === 'Native' && ( - <> - - passwordValidate(value), - }} - render={({ field }) => { - return ( - { - field.onChange( - e.target.value, - ); - }} - > - ); - }} - /> - - {dirtyFields.password && ( - - Note: Password must have one - letter, one capital, one number, - one special character, and be a - minimum of 8 characters. - - )} - - )} - - Permissions - - - - { - return ( - - field.onChange( - !field.value, - ) - } - /> - ); - }} - /> - } - > - - - - Admin} - secondary="All-Access pass to app information" - /> - - - { - return ( - - field.onChange( - !field.value, - ) - } - /> - ); - }} - /> - } - > - - - - Publisher} - secondary="Anyone on the platform can access" - /> - - - { - return ( - - field.onChange( - !field.value, - ) - } - /> - ); - }} - /> - } - > - - - - Exporter} - secondary="Anyone on the platform can access" - /> - - - -
-
- - - - -
-
- ); - }; - - return ( - <> - - - - - Members - - {members && ( - <> - - {members.map((mem, i) => { - return ( - - {mem.name[0]} - - ); - })} - - {members.length > 1 ? ( - - {members.length} members - - ) : ( - - {members.length} member - - )} - - )} - - - } - onClick={() => { - setAddMemberModal(true); - }} - > - Add New - - - - {members && ( - - - - - - Name - - - Email - - - Type - - - Role - - - Action - - - - - {members.map((mem, i) => ( - - {mem.name} - {mem.email} - {mem.type} - - { - updateMemberInfo({ - ...mem, - publisher: - !mem.publisher, - }); - }} - /> - { - updateMemberInfo({ - ...mem, - exporter: !mem.exporter, - }); - }} - /> - { - updateMemberInfo({ - ...mem, - admin: !mem.admin, - }); - }} - /> - - - { - setActiveMember(mem); - reset(mem); - setMemberInfoModal(true); - }} - > - - - { - deleteActiveMember(mem); - }} - > - - - - - ))} - - - - { - setPage(v); - }} - page={page} - count={Math.ceil(members.length / 5)} - /> - - -
-
- )} -
- - {/* MemberInfoModal */} - - {buildMemberModal()} - - {/* Add New User Modal */} - - {buildNewMemberModal()} - - - ); + return ; }; diff --git a/packages/client/src/pages/settings/MyProfilePage.tsx b/packages/client/src/pages/settings/MyProfilePage.tsx index e7eafcaf3f..18077abb98 100644 --- a/packages/client/src/pages/settings/MyProfilePage.tsx +++ b/packages/client/src/pages/settings/MyProfilePage.tsx @@ -238,17 +238,22 @@ export const MyProfilePage = () => { const response = await monolithStore.editMemberInfo(true, userObj); - notification.add({ - color: 'success', - message: 'Successfully edited profile information', - }); - } catch (e) { - if (e instanceof Error) { + if (response.data) { + notification.add({ + color: 'success', + message: 'Successfully edited profile information', + }); + } else { notification.add({ color: 'error', - message: e.message, + message: 'Error editing profile information', }); } + } catch (e) { + notification.add({ + color: 'error', + message: String(e), + }); } }; diff --git a/packages/client/src/stores/monolith/monolith.store.ts b/packages/client/src/stores/monolith/monolith.store.ts index 60b4391c02..e7c619e8a5 100644 --- a/packages/client/src/stores/monolith/monolith.store.ts +++ b/packages/client/src/stores/monolith/monolith.store.ts @@ -607,10 +607,19 @@ export class MonolithStore { /** * @name getEngineUsersNoCredentials * @param admin - * @param appId + * @param engineId + * @param limit + * @param offSet + * @param searchTerm * @returns */ - async getEngineUsersNoCredentials(admin: boolean, appId: string) { + async getEngineUsersNoCredentials( + admin: boolean, + engineId: string, + limit: number, + offset: number, + searchTerm: string, + ) { let url = `${Env.MODULE}/api/auth/`; // Currently no admin ENDPOINT; @@ -622,8 +631,21 @@ export class MonolithStore { // get the response const response = await axios - .get[]>(url, { - params: { engineId: appId }, + .get< + { + id: string; + email: string; + name: string; + type: string; + username: string; + }[] + >(url, { + params: { + engineId: engineId, + limit: limit, + offset: offset, + searchTerm: searchTerm, + }, }) .catch((error) => { throw Error(error); @@ -1814,10 +1836,19 @@ export class MonolithStore { /** * @name getProjectUsersNoCredentials * @param admin if admin initiated the call - * @param projectId the id of app + * @param appId the id of app + * @param limit + * @param offSet + * @param searchTerm * @desc get the existing users and their permissions for this app */ - async getProjectUsersNoCredentials(admin: boolean, projectId: string) { + async getProjectUsersNoCredentials( + admin: boolean, + appId: string, + limit: number, + offset: number, + searchTerm: string, + ) { let url = `${Env.MODULE}/api/auth/`; if (admin) { @@ -1828,8 +1859,21 @@ export class MonolithStore { // get the response const response = await axios - .get[]>(url, { - params: { projectId: projectId }, + .get< + { + id: string; + email: string; + name: string; + type: string; + username: string; + }[] + >(url, { + params: { + projectId: appId, + limit: limit, + offset: offset, + searchTerm: searchTerm, + }, }) .catch((error) => { throw Error(error); @@ -2535,7 +2579,12 @@ export class MonolithStore { * @param admin - is admin user * @returns MemberInterface[] */ - async getAllUsers(admin: boolean) { + async getAllUsers( + admin: boolean, + searchTerm?: string, + offset?: number, + limit?: number, + ) { let url = `${Env.MODULE}/api/auth/`; if (admin) { @@ -2556,8 +2605,18 @@ export class MonolithStore { publisher?: boolean; exporter?: boolean; email?: string; + phone?: string; + phoneextension?: string; + countrycode?: string; + username?: string; }[] - >(url) + >(url, { + params: { + filterWord: searchTerm, + offset: offset, + limit: limit, + }, + }) .catch((error) => { throw Error(error); }); @@ -2588,7 +2647,7 @@ export class MonolithStore { postData += 'user=' + encodeURIComponent(JSON.stringify(user)); const response = await axios - .post<{ success: boolean }>(url, postData, { + .post(url, postData, { headers: { 'content-type': 'application/x-www-form-urlencoded', }, @@ -2619,7 +2678,7 @@ export class MonolithStore { postData += 'userId=' + encodeURIComponent(userId); postData += '&type=' + encodeURIComponent(userType); - const response = await axios.post<{ success: boolean }>(url, postData, { + const response = await axios.post(url, postData, { headers: { 'content-type': 'application/x-www-form-urlencoded', }, @@ -2664,15 +2723,11 @@ export class MonolithStore { newUserInfo += '&password=' + encodeURIComponent(user.password); } - const response = await axios.post<{ success: boolean }>( - url, - newUserInfo, - { - headers: { - 'content-type': 'application/x-www-form-urlencoded', - }, + const response = await axios.post(url, newUserInfo, { + headers: { + 'content-type': 'application/x-www-form-urlencoded', }, - ); + }); return response; } diff --git a/packages/ui/src/components/Autocomplete/Autocomplete.tsx b/packages/ui/src/components/Autocomplete/Autocomplete.tsx index 6ad3ff0492..66702bb33c 100644 --- a/packages/ui/src/components/Autocomplete/Autocomplete.tsx +++ b/packages/ui/src/components/Autocomplete/Autocomplete.tsx @@ -34,14 +34,12 @@ export interface AutocompleteProps< | "PaperComponent" | "PopperComponent" | "renderGroup" - | "renderOption" | "renderTags" | "slotProps" | "unstable_classNamePrefix" | "unstable_isActiveElementInListbox" | "autoComplete" | "autoHighlight" - | "loading" | "autoSelect" | "blurOnSelect" | "clearOnBlur" @@ -53,7 +51,6 @@ export interface AutocompleteProps< | "disableListWrap" | "getOptionDisabled" | "handleHomeEndKeys" - | "includeInputInList" | "openOnFocus" | "selectOnFocus" | "selectOnFocus" diff --git a/packages/ui/src/components/Table/TablePagination.tsx b/packages/ui/src/components/Table/TablePagination.tsx index fa8438eb12..8447036c43 100644 --- a/packages/ui/src/components/Table/TablePagination.tsx +++ b/packages/ui/src/components/Table/TablePagination.tsx @@ -43,6 +43,12 @@ export interface TablePaginationProps { HTMLTextAreaElement | HTMLInputElement >; + /** + * Is the pagination enabled? + * + */ + disabled?: boolean; + /** custom style object */ sx?: SxProps; }