1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-31 13:47:02 +02:00

feat: Admin project roles table (#1030)

* feat: new admin project roles table

* small fixes

* replace Box in defaultColumn Cell with the new TextCell

* refactor: slight adjustments

* misc improvements

* add HighlightCell

* fix: description width

* Update src/component/admin/projectRoles/ProjectRoles/ProjectRoleList/ProjectRoleList.tsx

Co-authored-by: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com>

* address PR comments, small tooltip fixes

* fix: prettier fmt

Co-authored-by: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com>
This commit is contained in:
Nuno Góis 2022-05-26 15:27:20 +01:00 committed by GitHub
parent 9ac962da45
commit b61980e71b
8 changed files with 265 additions and 117 deletions

View File

@ -79,8 +79,6 @@ export const AddonList = () => {
condition={addons.length > 0} condition={addons.length > 0}
show={<ConfiguredAddons />} show={<ConfiguredAddons />}
/> />
<br />
<AvailableAddons loading={loading} providers={providers} /> <AvailableAddons loading={loading} providers={providers} />
</> </>
); );

View File

@ -1,19 +1,12 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { Box, Table, TableBody, TableCell, TableRow } from '@mui/material'; import { Table, TableBody, TableCell, TableRow } from 'component/common/Table';
import { Delete, Edit, Visibility, VisibilityOff } from '@mui/icons-material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import {
DELETE_ADDON,
UPDATE_ADDON,
} from 'component/providers/AccessProvider/permissions';
import { useNavigate } from 'react-router-dom';
import { PageContent } from 'component/common/PageContent/PageContent'; import { PageContent } from 'component/common/PageContent/PageContent';
import useAddons from 'hooks/api/getters/useAddons/useAddons'; import useAddons from 'hooks/api/getters/useAddons/useAddons';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import useAddonsApi from 'hooks/api/actions/useAddonsApi/useAddonsApi'; import useAddonsApi from 'hooks/api/actions/useAddonsApi/useAddonsApi';
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { IAddon } from 'interfaces/addons'; import { IAddon } from 'interfaces/addons';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import { Dialogue } from 'component/common/Dialogue/Dialogue'; import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
@ -169,6 +162,7 @@ export const ConfiguredAddons = () => {
<PageContent <PageContent
isLoading={loading} isLoading={loading}
header={<PageHeader title="Configured addons" />} header={<PageHeader title="Configured addons" />}
sx={theme => ({ marginBottom: theme.spacing(2) })}
> >
<Table {...getTableProps()}> <Table {...getTableProps()}>
<SortableTableHeader headerGroups={headerGroups} /> <SortableTableHeader headerGroups={headerGroups} />

View File

@ -59,7 +59,7 @@ function AdminMenu() {
to="/admin/roles" to="/admin/roles"
style={createNavLinkStyle} style={createNavLinkStyle}
> >
<span>Project Roles</span> <span>Project roles</span>
</NavLink> </NavLink>
} }
/> />
@ -69,7 +69,7 @@ function AdminMenu() {
value="/admin/api" value="/admin/api"
label={ label={
<NavLink to="/admin/api" style={createNavLinkStyle}> <NavLink to="/admin/api" style={createNavLinkStyle}>
API Access API access
</NavLink> </NavLink>
} }
/> />
@ -77,7 +77,7 @@ function AdminMenu() {
value="/admin/auth" value="/admin/auth"
label={ label={
<NavLink to="/admin/auth" style={createNavLinkStyle}> <NavLink to="/admin/auth" style={createNavLinkStyle}>
Single Sign-On Single sign-on
</NavLink> </NavLink>
} }
/> />

View File

@ -1,37 +1,47 @@
import { useContext, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { import {
Table, Table,
SortableTableHeader,
TableBody, TableBody,
TableCell, TableCell,
TableHead,
TableRow, TableRow,
} from '@mui/material'; TablePlaceholder,
import AccessContext from 'contexts/AccessContext'; TableSearch,
import usePagination from 'hooks/usePagination'; } from 'component/common/Table';
import { useTable, useGlobalFilter, useSortBy } from 'react-table';
import { ADMIN } from 'component/providers/AccessProvider/permissions'; import { ADMIN } from 'component/providers/AccessProvider/permissions';
import PaginateUI from 'component/common/PaginateUI/PaginateUI';
import ProjectRoleListItem from './ProjectRoleListItem/ProjectRoleListItem';
import useProjectRoles from 'hooks/api/getters/useProjectRoles/useProjectRoles'; import useProjectRoles from 'hooks/api/getters/useProjectRoles/useProjectRoles';
import IRole, { IProjectRole } from 'interfaces/role'; import IRole, { IProjectRole } from 'interfaces/role';
import useProjectRolesApi from 'hooks/api/actions/useProjectRolesApi/useProjectRolesApi'; import useProjectRolesApi from 'hooks/api/actions/useProjectRolesApi/useProjectRolesApi';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import ProjectRoleDeleteConfirm from '../ProjectRoleDeleteConfirm/ProjectRoleDeleteConfirm'; import ProjectRoleDeleteConfirm from '../ProjectRoleDeleteConfirm/ProjectRoleDeleteConfirm';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { useStyles } from './ProjectRoleListItem/ProjectRoleListItem.styles'; import { Box, Button, useMediaQuery } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import { Delete, Edit, SupervisedUserCircle } from '@mui/icons-material';
import { useNavigate } from 'react-router-dom';
import { PageContent } from 'component/common/PageContent/PageContent';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { sortTypes } from 'utils/sortTypes';
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
import theme from 'themes/theme';
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
const ROOTROLE = 'root'; const ROOTROLE = 'root';
const BUILTIN_ROLE_TYPE = 'project';
const ProjectRoleList = () => { const ProjectRoleList = () => {
const { hasAccess } = useContext(AccessContext); const navigate = useNavigate();
const { roles } = useProjectRoles(); const { roles, refetch, loading } = useProjectRoles();
const { classes: styles } = useStyles();
const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
const paginationFilter = (role: IRole) => role?.type !== ROOTROLE; const paginationFilter = (role: IRole) => role?.type !== ROOTROLE;
const data = roles.filter(paginationFilter);
const { page, pages, nextPage, prevPage, setPageIndex, pageIndex } =
usePagination(roles, 10, paginationFilter);
const { deleteRole } = useProjectRolesApi(); const { deleteRole } = useProjectRolesApi();
const { refetch } = useProjectRoles();
const [currentRole, setCurrentRole] = useState<IProjectRole | null>(null); const [currentRole, setCurrentRole] = useState<IProjectRole | null>(null);
const [delDialog, setDelDialog] = useState(false); const [delDialog, setDelDialog] = useState(false);
const [confirmName, setConfirmName] = useState(''); const [confirmName, setConfirmName] = useState('');
@ -54,60 +64,198 @@ const ProjectRoleList = () => {
setConfirmName(''); setConfirmName('');
}; };
const renderRoles = () => { const columns = useMemo(
return page.map((role: IProjectRole) => { () => [
return ( {
<ProjectRoleListItem id: 'Icon',
key={role.id} Cell: () => (
id={role.id} <IconCell
name={role.name} icon={<SupervisedUserCircle color="disabled" />}
type={role.type} />
description={role.description} ),
// @ts-expect-error },
setCurrentRole={setCurrentRole} {
setDelDialog={setDelDialog} Header: 'Project role',
/> accessor: 'name',
); },
}); {
}; Header: 'Description',
accessor: 'description',
width: '90%',
},
{
Header: 'Actions',
id: 'Actions',
align: 'center',
Cell: ({
row: {
original: { id, type, name, description },
},
}: any) => (
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<PermissionIconButton
data-loading
disabled={type === BUILTIN_ROLE_TYPE}
onClick={() => {
navigate(`/admin/roles/${id}/edit`);
}}
permission={ADMIN}
tooltipProps={{
title:
type === BUILTIN_ROLE_TYPE
? 'You cannot edit role'
: 'Edit role',
}}
>
<Edit />
</PermissionIconButton>
<PermissionIconButton
data-loading
disabled={type === BUILTIN_ROLE_TYPE}
onClick={() => {
setCurrentRole({
id,
name,
description,
} as IProjectRole);
setDelDialog(true);
}}
permission={ADMIN}
tooltipProps={{
title:
type === BUILTIN_ROLE_TYPE
? 'You cannot remove role'
: 'Remove role',
}}
>
<Delete />
</PermissionIconButton>
</Box>
),
width: 100,
disableSortBy: true,
},
],
[navigate]
);
if (!roles) return null; const initialState = useMemo(
() => ({
sortBy: [{ id: 'name', desc: false }],
}),
[]
);
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
state: { globalFilter },
setGlobalFilter,
setHiddenColumns,
} = useTable(
{
columns: columns as any[], // TODO: fix after `react-table` v8 update
data,
initialState,
sortTypes,
autoResetGlobalFilter: false,
autoResetSortBy: false,
disableSortRemove: true,
defaultColumn: {
Cell: HighlightCell,
},
},
useGlobalFilter,
useSortBy
);
useEffect(() => {
setHiddenColumns([]);
if (isExtraSmallScreen) {
setHiddenColumns(['Icon']);
}
}, [setHiddenColumns, isExtraSmallScreen]);
return ( return (
<div> <PageContent
<Table> isLoading={loading}
<TableHead> header={
<TableRow> <PageHeader
<TableCell className={styles.hideXS}></TableCell> title="Project roles"
<TableCell>Project Role</TableCell> actions={
<TableCell className={styles.hideSM}> <>
Description <TableSearch
</TableCell> initialValue={globalFilter}
<TableCell align="right"> onChange={setGlobalFilter}
{hasAccess(ADMIN) ? 'Action' : ''} />
</TableCell> <PageHeader.Divider />
</TableRow> <Button
</TableHead> variant="contained"
<TableBody>{renderRoles()}</TableBody> color="primary"
<PaginateUI onClick={() =>
pages={pages} navigate('/admin/create-project-role')
pageIndex={pageIndex} }
setPageIndex={setPageIndex} >
nextPage={nextPage} New project role
prevPage={prevPage} </Button>
</>
}
/> />
</Table> }
<br /> >
<SearchHighlightProvider value={globalFilter}>
<Table {...getTableProps()}>
<SortableTableHeader headerGroups={headerGroups} />
<TableBody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row);
return (
<TableRow hover {...row.getRowProps()}>
{row.cells.map(cell => (
<TableCell {...cell.getCellProps()}>
{cell.render('Cell')}
</TableCell>
))}
</TableRow>
);
})}
</TableBody>
</Table>
</SearchHighlightProvider>
<ConditionallyRender
condition={rows.length === 0}
show={
<ConditionallyRender
condition={globalFilter?.length > 0}
show={
<TablePlaceholder>
No project roles found matching &ldquo;
{globalFilter}
&rdquo;
</TablePlaceholder>
}
elseShow={
<TablePlaceholder>
No project roles available. Get started by
adding one.
</TablePlaceholder>
}
/>
}
/>
<ProjectRoleDeleteConfirm <ProjectRoleDeleteConfirm
// @ts-expect-error role={currentRole!}
role={currentRole}
open={delDialog} open={delDialog}
setDeldialogue={setDelDialog} setDeldialogue={setDelDialog}
handleDeleteRole={deleteProjectRole} handleDeleteRole={deleteProjectRole}
confirmName={confirmName} confirmName={confirmName}
setConfirmName={setConfirmName} setConfirmName={setConfirmName}
/> />
</div> </PageContent>
); );
}; };

