import { useEffect, useMemo, useState, VFC } from 'react'; import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table'; import { VirtualizedTable, TablePlaceholder } from 'component/common/Table'; import { Button, useMediaQuery, useTheme } from '@mui/material'; import { Delete, Edit } from '@mui/icons-material'; import { sortTypes } from 'utils/sortTypes'; import useProjectAccess, { ENTITY_TYPE, IProjectAccess, } from 'hooks/api/getters/useProjectAccess/useProjectAccess'; import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell'; import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { useSearch } from 'hooks/useSearch'; import { useSearchParams } from 'react-router-dom'; import { createLocalStorage } from 'utils/createLocalStorage'; import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell'; import { PageContent } from 'component/common/PageContent/PageContent'; import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { Search } from 'component/common/Search/Search'; import { ProjectAccessAssign } from 'component/project/ProjectAccess/ProjectAccessAssign/ProjectAccessAssign'; import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi'; import useToast from 'hooks/useToast'; import { Dialogue } from 'component/common/Dialogue/Dialogue'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import { ProjectGroupView } from '../ProjectGroupView/ProjectGroupView'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { IUser } from 'interfaces/user'; import { IGroup } from 'interfaces/group'; import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; export type PageQueryType = Partial< Record<'sort' | 'order' | 'search', string> >; const defaultSort: SortingRule = { id: 'added' }; const { value: storedParams, setValue: setStoredParams } = createLocalStorage( 'ProjectAccess:v1', defaultSort ); export const ProjectAccessTable: VFC = () => { const projectId = useRequiredPathParam('projectId'); const { uiConfig } = useUiConfig(); const { flags } = uiConfig; const entityType = flags.UG ? 'user / group' : 'user'; const theme = useTheme(); const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); const { setToastData } = useToast(); const { access, refetchProjectAccess } = useProjectAccess(projectId); const { removeUserFromRole, removeGroupFromRole } = useProjectApi(); const [assignOpen, setAssignOpen] = useState(false); const [removeOpen, setRemoveOpen] = useState(false); const [groupOpen, setGroupOpen] = useState(false); const [selectedRow, setSelectedRow] = useState(); useEffect(() => { if (!assignOpen && !groupOpen) { setSelectedRow(undefined); } }, [assignOpen, groupOpen]); const roles = useMemo( () => access.roles || [], // eslint-disable-next-line react-hooks/exhaustive-deps [JSON.stringify(access.roles)] ); const mappedData: IProjectAccess[] = useMemo(() => { const users = access.users || []; const groups = access.groups || []; return [ ...users.map(user => ({ entity: user, type: ENTITY_TYPE.USER, })), ...groups.map(group => ({ entity: group, type: ENTITY_TYPE.GROUP, })), ]; }, [access]); const columns = useMemo( () => [ { Header: 'Avatar', accessor: 'imageUrl', Cell: ({ row: { original: row } }: any) => ( {row.entity.users?.length} ), maxWidth: 85, disableSortBy: true, }, { id: 'name', Header: 'Name', accessor: (row: IProjectAccess) => row.entity.name || '', Cell: ({ value, row: { original: row } }: any) => ( { setSelectedRow(row); setGroupOpen(true); }} title={value} subtitle={`${row.entity.users?.length} users`} /> } elseShow={} /> ), minWidth: 100, searchable: true, }, { id: 'username', Header: 'Username', accessor: (row: IProjectAccess) => { if (row.type === ENTITY_TYPE.USER) { const userRow = row.entity as IUser; return userRow.username || userRow.email; } return ''; }, Cell: HighlightCell, minWidth: 100, searchable: true, }, { Header: 'Role', accessor: (row: IProjectAccess) => roles.find(({ id }) => id === row.entity.roleId)?.name, minWidth: 120, filterName: 'role', }, { id: 'added', Header: 'Added', accessor: (row: IProjectAccess) => { const userRow = row.entity as IUser | IGroup; return userRow.addedAt || ''; }, Cell: ({ value }: { value: Date }) => ( ), sortType: 'date', maxWidth: 150, }, { Header: 'Last login', accessor: (row: IProjectAccess) => { if (row.type === ENTITY_TYPE.USER) { const userRow = row.entity as IUser; return userRow.seenAt || ''; } const userGroup = row.entity as IGroup; return userGroup.users .map(({ seenAt }) => seenAt) .sort() .reverse()[0]; }, Cell: ({ value }: { value: Date }) => ( ), sortType: 'date', maxWidth: 150, }, { id: 'actions', Header: 'Actions', disableSortBy: true, align: 'center', maxWidth: 200, Cell: ({ row: { original: row } }: any) => ( { setSelectedRow(row); setAssignOpen(true); }} disabled={mappedData.length === 1} tooltipProps={{ title: mappedData.length === 1 ? 'Cannot edit access. A project must have at least one owner' : 'Edit access', }} > { setSelectedRow(row); setRemoveOpen(true); }} disabled={mappedData.length === 1} tooltipProps={{ title: mappedData.length === 1 ? 'Cannot remove access. A project must have at least one owner' : 'Remove access', }} > ), }, ], [roles, mappedData.length, projectId] ); const [searchParams, setSearchParams] = useSearchParams(); const [initialState] = useState(() => ({ sortBy: [ { id: searchParams.get('sort') || storedParams.id, desc: searchParams.has('order') ? searchParams.get('order') === 'desc' : storedParams.desc, }, ], globalFilter: searchParams.get('search') || '', })); const [searchValue, setSearchValue] = useState(initialState.globalFilter); const { data, getSearchText, getSearchContext } = useSearch( columns, searchValue, mappedData ?? [] ); const { headerGroups, rows, prepareRow, state: { sortBy }, } = useTable( { columns: columns as any[], data, initialState, sortTypes, autoResetSortBy: false, disableSortRemove: true, disableMultiSort: true, defaultColumn: { Cell: TextCell, }, }, useSortBy, useFlexLayout ); useEffect(() => { const tableState: PageQueryType = {}; tableState.sort = sortBy[0].id; if (sortBy[0].desc) { tableState.order = 'desc'; } if (searchValue) { tableState.search = searchValue; } setSearchParams(tableState, { replace: true, }); setStoredParams({ id: sortBy[0].id, desc: sortBy[0].desc || false }); }, [sortBy, searchValue, setSearchParams]); const removeAccess = async (userOrGroup?: IProjectAccess) => { if (!userOrGroup) return; const { id, roleId } = userOrGroup.entity; let name = userOrGroup.entity.name; if (userOrGroup.type === ENTITY_TYPE.USER) { const user = userOrGroup.entity as IUser; name = name || user.email || user.username || ''; } try { if (userOrGroup.type === ENTITY_TYPE.USER) { await removeUserFromRole(projectId, roleId, id); } else { await removeGroupFromRole(projectId, roleId, id); } refetchProjectAccess(); setToastData({ type: 'success', title: `${ name || `The ${entityType}` } has been removed from project`, }); } catch (err: any) { setToastData({ type: 'error', title: err.message || `Server problems when removing ${entityType}.`, }); } setRemoveOpen(false); setSelectedRow(undefined); }; return ( } /> } > } /> } > 0} show={ No access found matching “ {searchValue} ” } elseShow={ No access available. Get started by assigning a{' '} {entityType}. } /> } /> removeAccess(selectedRow)} onClose={() => { setSelectedRow(undefined); setRemoveOpen(false); }} title={`Really remove ${entityType} from this project?`} /> id === selectedRow?.entity.roleId) ?.name }`} onEdit={() => { setAssignOpen(true); console.log('Assign Open true'); }} onRemove={() => { setGroupOpen(false); setRemoveOpen(true); }} /> ); };