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:
parent
9ac962da45
commit
b61980e71b
@ -79,8 +79,6 @@ export const AddonList = () => {
|
||||
condition={addons.length > 0}
|
||||
show={<ConfiguredAddons />}
|
||||
/>
|
||||
|
||||
<br />
|
||||
<AvailableAddons loading={loading} providers={providers} />
|
||||
</>
|
||||
);
|
||||
|
@ -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} />
|
||||
|
@ -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>
|
||||
}
|
||||
/>
|
||||
|
@ -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 “
|
||||
{globalFilter}
|
||||
”
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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}
|
||||
|
@ -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>}
|
||||
|
Loading…
Reference in New Issue
Block a user