View File

@ -1,61 +1,22 @@
import { Button } from '@mui/material';
import { useContext } from 'react'; import { useContext } from 'react';
import { useNavigate } from 'react-router-dom';
import AccessContext from 'contexts/AccessContext'; import AccessContext from 'contexts/AccessContext';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { PageContent } from 'component/common/PageContent/PageContent';
import { ADMIN } from 'component/providers/AccessProvider/permissions'; import { ADMIN } from 'component/providers/AccessProvider/permissions';
import AdminMenu from 'component/admin/menu/AdminMenu'; import AdminMenu from 'component/admin/menu/AdminMenu';
import { useStyles } from './ProjectRoles.styles';
import ProjectRoleList from './ProjectRoleList/ProjectRoleList'; import ProjectRoleList from './ProjectRoleList/ProjectRoleList';
import { AdminAlert } from 'component/common/AdminAlert/AdminAlert'; import { AdminAlert } from 'component/common/AdminAlert/AdminAlert';
const ProjectRoles = () => { const ProjectRoles = () => {
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
const { classes: styles } = useStyles();
const navigate = useNavigate();
return ( return (
<div> <div>
<AdminMenu /> <AdminMenu />
<PageContent <ConditionallyRender
bodyClass={styles.rolesListBody} condition={hasAccess(ADMIN)}
header={ show={<ProjectRoleList />}
<PageHeader elseShow={<AdminAlert />}
title="Project Roles" />
actions={
<ConditionallyRender
condition={hasAccess(ADMIN)}
show={
<Button
variant="contained"
color="primary"
onClick={() =>
navigate(
'/admin/create-project-role'
)
}
>
New Project role
</Button>
}
elseShow={
<small>
PS! Only admins can add/remove roles.
</small>
}
/>
}
/>
}
>
<ConditionallyRender
condition={hasAccess(ADMIN)}
show={<ProjectRoleList />}
elseShow={<AdminAlert />}
/>
</PageContent>
</div> </div>
); );
}; };

