1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-29 01:15:48 +02:00

Merge branch 'main' into task/Add_strategy_information_to_playground_results

This commit is contained in:
andreas-unleash 2022-08-08 14:07:08 +03:00 committed by GitHub
commit 74ccde2096
17 changed files with 306 additions and 144 deletions

View File

@ -18,18 +18,19 @@ import { CopyApiTokenButton } from 'component/admin/apiToken/CopyApiTokenButton/
import { RemoveApiTokenButton } from 'component/admin/apiToken/RemoveApiTokenButton/RemoveApiTokenButton'; import { RemoveApiTokenButton } from 'component/admin/apiToken/RemoveApiTokenButton/RemoveApiTokenButton';
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
import { sortTypes } from 'utils/sortTypes'; import { sortTypes } from 'utils/sortTypes';
import { useEffect, useMemo } from 'react'; import { useMemo } from 'react';
import theme from 'themes/theme'; import theme from 'themes/theme';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { ProjectsList } from 'component/admin/apiToken/ProjectsList/ProjectsList'; import { ProjectsList } from 'component/admin/apiToken/ProjectsList/ProjectsList';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
import { Search } from 'component/common/Search/Search'; import { Search } from 'component/common/Search/Search';
import useHiddenColumns from 'hooks/useHiddenColumns';
export const ApiTokenTable = () => { export const ApiTokenTable = () => {
const { tokens, loading } = useApiTokens(); const { tokens, loading } = useApiTokens();
const hiddenColumns = useHiddenColumns();
const initialState = useMemo(() => ({ sortBy: [{ id: 'createdAt' }] }), []); const initialState = useMemo(() => ({ sortBy: [{ id: 'createdAt' }] }), []);
const { uiConfig } = useUiConfig();
const { const {
getTableProps, getTableProps,
@ -52,9 +53,16 @@ export const ApiTokenTable = () => {
useSortBy useSortBy
); );
useEffect(() => { useHiddenColumns(
setHiddenColumns(hiddenColumns); setHiddenColumns,
}, [setHiddenColumns, hiddenColumns]); ['Icon', 'createdAt'],
useMediaQuery(theme.breakpoints.down('md'))
);
useHiddenColumns(
setHiddenColumns,
['projects', 'environment'],
!uiConfig.flags.E
);
return ( return (
<PageContent <PageContent
@ -124,27 +132,6 @@ export const ApiTokenTable = () => {
); );
}; };
const useHiddenColumns = (): string[] => {
const { uiConfig } = useUiConfig();
const isMediumScreen = useMediaQuery(theme.breakpoints.down('md'));
return useMemo(() => {
const hidden: string[] = [];
if (!uiConfig.flags.E) {
hidden.push('projects');
hidden.push('environment');
}
if (isMediumScreen) {
hidden.push('Icon');
hidden.push('createdAt');
}
return hidden;
}, [uiConfig, isMediumScreen]);
};
const COLUMNS = [ const COLUMNS = [
{ {
id: 'Icon', id: 'Icon',

View File

@ -1,13 +1,12 @@
import { useEffect, useMemo, useState, VFC } from 'react'; import { useEffect, useMemo, useState, VFC } from 'react';
import { import {
Button,
IconButton, IconButton,
styled, styled,
Tooltip, Tooltip,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from '@mui/material'; } from '@mui/material';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams, Link } from 'react-router-dom';
import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table'; import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table';
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table'; import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
import { useGroup } from 'hooks/api/getters/useGroup/useGroup'; import { useGroup } from 'hooks/api/getters/useGroup/useGroup';
@ -26,17 +25,17 @@ import { HighlightCell } from 'component/common/Table/cells/HighlightCell/Highli
import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell'; import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell';
import { GroupUserRoleCell } from 'component/admin/groups/GroupUserRoleCell/GroupUserRoleCell'; import { GroupUserRoleCell } from 'component/admin/groups/GroupUserRoleCell/GroupUserRoleCell';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import { Delete, Edit } from '@mui/icons-material'; import { Add, Delete, Edit } from '@mui/icons-material';
import { ADMIN } from 'component/providers/AccessProvider/permissions'; import { ADMIN } from 'component/providers/AccessProvider/permissions';
import { MainHeader } from 'component/common/MainHeader/MainHeader'; import { MainHeader } from 'component/common/MainHeader/MainHeader';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { RemoveGroup } from 'component/admin/groups/RemoveGroup/RemoveGroup'; import { RemoveGroup } from 'component/admin/groups/RemoveGroup/RemoveGroup';
import { Link } from 'react-router-dom';
import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell'; import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
import { AddGroupUser } from './AddGroupUser/AddGroupUser'; import { AddGroupUser } from './AddGroupUser/AddGroupUser';
import { EditGroupUser } from './EditGroupUser/EditGroupUser'; import { EditGroupUser } from './EditGroupUser/EditGroupUser';
import { RemoveGroupUser } from './RemoveGroupUser/RemoveGroupUser'; import { RemoveGroupUser } from './RemoveGroupUser/RemoveGroupUser';
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
import { import {
UG_EDIT_BTN_ID, UG_EDIT_BTN_ID,
UG_DELETE_BTN_ID, UG_DELETE_BTN_ID,
@ -140,30 +139,36 @@ export const Group: VFC = () => {
Cell: ({ row: { original: rowUser } }: any) => ( Cell: ({ row: { original: rowUser } }: any) => (
<ActionCell> <ActionCell>
<Tooltip title="Edit user" arrow describeChild> <Tooltip title="Edit user" arrow describeChild>
<IconButton <span>
data-testid={`${UG_EDIT_USER_BTN_ID}-${rowUser.id}`} <IconButton
onClick={() => { data-testid={`${UG_EDIT_USER_BTN_ID}-${rowUser.id}`}
setSelectedUser(rowUser); disabled={group?.users.length === 1}
setEditUserOpen(true); onClick={() => {
}} setSelectedUser(rowUser);
> setEditUserOpen(true);
<Edit /> }}
</IconButton> >
<Edit />
</IconButton>
</span>
</Tooltip> </Tooltip>
<Tooltip <Tooltip
title="Remove user from group" title="Remove user from group"
arrow arrow
describeChild describeChild
> >
<IconButton <span>
data-testid={`${UG_REMOVE_USER_BTN_ID}-${rowUser.id}`} <IconButton
onClick={() => { data-testid={`${UG_REMOVE_USER_BTN_ID}-${rowUser.id}`}
setSelectedUser(rowUser); disabled={group?.users.length === 1}
setRemoveUserOpen(true); onClick={() => {
}} setSelectedUser(rowUser);
> setRemoveUserOpen(true);
<Delete /> }}
</IconButton> >
<Delete />
</IconButton>
</span>
</Tooltip> </Tooltip>
</ActionCell> </ActionCell>
), ),
@ -171,7 +176,7 @@ export const Group: VFC = () => {
disableSortBy: true, disableSortBy: true,
}, },
], ],
[setSelectedUser, setRemoveUserOpen] [setSelectedUser, setRemoveUserOpen, group?.users.length]
); );
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
@ -306,16 +311,17 @@ export const Group: VFC = () => {
</> </>
} }
/> />
<Button <ResponsiveButton
data-testid={UG_ADD_USER_BTN_ID} data-testid={UG_ADD_USER_BTN_ID}
variant="contained"
color="primary"
onClick={() => { onClick={() => {
setAddUserOpen(true); setAddUserOpen(true);
}} }}
maxWidth="700px"
Icon={Add}
permission={ADMIN}
> >
Add user Add user
</Button> </ResponsiveButton>
</> </>
} }
> >
@ -376,13 +382,13 @@ export const Group: VFC = () => {
<EditGroupUser <EditGroupUser
open={editUserOpen} open={editUserOpen}
setOpen={setEditUserOpen} setOpen={setEditUserOpen}
user={selectedUser!} user={selectedUser}
group={group!} group={group!}
/> />
<RemoveGroupUser <RemoveGroupUser
open={removeUserOpen} open={removeUserOpen}
setOpen={setRemoveUserOpen} setOpen={setRemoveUserOpen}
user={selectedUser!} user={selectedUser}
group={group!} group={group!}
/> />
</PageContent> </PageContent>

View File

@ -47,23 +47,22 @@ export const RemoveGroupUser: FC<IRemoveGroupUserProps> = ({
} }
}; };
const userName = user?.name || user?.username || user?.email;
return ( return (
<Dialogue <Dialogue
open={open && Boolean(user)} open={open && Boolean(user)}
primaryButtonText="Remove" primaryButtonText="Remove user"
secondaryButtonText="Cancel" secondaryButtonText="Cancel"
onClick={onRemoveClick} onClick={onRemoveClick}
onClose={() => { onClose={() => {
setOpen(false); setOpen(false);
}} }}
title="Remove user from group" title="Remove user from group?"
> >
<Typography> <Typography>
Are you sure you wish to remove{' '} Do you really want to remove <strong>{userName}</strong> from{' '}
<strong>{user?.name || user?.username || user?.email}</strong>{' '} <strong>{group.name}</strong>? <strong>{userName}</strong> will
from <strong>{group.name}</strong>? Removing the user from this lose all access rights granted by this group.
group may also remove their access from projects this group is
assigned to.
</Typography> </Typography>
</Dialogue> </Dialogue>
); );

