2022-07-22 09:31:08 +02:00
|
|
|
import { useEffect, useMemo, useState, VFC } from 'react';
|
|
|
|
import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table';
|
|
|
|
import { VirtualizedTable, TablePlaceholder } from 'component/common/Table';
|
|
|
|
import { Avatar, Button, styled, useMediaQuery, useTheme } from '@mui/material';
|
|
|
|
import { Delete, Edit } from '@mui/icons-material';
|
2022-05-26 10:37:33 +02:00
|
|
|
import { sortTypes } from 'utils/sortTypes';
|
2022-07-22 09:31:08 +02:00
|
|
|
import useProjectAccess, {
|
|
|
|
ENTITY_TYPE,
|
|
|
|
IProjectAccess,
|
2022-05-26 10:37:33 +02:00
|
|
|
} 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';
|
2022-06-21 09:08:37 +02:00
|
|
|
import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
|
2022-07-22 09:31:08 +02:00
|
|
|
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';
|
2022-05-26 10:37:33 +02:00
|
|
|
|
2022-07-22 09:31:08 +02:00
|
|
|
const StyledAvatar = styled(Avatar)(({ theme }) => ({
|
|
|
|
width: theme.spacing(4),
|
|
|
|
height: theme.spacing(4),
|
|
|
|
margin: 'auto',
|
|
|
|
backgroundColor: theme.palette.secondary.light,
|
|
|
|
color: theme.palette.text.primary,
|
|
|
|
fontSize: theme.fontSizes.smallBody,
|
|
|
|
fontWeight: theme.fontWeight.bold,
|
|
|
|
}));
|
|
|
|
|
|
|
|
export type PageQueryType = Partial<
|
|
|
|
Record<'sort' | 'order' | 'search', string>
|
|
|
|
>;
|
|
|
|
|
|
|
|
const defaultSort: SortingRule<string> = { 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();
|
2022-06-02 09:21:51 +02:00
|
|
|
|
2022-07-22 09:31:08 +02:00
|
|
|
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<IProjectAccess>();
|
|
|
|
|
|
|
|
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]);
|
2022-05-26 10:37:33 +02:00
|
|
|
|
|
|
|
const columns = useMemo(
|
|
|
|
() => [
|
|
|
|
{
|
|
|
|
Header: 'Avatar',
|
|
|
|
accessor: 'imageUrl',
|
2022-07-22 09:31:08 +02:00
|
|
|
Cell: ({ row: { original: row } }: any) => (
|
|
|
|
<TextCell>
|
|
|
|
<StyledAvatar
|
|
|
|
data-loading
|
|
|
|
alt="Gravatar"
|
|
|
|
src={row.entity.imageUrl}
|
|
|
|
title={`${
|
|
|
|
row.entity.name ||
|
|
|
|
row.entity.email ||
|
|
|
|
row.entity.username
|
|
|
|
} (id: ${row.entity.id})`}
|
|
|
|
>
|
|
|
|
{row.entity.users?.length}
|
|
|
|
</StyledAvatar>
|
|
|
|
</TextCell>
|
2022-05-26 10:37:33 +02:00
|
|
|
),
|
2022-07-22 09:31:08 +02:00
|
|
|
maxWidth: 85,
|
|
|
|
disableSortBy: true,
|
2022-05-26 10:37:33 +02:00
|
|
|
},
|
|
|
|
{
|
2022-06-02 09:21:51 +02:00
|
|
|
id: 'name',
|
2022-05-26 10:37:33 +02:00
|
|
|
Header: 'Name',
|
2022-07-22 09:31:08 +02:00
|
|
|
accessor: (row: IProjectAccess) => row.entity.name || '',
|
|
|
|
Cell: ({ value, row: { original: row } }: any) => (
|
|
|
|
<ConditionallyRender
|
|
|
|
condition={row.type === ENTITY_TYPE.GROUP}
|
|
|
|
show={
|
|
|
|
<LinkCell
|
|
|
|
onClick={() => {
|
|
|
|
setSelectedRow(row);
|
|
|
|
setGroupOpen(true);
|
|
|
|
}}
|
|
|
|
title={value}
|
|
|
|
subtitle={`${row.entity.users?.length} users`}
|
|
|
|
/>
|
|
|
|
}
|
|
|
|
elseShow={<HighlightCell value={value} />}
|
|
|
|
/>
|
|
|
|
),
|
|
|
|
minWidth: 100,
|
|
|
|
searchable: true,
|
2022-05-26 10:37:33 +02:00
|
|
|
},
|
|
|
|
{
|
|
|
|
id: 'username',
|
2022-06-02 09:21:51 +02:00
|
|
|
Header: 'Username',
|
2022-07-22 09:31:08 +02:00
|
|
|
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,
|
2022-05-26 10:37:33 +02:00
|
|
|
},
|
|
|
|
{
|
|
|
|
Header: 'Role',
|
2022-07-22 09:31:08 +02:00
|
|
|
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 }) => (
|
|
|
|
<TimeAgoCell value={value} emptyText="Never logged" />
|
2022-05-26 10:37:33 +02:00
|
|
|
),
|
2022-07-22 09:31:08 +02:00
|
|
|
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 }) => (
|
|
|
|
<TimeAgoCell value={value} emptyText="Never logged" />
|
|
|
|
),
|
|
|
|
sortType: 'date',
|
|
|
|
maxWidth: 150,
|
2022-05-26 10:37:33 +02:00
|
|
|
},
|
|
|
|
{
|
|
|
|
id: 'actions',
|
2022-06-02 09:21:51 +02:00
|
|
|
Header: 'Actions',
|
2022-05-26 10:37:33 +02:00
|
|
|
disableSortBy: true,
|
|
|
|
align: 'center',
|
2022-07-22 09:31:08 +02:00
|
|
|
maxWidth: 200,
|
|
|
|
Cell: ({ row: { original: row } }: any) => (
|
2022-06-21 09:08:37 +02:00
|
|
|
<ActionCell>
|
2022-05-26 10:37:33 +02:00
|
|
|
<PermissionIconButton
|
|
|
|
permission={UPDATE_PROJECT}
|
|
|
|
projectId={projectId}
|
2022-07-22 09:31:08 +02:00
|
|
|
onClick={() => {
|
|
|
|
setSelectedRow(row);
|
|
|
|
setAssignOpen(true);
|
|
|
|
}}
|
|
|
|
disabled={mappedData.length === 1}
|
2022-05-26 10:37:33 +02:00
|
|
|
tooltipProps={{
|
|
|
|
title:
|
2022-07-22 09:31:08 +02:00
|
|
|
mappedData.length === 1
|
|
|
|
? 'Cannot edit access. A project must have at least one owner'
|
|
|
|
: 'Edit access',
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Edit />
|
|
|
|
</PermissionIconButton>
|
|
|
|
<PermissionIconButton
|
|
|
|
permission={UPDATE_PROJECT}
|
|
|
|
projectId={projectId}
|
|
|
|
onClick={() => {
|
|
|
|
setSelectedRow(row);
|
|
|
|
setRemoveOpen(true);
|
|
|
|
}}
|
|
|
|
disabled={mappedData.length === 1}
|
|
|
|
tooltipProps={{
|
|
|
|
title:
|
|
|
|
mappedData.length === 1
|
2022-05-26 10:37:33 +02:00
|
|
|
? 'Cannot remove access. A project must have at least one owner'
|
|
|
|
: 'Remove access',
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Delete />
|
|
|
|
</PermissionIconButton>
|
2022-06-21 09:08:37 +02:00
|
|
|
</ActionCell>
|
2022-05-26 10:37:33 +02:00
|
|
|
),
|
|
|
|
},
|
|
|
|
],
|
2022-07-22 09:31:08 +02:00
|
|
|
[roles, mappedData.length, projectId]
|
2022-05-26 10:37:33 +02:00
|
|
|
);
|
|
|
|
|
2022-07-22 09:31:08 +02:00
|
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
|
|
const [initialState] = useState(() => ({
|
|
|
|
sortBy: [
|
2022-05-26 10:37:33 +02:00
|
|
|
{
|
2022-07-22 09:31:08 +02:00
|
|
|
id: searchParams.get('sort') || storedParams.id,
|
|
|
|
desc: searchParams.has('order')
|
|
|
|
? searchParams.get('order') === 'desc'
|
|
|
|
: storedParams.desc,
|
2022-05-26 10:37:33 +02:00
|
|
|
},
|
2022-07-22 09:31:08 +02:00
|
|
|
],
|
|
|
|
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);
|
|
|
|
};
|
2022-05-26 10:37:33 +02:00
|
|
|
return (
|
2022-07-22 09:31:08 +02:00
|
|
|
<PageContent
|
|
|
|
header={
|
|
|
|
<PageHeader
|
|
|
|
secondary
|
|
|
|
title={`Access (${
|
|
|
|
rows.length < data.length
|
|
|
|
? `${rows.length} of ${data.length}`
|
|
|
|
: data.length
|
|
|
|
})`}
|
|
|
|
actions={
|
|
|
|
<>
|
|
|
|
<ConditionallyRender
|
|
|
|
condition={!isSmallScreen}
|
|
|
|
show={
|
|
|
|
<>
|
|
|
|
<Search
|
|
|
|
initialValue={searchValue}
|
|
|
|
onChange={setSearchValue}
|
|
|
|
hasFilters
|
|
|
|
getSearchContext={getSearchContext}
|
|
|
|
/>
|
|
|
|
<PageHeader.Divider />
|
|
|
|
</>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
<Button
|
|
|
|
variant="contained"
|
|
|
|
color="primary"
|
|
|
|
onClick={() => setAssignOpen(true)}
|
|
|
|
>
|
|
|
|
Assign {entityType}
|
|
|
|
</Button>
|
|
|
|
</>
|
|
|
|
}
|
|
|
|
>
|
|
|
|
<ConditionallyRender
|
|
|
|
condition={isSmallScreen}
|
|
|
|
show={
|
|
|
|
<Search
|
|
|
|
initialValue={searchValue}
|
|
|
|
onChange={setSearchValue}
|
|
|
|
hasFilters
|
|
|
|
getSearchContext={getSearchContext}
|
|
|
|
/>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
</PageHeader>
|
|
|
|
}
|
|
|
|
>
|
|
|
|
<SearchHighlightProvider value={getSearchText(searchValue)}>
|
|
|
|
<VirtualizedTable
|
|
|
|
rows={rows}
|
|
|
|
headerGroups={headerGroups}
|
|
|
|
prepareRow={prepareRow}
|
|
|
|
/>
|
|
|
|
</SearchHighlightProvider>
|
|
|
|
<ConditionallyRender
|
|
|
|
condition={rows.length === 0}
|
|
|
|
show={
|
|
|
|
<ConditionallyRender
|
|
|
|
condition={searchValue?.length > 0}
|
|
|
|
show={
|
|
|
|
<TablePlaceholder>
|
|
|
|
No access found matching “
|
|
|
|
{searchValue}
|
|
|
|
”
|
|
|
|
</TablePlaceholder>
|
|
|
|
}
|
|
|
|
elseShow={
|
|
|
|
<TablePlaceholder>
|
|
|
|
No access available. Get started by assigning a{' '}
|
|
|
|
{entityType}.
|
|
|
|
</TablePlaceholder>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
<ProjectAccessAssign
|
|
|
|
open={assignOpen}
|
|
|
|
setOpen={setAssignOpen}
|
|
|
|
selected={selectedRow}
|
|
|
|
accesses={mappedData}
|
|
|
|
roles={roles}
|
|
|
|
entityType={entityType}
|
|
|
|
/>
|
|
|
|
<Dialogue
|
|
|
|
open={removeOpen}
|
|
|
|
onClick={() => removeAccess(selectedRow)}
|
|
|
|
onClose={() => {
|
|
|
|
setSelectedRow(undefined);
|
|
|
|
setRemoveOpen(false);
|
|
|
|
}}
|
|
|
|
title={`Really remove ${entityType} from this project?`}
|
|
|
|
/>
|
|
|
|
<ProjectGroupView
|
|
|
|
open={groupOpen}
|
|
|
|
setOpen={setGroupOpen}
|
|
|
|
group={selectedRow?.entity as IGroup}
|
|
|
|
projectId={projectId}
|
|
|
|
subtitle={`Role: ${
|
|
|
|
roles.find(({ id }) => id === selectedRow?.entity.roleId)
|
|
|
|
?.name
|
|
|
|
}`}
|
|
|
|
onEdit={() => {
|
|
|
|
setAssignOpen(true);
|
|
|
|
console.log('Assign Open true');
|
|
|
|
}}
|
|
|
|
onRemove={() => {
|
|
|
|
setGroupOpen(false);
|
|
|
|
setRemoveOpen(true);
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</PageContent>
|
2022-05-26 10:37:33 +02:00
|
|
|
);
|
|
|
|
};
|