View File

@ -0,0 +1,29 @@
import { VFC } from 'react';
import { Box } from '@mui/material';
import { Highlighter } from 'component/common/Highlighter/Highlighter';
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
interface IHighlightCellProps {
value?: string | null;
children?: string | null;
}
export const HighlightCell: VFC<IHighlightCellProps> = ({
value,
children,
}) => {
const { searchQuery } = useSearchHighlightContext();
const text = children ?? value;
if (!text) {
return <Box sx={{ py: 1.5, px: 2 }} />;
}
return (
<Box sx={{ py: 1.5, px: 2 }}>
<span data-loading role="tooltip">
<Highlighter search={searchQuery}>{text}</Highlighter>
</span>
</Box>
);
};

View File

@ -138,7 +138,14 @@ export const EnvironmentActionCell = ({
<ConditionallyRender <ConditionallyRender
condition={updatePermission} condition={updatePermission}
show={ show={
<Tooltip title="Edit environment" arrow> <Tooltip
title={
environment.protected
? 'You cannot edit environment'
: 'Edit environment'
}
arrow
>
<span id={editId}> <span id={editId}>
<IconButton <IconButton
aria-describedby={editId} aria-describedby={editId}
@ -159,7 +166,14 @@ export const EnvironmentActionCell = ({
<ConditionallyRender <ConditionallyRender
condition={hasAccess(DELETE_ENVIRONMENT)} condition={hasAccess(DELETE_ENVIRONMENT)}
show={ show={
<Tooltip title="Delete environment" arrow> <Tooltip
title={
environment.protected
? 'You cannot delete environment'
: 'Delete environment'
}
arrow
>
<span id={deleteId}> <span id={deleteId}>
<IconButton <IconButton
aria-describedby={deleteId} aria-describedby={deleteId}

View File

@ -2,6 +2,8 @@ import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { IEnvironment } from 'interfaces/environments'; import { IEnvironment } from 'interfaces/environments';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { StatusBadge } from 'component/common/StatusBadge/StatusBadge'; import { StatusBadge } from 'component/common/StatusBadge/StatusBadge';
import { Highlighter } from 'component/common/Highlighter/Highlighter';
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
interface IEnvironmentNameCellProps { interface IEnvironmentNameCellProps {
environment: IEnvironment; environment: IEnvironment;
@ -10,9 +12,11 @@ interface IEnvironmentNameCellProps {
export const EnvironmentNameCell = ({ export const EnvironmentNameCell = ({
environment, environment,
}: IEnvironmentNameCellProps) => { }: IEnvironmentNameCellProps) => {
const { searchQuery } = useSearchHighlightContext();
return ( return (
<TextCell> <TextCell>
{environment.name} <Highlighter search={searchQuery}>{environment.name}</Highlighter>
<ConditionallyRender <ConditionallyRender
condition={!environment.enabled} condition={!environment.enabled}
show={<StatusBadge severity="warning">Disabled</StatusBadge>} show={<StatusBadge severity="warning">Disabled</StatusBadge>}