View File

@ -1,5 +1,5 @@
import { useMemo, VFC } from 'react'; import { useMemo, VFC } from 'react';
import { IconButton, Tooltip } from '@mui/material'; import { IconButton, Tooltip, useMediaQuery } from '@mui/material';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { IGroupUser } from 'interfaces/group'; import { IGroupUser } from 'interfaces/group';
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
@ -11,6 +11,8 @@ import { VirtualizedTable } from 'component/common/Table';
import { useFlexLayout, useSortBy, useTable } from 'react-table'; import { useFlexLayout, useSortBy, useTable } from 'react-table';
import { sortTypes } from 'utils/sortTypes'; import { sortTypes } from 'utils/sortTypes';
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
import theme from 'themes/theme';
import useHiddenColumns from 'hooks/useHiddenColumns';
interface IGroupFormUsersTableProps { interface IGroupFormUsersTableProps {
users: IGroupUser[]; users: IGroupUser[];
@ -106,7 +108,7 @@ export const GroupFormUsersTable: VFC<IGroupFormUsersTableProps> = ({
[setUsers] [setUsers]
); );
const { headerGroups, rows, prepareRow } = useTable( const { headerGroups, rows, prepareRow, setHiddenColumns } = useTable(
{ {
columns: columns as any[], columns: columns as any[],
data: users as any[], data: users as any[],
@ -119,6 +121,12 @@ export const GroupFormUsersTable: VFC<IGroupFormUsersTableProps> = ({
useFlexLayout useFlexLayout
); );
useHiddenColumns(
setHiddenColumns,
['imageUrl', 'name'],
useMediaQuery(theme.breakpoints.down('md'))
);
return ( return (
<ConditionallyRender <ConditionallyRender
condition={rows.length > 0} condition={rows.length > 0}

View File

@ -1,6 +1,6 @@
import { useEffect, useMemo, useState, VFC } from 'react'; import { useEffect, useMemo, useState, VFC } from 'react';
import { useGroups } from 'hooks/api/getters/useGroups/useGroups'; import { useGroups } from 'hooks/api/getters/useGroups/useGroups';
import { Link, useSearchParams } from 'react-router-dom'; import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { IGroup } from 'interfaces/group'; import { IGroup } from 'interfaces/group';
import { PageContent } from 'component/common/PageContent/PageContent'; import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { PageHeader } from 'component/common/PageHeader/PageHeader';
@ -12,6 +12,9 @@ import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightC
import { TablePlaceholder } from 'component/common/Table'; import { TablePlaceholder } from 'component/common/Table';
import { GroupCard } from './GroupCard/GroupCard'; import { GroupCard } from './GroupCard/GroupCard';
import { GroupEmpty } from './GroupEmpty/GroupEmpty'; import { GroupEmpty } from './GroupEmpty/GroupEmpty';
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
import { ADMIN } from 'component/providers/AccessProvider/permissions';
import { Add } from '@mui/icons-material';
import { NAVIGATE_TO_CREATE_GROUP } from 'utils/testIds'; import { NAVIGATE_TO_CREATE_GROUP } from 'utils/testIds';
type PageQueryType = Partial<Record<'search', string>>; type PageQueryType = Partial<Record<'search', string>>;
@ -33,6 +36,7 @@ const groupsSearch = (group: IGroup, searchValue: string) => {
}; };
export const GroupsList: VFC = () => { export const GroupsList: VFC = () => {
const navigate = useNavigate();
const { groups = [], loading } = useGroups(); const { groups = [], loading } = useGroups();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const [searchValue, setSearchValue] = useState( const [searchValue, setSearchValue] = useState(
@ -81,15 +85,17 @@ export const GroupsList: VFC = () => {
</> </>
} }
/> />
<Button <ResponsiveButton
to="/admin/groups/create-group" onClick={() =>
component={Link} navigate('/admin/groups/create-group')
variant="contained" }
color="primary" maxWidth="700px"
Icon={Add}
permission={ADMIN}
data-testid={NAVIGATE_TO_CREATE_GROUP} data-testid={NAVIGATE_TO_CREATE_GROUP}
> >
New group New group
</Button> </ResponsiveButton>
</> </>
} }
> >

View File

@ -42,18 +42,18 @@ export const RemoveGroup: FC<IRemoveGroupProps> = ({
return ( return (
<Dialogue <Dialogue
open={open} open={open}
primaryButtonText="Remove" primaryButtonText="Delete group"
secondaryButtonText="Cancel" secondaryButtonText="Cancel"
onClick={onRemoveClick} onClick={onRemoveClick}
onClose={() => { onClose={() => {
setOpen(false); setOpen(false);
}} }}
title="Delete group" title="Delete group?"
> >
<Typography> <Typography>
Are you sure you wish to delete <strong>{group.name}</strong>? Do you really want to delete <strong>{group.name}</strong>?
If this group is currently assigned to one or more projects then Users who are granted access to projects only via this group
users belonging to this group may lose access to those projects. will lose access to those projects.
</Typography> </Typography>
</Dialogue> </Dialogue>
); );

View File

@ -13,7 +13,7 @@ const StyledMainHeader = styled(Paper)(({ theme }) => ({
const StyledTitleHeader = styled('div')(({ theme }) => ({ const StyledTitleHeader = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'flex-start',
justifyContent: 'space-between', justifyContent: 'space-between',
})); }));

View File

@ -0,0 +1,50 @@
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import React from 'react';
import { formatUnknownError } from 'utils/formatUnknownError';
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
import useProjects from 'hooks/api/getters/useProjects/useProjects';
import useToast from 'hooks/useToast';
interface IDeleteProjectDialogueProps {
project: string;
open: boolean;
onClose?: () => void;
onSuccess?: () => void;
}
export const DeleteProjectDialogue = ({
open,
onClose,
project,
onSuccess,
}: IDeleteProjectDialogueProps) => {
const { deleteProject } = useProjectApi();
const { refetch: refetchProjectOverview } = useProjects();
const { setToastData, setToastApiError } = useToast();
const onClick = async (e: React.SyntheticEvent) => {
e.preventDefault();
try {
await deleteProject(project);
refetchProjectOverview();
setToastData({
title: 'Deleted project',
type: 'success',
text: 'Successfully deleted project',
});
onSuccess?.();
} catch (ex: unknown) {
setToastApiError(formatUnknownError(ex));
}
onClose?.();
};
return (
<Dialogue
open={open}
onClick={onClick}
onClose={onClose}
title="Really delete project"
/>
);
};

View File

@ -18,7 +18,7 @@ export const useStyles = makeStyles()(theme => ({
marginBottom: '1rem', marginBottom: '1rem',
}, },
innerContainer: { innerContainer: {
padding: '1rem 2rem', padding: '1.25rem 2rem',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
}, },

View File

@ -4,21 +4,44 @@ import useLoading from 'hooks/useLoading';
import ApiError from 'component/common/ApiError/ApiError'; import ApiError from 'component/common/ApiError/ApiError';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useStyles } from './Project.styles'; import { useStyles } from './Project.styles';
import { Tab, Tabs } from '@mui/material'; import { styled, Tab, Tabs } from '@mui/material';
import { Edit } from '@mui/icons-material'; import { Delete, Edit } from '@mui/icons-material';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import useQueryParams from 'hooks/useQueryParams'; import useQueryParams from 'hooks/useQueryParams';
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import { ProjectAccess } from '../ProjectAccess/ProjectAccess'; import { ProjectAccess } from '../ProjectAccess/ProjectAccess';
import ProjectEnvironment from '../ProjectEnvironment/ProjectEnvironment'; import ProjectEnvironment from '../ProjectEnvironment/ProjectEnvironment';
import { ProjectFeaturesArchive } from './ProjectFeaturesArchive/ProjectFeaturesArchive'; import { ProjectFeaturesArchive } from './ProjectFeaturesArchive/ProjectFeaturesArchive';
import ProjectOverview from './ProjectOverview'; import ProjectOverview from './ProjectOverview';
import ProjectHealth from './ProjectHealth/ProjectHealth'; import ProjectHealth from './ProjectHealth/ProjectHealth';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions'; import {
DELETE_PROJECT,
UPDATE_PROJECT,
} from 'component/providers/AccessProvider/permissions';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { Routes, Route, useLocation } from 'react-router-dom'; import { Routes, Route, useLocation } from 'react-router-dom';
import { DeleteProjectDialogue } from './DeleteProject/DeleteProjectDialogue';
const StyledDiv = styled('div')(() => ({
display: 'flex',
}));
const StyledName = styled('div')(({ theme }) => ({
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
paddingBottom: theme.spacing(2),
}));
const StyledTitle = styled('span')(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
fontWeight: 'normal',
}));
const StyledText = styled(StyledTitle)(({ theme }) => ({
color: theme.palette.grey[800],
}));
const Project = () => { const Project = () => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
@ -33,6 +56,8 @@ const Project = () => {
const basePath = `/projects/${projectId}`; const basePath = `/projects/${projectId}`;
const projectName = project?.name || projectId; const projectName = project?.name || projectId;
const [showDelDialog, setShowDelDialog] = useState(false);
const tabs = [ const tabs = [
{ {
title: 'Overview', title: 'Overview',
@ -85,21 +110,60 @@ const Project = () => {
<div className={styles.header}> <div className={styles.header}>
<div className={styles.innerContainer}> <div className={styles.innerContainer}>
<h2 className={styles.title}> <h2 className={styles.title}>
<div className={styles.titleText} data-loading> <div>
{projectName} <StyledName data-loading>{projectName}</StyledName>
<ConditionallyRender
condition={Boolean(project.description)}
show={
<StyledDiv>
<StyledTitle data-loading>
Description:&nbsp;
</StyledTitle>
<StyledText data-loading>
{project.description}
</StyledText>
</StyledDiv>
}
/>
<StyledDiv>
<StyledTitle data-loading>
projectId:&nbsp;
</StyledTitle>
<StyledText data-loading>
{projectId}
</StyledText>
</StyledDiv>
</div> </div>
<PermissionIconButton <StyledDiv>
permission={UPDATE_PROJECT} <PermissionIconButton
projectId={projectId} permission={UPDATE_PROJECT}
sx={{ visibility: isOss() ? 'hidden' : 'visible' }} projectId={projectId}
onClick={() => sx={{
navigate(`/projects/${projectId}/edit`) visibility: isOss() ? 'hidden' : 'visible',
} }}
tooltipProps={{ title: 'Edit project' }} onClick={() =>
data-loading navigate(`/projects/${projectId}/edit`)
> }
<Edit /> tooltipProps={{ title: 'Edit project' }}
</PermissionIconButton> data-loading
>
<Edit />
</PermissionIconButton>
<PermissionIconButton
permission={DELETE_PROJECT}
projectId={projectId}
sx={{
visibility: isOss() ? 'hidden' : 'visible',
}}
onClick={() => {
setShowDelDialog(true);
}}
tooltipProps={{ title: 'Delete project' }}
data-loading
>
<Delete />
</PermissionIconButton>
</StyledDiv>
</h2> </h2>
</div> </div>
<ConditionallyRender <ConditionallyRender
@ -132,6 +196,16 @@ const Project = () => {
</Tabs> </Tabs>
</div> </div>
</div> </div>
<DeleteProjectDialogue
project={projectId}
open={showDelDialog}
onClose={() => {
setShowDelDialog(false);
}}
onSuccess={() => {
navigate('/projects');
}}
/>
<Routes> <Routes>
<Route path="health" element={<ProjectHealth />} /> <Route path="health" element={<ProjectHealth />} />
<Route path="access/*" element={<ProjectAccess />} /> <Route path="access/*" element={<ProjectAccess />} />

View File

@ -47,8 +47,8 @@ const StyledAutocompleteWrapper = styled('div')(({ theme }) => ({
}, },
})); }));
const StyledButtonContainer = styled('div')(() => ({ const StyledButtonContainer = styled('div')(({ theme }) => ({
marginTop: 'auto', marginTop: theme.spacing(6),
display: 'flex', display: 'flex',
justifyContent: 'flex-end', justifyContent: 'flex-end',
})); }));

View File

@ -9,7 +9,7 @@ const StyledDescription = styled('div')(({ theme }) => ({
padding: theme.spacing(3), padding: theme.spacing(3),
backgroundColor: theme.palette.neutral.light, backgroundColor: theme.palette.neutral.light,
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
fontSize: theme.fontSizes.smallerBody, fontSize: theme.fontSizes.smallBody,
borderRadius: theme.shape.borderRadiusMedium, borderRadius: theme.shape.borderRadiusMedium,
})); }));

View File

@ -1,8 +1,8 @@
import { useEffect, useMemo, useState, VFC } from 'react'; import { useEffect, useMemo, useState, VFC } from 'react';
import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table'; import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table';
import { VirtualizedTable, TablePlaceholder } from 'component/common/Table'; import { VirtualizedTable, TablePlaceholder } from 'component/common/Table';
import { Button, useMediaQuery, useTheme } from '@mui/material'; import { styled, useMediaQuery, useTheme } from '@mui/material';
import { Delete, Edit } from '@mui/icons-material'; import { Add, Delete, Edit } from '@mui/icons-material';
import { sortTypes } from 'utils/sortTypes'; import { sortTypes } from 'utils/sortTypes';
import useProjectAccess, { import useProjectAccess, {
ENTITY_TYPE, ENTITY_TYPE,
@ -38,9 +38,11 @@ import { IUser } from 'interfaces/user';
import { IGroup } from 'interfaces/group'; import { IGroup } from 'interfaces/group';
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
import { ProjectAccessCreate } from 'component/project/ProjectAccess/ProjectAccessCreate/ProjectAccessCreate'; import { ProjectAccessCreate } from 'component/project/ProjectAccess/ProjectAccessCreate/ProjectAccessCreate';
import { ProjectAccessEditUser } from 'component/project/ProjectAccess/ProjectAccessEditUser/ProjectAccessEditUser'; import { ProjectAccessEditUser } from 'component/project/ProjectAccess/ProjectAccessEditUser/ProjectAccessEditUser';
import { ProjectAccessEditGroup } from 'component/project/ProjectAccess/ProjectAccessEditGroup/ProjectAccessEditGroup'; import { ProjectAccessEditGroup } from 'component/project/ProjectAccess/ProjectAccessEditGroup/ProjectAccessEditGroup';
import useHiddenColumns from 'hooks/useHiddenColumns';
export type PageQueryType = Partial< export type PageQueryType = Partial<
Record<'sort' | 'order' | 'search', string> Record<'sort' | 'order' | 'search', string>
@ -53,6 +55,20 @@ const { value: storedParams, setValue: setStoredParams } = createLocalStorage(
defaultSort defaultSort
); );
const StyledUserAvatars = styled('div')(({ theme }) => ({
display: 'inline-flex',
alignItems: 'center',
flexWrap: 'wrap',
marginLeft: theme.spacing(1),
}));
const StyledEmptyAvatar = styled(UserAvatar)(({ theme }) => ({
marginRight: theme.spacing(-3.5),
}));
const StyledGroupAvatar = styled(UserAvatar)(({ theme }) => ({
outline: `${theme.spacing(0.25)} solid ${theme.palette.background.paper}`,
}));
export const ProjectAccessTable: VFC = () => { export const ProjectAccessTable: VFC = () => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
@ -77,11 +93,15 @@ export const ProjectAccessTable: VFC = () => {
Header: 'Avatar', Header: 'Avatar',
accessor: 'imageUrl', accessor: 'imageUrl',
Cell: ({ row: { original: row } }: any) => ( Cell: ({ row: { original: row } }: any) => (
<TextCell> <StyledUserAvatars>
<UserAvatar user={row.entity}> <ConditionallyRender
condition={row.type === ENTITY_TYPE.GROUP}
show={<StyledEmptyAvatar />}
/>
<StyledGroupAvatar user={row.entity}>
{row.entity.users?.length} {row.entity.users?.length}
</UserAvatar> </StyledGroupAvatar>
</TextCell> </StyledUserAvatars>
), ),
maxWidth: 85, maxWidth: 85,
disableSortBy: true, disableSortBy: true,
@ -124,6 +144,7 @@ export const ProjectAccessTable: VFC = () => {
searchable: true, searchable: true,
}, },
{ {
id: 'role',
Header: 'Role', Header: 'Role',
accessor: (row: IProjectAccess) => accessor: (row: IProjectAccess) =>
access?.roles.find(({ id }) => id === row.entity.roleId) access?.roles.find(({ id }) => id === row.entity.roleId)
@ -145,6 +166,7 @@ export const ProjectAccessTable: VFC = () => {
maxWidth: 150, maxWidth: 150,
}, },
{ {
id: 'lastLogin',
Header: 'Last login', Header: 'Last login',
accessor: (row: IProjectAccess) => { accessor: (row: IProjectAccess) => {
if (row.type === ENTITY_TYPE.USER) { if (row.type === ENTITY_TYPE.USER) {
@ -240,6 +262,7 @@ export const ProjectAccessTable: VFC = () => {
headerGroups, headerGroups,
rows, rows,
prepareRow, prepareRow,
setHiddenColumns,
state: { sortBy }, state: { sortBy },
} = useTable( } = useTable(
{ {
@ -258,6 +281,12 @@ export const ProjectAccessTable: VFC = () => {
useFlexLayout useFlexLayout
); );
useHiddenColumns(
setHiddenColumns,
['imageUrl', 'username', 'role', 'added', 'lastLogin'],
isSmallScreen
);
useEffect(() => { useEffect(() => {
const tableState: PageQueryType = {}; const tableState: PageQueryType = {};
tableState.sort = sortBy[0].id; tableState.sort = sortBy[0].id;
@ -332,14 +361,15 @@ export const ProjectAccessTable: VFC = () => {
</> </>
} }
/> />
<Button <ResponsiveButton
component={Link} onClick={() => navigate('create')}
to={`create`} maxWidth="700px"
variant="contained" Icon={Add}
color="primary" permission={UPDATE_PROJECT}
projectId={projectId}
> >
Assign {entityType} Assign {entityType}
</Button> </ResponsiveButton>
</> </>
} }
> >

View File

@ -20,17 +20,22 @@ import { IGroup, IGroupUser } from 'interfaces/group';
import { VFC, useState } from 'react'; import { VFC, useState } from 'react';
import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table'; import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table';
import { sortTypes } from 'utils/sortTypes'; import { sortTypes } from 'utils/sortTypes';
import useHiddenColumns from 'hooks/useHiddenColumns';
const StyledPageContent = styled(PageContent)(({ theme }) => ({ const StyledPageContent = styled(PageContent)(({ theme }) => ({
height: '100vh', height: '100vh',
overflow: 'auto', overflow: 'auto',
padding: theme.spacing(7.5, 6), padding: theme.spacing(7.5, 6),
[theme.breakpoints.down('md')]: {
padding: theme.spacing(4, 2),
},
'& .header': { '& .header': {
padding: theme.spacing(0, 0, 2, 0), padding: theme.spacing(0, 0, 2, 0),
}, },
'& .body': { '& .body': {
padding: theme.spacing(3, 0, 0, 0), padding: theme.spacing(3, 0, 0, 0),
}, },
borderRadius: `${theme.spacing(1.5, 0, 0, 1.5)} !important`,
})); }));
const StyledTitle = styled('div')(({ theme }) => ({ const StyledTitle = styled('div')(({ theme }) => ({
@ -38,7 +43,7 @@ const StyledTitle = styled('div')(({ theme }) => ({
flexDirection: 'column', flexDirection: 'column',
'& > span': { '& > span': {
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
fontSize: theme.fontSizes.smallBody, fontSize: theme.fontSizes.bodySize,
}, },
})); }));
@ -80,6 +85,7 @@ const columns = [
filterName: 'type', filterName: 'type',
}, },
{ {
id: 'joined',
Header: 'Joined', Header: 'Joined',
accessor: 'joinedAt', accessor: 'joinedAt',
Cell: DateCell, Cell: DateCell,
@ -87,6 +93,7 @@ const columns = [
maxWidth: 150, maxWidth: 150,
}, },
{ {
id: 'lastLogin',
Header: 'Last login', Header: 'Last login',
accessor: (row: IGroupUser) => row.seenAt || '', accessor: (row: IGroupUser) => row.seenAt || '',
Cell: ({ row: { original: user } }: any) => ( Cell: ({ row: { original: user } }: any) => (
@ -135,7 +142,7 @@ export const ProjectGroupView: VFC<IProjectGroupViewProps> = ({
group?.users ?? [] group?.users ?? []
); );
const { headerGroups, rows, prepareRow } = useTable( const { headerGroups, rows, prepareRow, setHiddenColumns } = useTable(
{ {
columns: columns as any[], columns: columns as any[],
data, data,
@ -149,6 +156,12 @@ export const ProjectGroupView: VFC<IProjectGroupViewProps> = ({
useFlexLayout useFlexLayout
); );
useHiddenColumns(
setHiddenColumns,
['imageUrl', 'name', 'joined', 'lastLogin'],
useMediaQuery(theme.breakpoints.down('md'))
);
return ( return (
<SidebarModal <SidebarModal
open={open} open={open}

View File

@ -32,6 +32,7 @@ export const useStyles = makeStyles()(theme => ({
boxOrient: 'vertical', boxOrient: 'vertical',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
overflow: 'hidden', overflow: 'hidden',
alignItems: 'flex-start',
}, },
projectIcon: { projectIcon: {

View File

@ -4,21 +4,17 @@ import MoreVertIcon from '@mui/icons-material/MoreVert';
import { ReactComponent as ProjectIcon } from 'assets/icons/projectIcon.svg'; import { ReactComponent as ProjectIcon } from 'assets/icons/projectIcon.svg';
import React, { useState, SyntheticEvent, useContext } from 'react'; import React, { useState, SyntheticEvent, useContext } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
import useProjects from 'hooks/api/getters/useProjects/useProjects';
import { Delete, Edit } from '@mui/icons-material'; import { Delete, Edit } from '@mui/icons-material';
import { getProjectEditPath } from 'utils/routePathHelpers'; import { getProjectEditPath } from 'utils/routePathHelpers';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import useToast from 'hooks/useToast';
import { import {
UPDATE_PROJECT, UPDATE_PROJECT,
DELETE_PROJECT, DELETE_PROJECT,
} from 'component/providers/AccessProvider/permissions'; } from 'component/providers/AccessProvider/permissions';
import { formatUnknownError } from 'utils/formatUnknownError';
import AccessContext from 'contexts/AccessContext'; import AccessContext from 'contexts/AccessContext';
import { DEFAULT_PROJECT_ID } from 'hooks/api/getters/useDefaultProject/useDefaultProjectId'; import { DEFAULT_PROJECT_ID } from 'hooks/api/getters/useDefaultProject/useDefaultProjectId';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { DeleteProjectDialogue } from '../Project/DeleteProject/DeleteProjectDialogue';
interface IProjectCardProps { interface IProjectCardProps {
name: string; name: string;
@ -40,36 +36,15 @@ export const ProjectCard = ({
const { classes } = useStyles(); const { classes } = useStyles();
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
const { isOss } = useUiConfig(); const { isOss } = useUiConfig();
const { refetch: refetchProjectOverview } = useProjects();
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null);
const [showDelDialog, setShowDelDialog] = useState(false); const [showDelDialog, setShowDelDialog] = useState(false);
const { deleteProject } = useProjectApi();
const navigate = useNavigate(); const navigate = useNavigate();
const { setToastData, setToastApiError } = useToast();
// @ts-expect-error // @ts-expect-error
const handleClick = e => { const handleClick = e => {
e.preventDefault(); e.preventDefault();
setAnchorEl(e.currentTarget); setAnchorEl(e.currentTarget);
}; };
const onRemoveProject = async (e: React.SyntheticEvent) => {
e.preventDefault();
try {
await deleteProject(id);
refetchProjectOverview();
setToastData({
title: 'Deleted project',
type: 'success',
text: 'Successfully deleted project',
});
} catch (e: unknown) {
setToastApiError(formatUnknownError(e));
}
setShowDelDialog(false);
setAnchorEl(null);
};
const canDeleteProject = const canDeleteProject =
hasAccess(DELETE_PROJECT, id) && id !== DEFAULT_PROJECT_ID; hasAccess(DELETE_PROJECT, id) && id !== DEFAULT_PROJECT_ID;
@ -152,15 +127,13 @@ export const ProjectCard = ({
<p data-loading>members</p> <p data-loading>members</p>
</div> </div>
</div> </div>
<Dialogue <DeleteProjectDialogue
project={id}
open={showDelDialog} open={showDelDialog}
onClick={onRemoveProject} onClose={() => {
onClose={event => {
event.preventDefault();
setAnchorEl(null); setAnchorEl(null);
setShowDelDialog(false); setShowDelDialog(false);
}} }}
title="Really delete project"
/> />
</Card> </Card>
); );

View File

@ -0,0 +1,15 @@
import { useEffect } from 'react';
import { IdType } from 'react-table';
const useHiddenColumns = (
setHiddenColumns: <D>(param: Array<IdType<D>>) => void,
hiddenColumns: string[],
condition: boolean
) => {
useEffect(() => {
const hidden = condition ? hiddenColumns : [];
setHiddenColumns(hidden);
}, [setHiddenColumns, condition]);
};
export default useHiddenColumns;