1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01: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}
show={<ConfiguredAddons />}
/>
<br />
<AvailableAddons loading={loading} providers={providers} />
</>
);

View File

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

View File

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

View File

@ -1,37 +1,47 @@
import { useContext, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import {
Table,
SortableTableHeader,
TableBody,
TableCell,
TableHead,
TableRow,
} from '@mui/material';
import AccessContext from 'contexts/AccessContext';
import usePagination from 'hooks/usePagination';
TablePlaceholder,
TableSearch,
} from 'component/common/Table';
import { useTable, useGlobalFilter, useSortBy } from 'react-table';
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 IRole, { IProjectRole } from 'interfaces/role';
import useProjectRolesApi from 'hooks/api/actions/useProjectRolesApi/useProjectRolesApi';
import useToast from 'hooks/useToast';
import ProjectRoleDeleteConfirm from '../ProjectRoleDeleteConfirm/ProjectRoleDeleteConfirm';
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 BUILTIN_ROLE_TYPE = 'project';
const ProjectRoleList = () => {
const { hasAccess } = useContext(AccessContext);
const { roles } = useProjectRoles();
const { classes: styles } = useStyles();
const navigate = useNavigate();
const { roles, refetch, loading } = useProjectRoles();
const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
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 { refetch } = useProjectRoles();
const [currentRole, setCurrentRole] = useState<IProjectRole | null>(null);
const [delDialog, setDelDialog] = useState(false);
const [confirmName, setConfirmName] = useState('');
@ -54,60 +64,198 @@ const ProjectRoleList = () => {
setConfirmName('');
};
const renderRoles = () => {
return page.map((role: IProjectRole) => {
return (
<ProjectRoleListItem
key={role.id}
id={role.id}
name={role.name}
type={role.type}
description={role.description}
// @ts-expect-error
setCurrentRole={setCurrentRole}
setDelDialog={setDelDialog}
/>
);
});
};
const columns = useMemo(
() => [
{
id: 'Icon',
Cell: () => (
<IconCell
icon={<SupervisedUserCircle color="disabled" />}
/>
),
},
{
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 (
<div>
<Table>
<TableHead>
<TableRow>
<TableCell className={styles.hideXS}></TableCell>
<TableCell>Project Role</TableCell>
<TableCell className={styles.hideSM}>
Description
</TableCell>
<TableCell align="right">
{hasAccess(ADMIN) ? 'Action' : ''}
</TableCell>
</TableRow>
</TableHead>
<TableBody>{renderRoles()}</TableBody>
<PaginateUI
pages={pages}
pageIndex={pageIndex}
setPageIndex={setPageIndex}
nextPage={nextPage}
prevPage={prevPage}
<PageContent
isLoading={loading}
header={
<PageHeader
title="Project roles"
actions={
<>
<TableSearch
initialValue={globalFilter}
onChange={setGlobalFilter}
/>
<PageHeader.Divider />
<Button
variant="contained"
color="primary"
onClick={() =>
navigate('/admin/create-project-role')
}
>
New project role
</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
// @ts-expect-error
role={currentRole}
role={currentRole!}
open={delDialog}
setDeldialogue={setDelDialog}
handleDeleteRole={deleteProjectRole}
confirmName={confirmName}
setConfirmName={setConfirmName}
/>
</div>
</PageContent>
);
};

View File

@ -1,61 +1,22 @@
import { Button } from '@mui/material';
import { useContext } from 'react';
import { useNavigate } from 'react-router-dom';
import AccessContext from 'contexts/AccessContext';
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 AdminMenu from 'component/admin/menu/AdminMenu';
import { useStyles } from './ProjectRoles.styles';
import ProjectRoleList from './ProjectRoleList/ProjectRoleList';
import { AdminAlert } from 'component/common/AdminAlert/AdminAlert';
const ProjectRoles = () => {
const { hasAccess } = useContext(AccessContext);
const { classes: styles } = useStyles();
const navigate = useNavigate();
return (
<div>
<AdminMenu />
<PageContent
bodyClass={styles.rolesListBody}
header={
<PageHeader
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>
<ConditionallyRender
condition={hasAccess(ADMIN)}
show={<ProjectRoleList />}
elseShow={<AdminAlert />}
/>
</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
condition={updatePermission}
show={
<Tooltip title="Edit environment" arrow>
<Tooltip
title={
environment.protected
? 'You cannot edit environment'
: 'Edit environment'
}
arrow
>
<span id={editId}>
<IconButton
aria-describedby={editId}
@ -159,7 +166,14 @@ export const EnvironmentActionCell = ({
<ConditionallyRender
condition={hasAccess(DELETE_ENVIRONMENT)}
show={
<Tooltip title="Delete environment" arrow>
<Tooltip
title={
environment.protected
? 'You cannot delete environment'
: 'Delete environment'
}
arrow
>
<span id={deleteId}>
<IconButton
aria-describedby={deleteId}

View File

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