mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-02 01:17:58 +02:00
feat: implement better roles sub-tabs (#4009)
https://linear.app/unleash/issue/2-1145/improve-roles-sub-tabs Improves UI/UX of the roles sub-tabs. Some of the logic is a bit specific due to the feature flag, will be nice to clean this up once we remove it. Before:  After: 
This commit is contained in:
parent
02600880d1
commit
3a27f2a4bd
@ -1,49 +1,86 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext, useState } from 'react';
|
||||||
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 { ADMIN } from 'component/providers/AccessProvider/permissions';
|
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||||
import { RolesTable } from './RolesTable/RolesTable';
|
import { RolesTable } from './RolesTable/RolesTable';
|
||||||
import { AdminAlert } from 'component/common/AdminAlert/AdminAlert';
|
import { AdminAlert } from 'component/common/AdminAlert/AdminAlert';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import { Tab, Tabs, styled } from '@mui/material';
|
import { Tab, Tabs, styled, useMediaQuery } from '@mui/material';
|
||||||
import { Route, Routes, useLocation } from 'react-router-dom';
|
import { Route, Routes, useLocation } from 'react-router-dom';
|
||||||
import { CenteredNavLink } from '../menu/CenteredNavLink';
|
import { CenteredNavLink } from '../menu/CenteredNavLink';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { PROJECT_ROLE_TYPE } from '@server/util/constants';
|
import { PROJECT_ROLE_TYPE, ROOT_ROLE_TYPE } from '@server/util/constants';
|
||||||
|
import { useRoles } from 'hooks/api/getters/useRoles/useRoles';
|
||||||
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
import theme from 'themes/theme';
|
||||||
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
|
import { Add } from '@mui/icons-material';
|
||||||
|
import { UPDATE_ROLE } from '@server/types/permissions';
|
||||||
|
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
|
||||||
|
import IRole from 'interfaces/role';
|
||||||
|
|
||||||
const StyledPageContent = styled(PageContent)(({ theme }) => ({
|
const StyledPageContent = styled(PageContent)(({ theme }) => ({
|
||||||
'.page-header': {
|
'& .page-header': {
|
||||||
padding: 0,
|
padding: theme.spacing(0, 4),
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const tabs = [
|
const StyledHeader = styled('div')(({ theme }) => ({
|
||||||
{
|
display: 'flex',
|
||||||
label: 'Root',
|
justifyContent: 'space-between',
|
||||||
path: '/admin/roles',
|
alignItems: 'center',
|
||||||
},
|
}));
|
||||||
{
|
|
||||||
label: 'Project',
|
const StyledTabsContainer = styled('div')({
|
||||||
path: '/admin/roles/project-roles',
|
flex: 1,
|
||||||
},
|
});
|
||||||
];
|
|
||||||
|
const StyledActions = styled('div')({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
});
|
||||||
|
|
||||||
export const Roles = () => {
|
export const Roles = () => {
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
if (!uiConfig.flags.customRootRoles) {
|
const { roles, projectRoles, loading } = useRoles();
|
||||||
return (
|
|
||||||
<div>
|
const [searchValue, setSearchValue] = useState('');
|
||||||
<ConditionallyRender
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
condition={hasAccess(ADMIN)}
|
const [selectedRole, setSelectedRole] = useState<IRole>();
|
||||||
show={<RolesTable type={PROJECT_ROLE_TYPE} />}
|
|
||||||
elseShow={<AdminAlert />}
|
const tabs = uiConfig.flags.customRootRoles
|
||||||
/>
|
? [
|
||||||
</div>
|
{
|
||||||
);
|
label: 'Root roles',
|
||||||
}
|
path: '/admin/roles',
|
||||||
|
total: roles.length,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Project roles',
|
||||||
|
path: '/admin/roles/project-roles',
|
||||||
|
total: projectRoles.length,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
label: 'Project roles',
|
||||||
|
path: '/admin/roles',
|
||||||
|
total: projectRoles.length,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
|
||||||
|
const type =
|
||||||
|
!uiConfig.flags.customRootRoles || pathname.includes('project-roles')
|
||||||
|
? PROJECT_ROLE_TYPE
|
||||||
|
: ROOT_ROLE_TYPE;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -53,36 +90,111 @@ export const Roles = () => {
|
|||||||
<StyledPageContent
|
<StyledPageContent
|
||||||
headerClass="page-header"
|
headerClass="page-header"
|
||||||
bodyClass="page-body"
|
bodyClass="page-body"
|
||||||
|
isLoading={loading}
|
||||||
header={
|
header={
|
||||||
<Tabs
|
<>
|
||||||
value={pathname}
|
<StyledHeader>
|
||||||
indicatorColor="primary"
|
<StyledTabsContainer>
|
||||||
textColor="primary"
|
<Tabs
|
||||||
variant="scrollable"
|
value={pathname}
|
||||||
allowScrollButtonsMobile
|
indicatorColor="primary"
|
||||||
>
|
textColor="primary"
|
||||||
{tabs.map(({ label, path }) => (
|
variant="scrollable"
|
||||||
<Tab
|
allowScrollButtonsMobile
|
||||||
key={label}
|
>
|
||||||
value={path}
|
{tabs.map(
|
||||||
label={
|
({ label, path, total }) => (
|
||||||
<CenteredNavLink to={path}>
|
<Tab
|
||||||
<span>{label}</span>
|
key={label}
|
||||||
</CenteredNavLink>
|
value={path}
|
||||||
}
|
label={
|
||||||
/>
|
<CenteredNavLink
|
||||||
))}
|
to={path}
|
||||||
</Tabs>
|
>
|
||||||
|
<span>
|
||||||
|
{label} (
|
||||||
|
{total})
|
||||||
|
</span>
|
||||||
|
</CenteredNavLink>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Tabs>
|
||||||
|
</StyledTabsContainer>
|
||||||
|
<StyledActions>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={!isSmallScreen}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<Search
|
||||||
|
initialValue={
|
||||||
|
searchValue
|
||||||
|
}
|
||||||
|
onChange={
|
||||||
|
setSearchValue
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<PageHeader.Divider />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ResponsiveButton
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedRole(undefined);
|
||||||
|
setModalOpen(true);
|
||||||
|
}}
|
||||||
|
maxWidth={`${theme.breakpoints.values['sm']}px`}
|
||||||
|
Icon={Add}
|
||||||
|
permission={UPDATE_ROLE}
|
||||||
|
>
|
||||||
|
New {type} role
|
||||||
|
</ResponsiveButton>
|
||||||
|
</StyledActions>
|
||||||
|
</StyledHeader>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={isSmallScreen}
|
||||||
|
show={
|
||||||
|
<Search
|
||||||
|
initialValue={searchValue}
|
||||||
|
onChange={setSearchValue}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
path="project-roles"
|
path="project-roles"
|
||||||
element={
|
element={
|
||||||
<RolesTable type={PROJECT_ROLE_TYPE} />
|
<RolesTable
|
||||||
|
type={PROJECT_ROLE_TYPE}
|
||||||
|
searchValue={searchValue}
|
||||||
|
modalOpen={modalOpen}
|
||||||
|
setModalOpen={setModalOpen}
|
||||||
|
selectedRole={selectedRole}
|
||||||
|
setSelectedRole={setSelectedRole}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="*"
|
||||||
|
element={
|
||||||
|
<RolesTable
|
||||||
|
type={
|
||||||
|
uiConfig.flags.customRootRoles
|
||||||
|
? ROOT_ROLE_TYPE
|
||||||
|
: PROJECT_ROLE_TYPE
|
||||||
|
}
|
||||||
|
searchValue={searchValue}
|
||||||
|
modalOpen={modalOpen}
|
||||||
|
setModalOpen={setModalOpen}
|
||||||
|
selectedRole={selectedRole}
|
||||||
|
setSelectedRole={setSelectedRole}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route path="*" element={<RolesTable />} />
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</StyledPageContent>
|
</StyledPageContent>
|
||||||
}
|
}
|
||||||
|
@ -5,14 +5,12 @@ import IRole, { PredefinedRoleType } from 'interfaces/role';
|
|||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { useMediaQuery } from '@mui/material';
|
||||||
import { Button, useMediaQuery } from '@mui/material';
|
|
||||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
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 { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||||
import theme from 'themes/theme';
|
import theme from 'themes/theme';
|
||||||
import { Search } from 'component/common/Search/Search';
|
|
||||||
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
||||||
import { useSearch } from 'hooks/useSearch';
|
import { useSearch } from 'hooks/useSearch';
|
||||||
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
|
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
|
||||||
@ -28,18 +26,27 @@ import { ROOT_ROLE_TYPE } from '@server/util/constants';
|
|||||||
|
|
||||||
interface IRolesTableProps {
|
interface IRolesTableProps {
|
||||||
type?: PredefinedRoleType;
|
type?: PredefinedRoleType;
|
||||||
|
searchValue?: string;
|
||||||
|
modalOpen: boolean;
|
||||||
|
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
selectedRole?: IRole;
|
||||||
|
setSelectedRole: React.Dispatch<React.SetStateAction<IRole | undefined>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RolesTable = ({ type = ROOT_ROLE_TYPE }: IRolesTableProps) => {
|
export const RolesTable = ({
|
||||||
|
type = ROOT_ROLE_TYPE,
|
||||||
|
searchValue = '',
|
||||||
|
modalOpen,
|
||||||
|
setModalOpen,
|
||||||
|
selectedRole,
|
||||||
|
setSelectedRole,
|
||||||
|
}: IRolesTableProps) => {
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
|
||||||
const { roles, projectRoles, refetch, loading } = useRoles();
|
const { roles, projectRoles, refetch, loading } = useRoles();
|
||||||
const { removeRole } = useRolesApi();
|
const { removeRole } = useRolesApi();
|
||||||
|
|
||||||
const [searchValue, setSearchValue] = useState('');
|
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
|
||||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
const [selectedRole, setSelectedRole] = useState<IRole>();
|
|
||||||
|
|
||||||
const onDeleteConfirm = async (role: IRole) => {
|
const onDeleteConfirm = async (role: IRole) => {
|
||||||
try {
|
try {
|
||||||
@ -154,53 +161,8 @@ export const RolesTable = ({ type = ROOT_ROLE_TYPE }: IRolesTableProps) => {
|
|||||||
columns
|
columns
|
||||||
);
|
);
|
||||||
|
|
||||||
const titledCaseType = type[0].toUpperCase() + type.slice(1);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent
|
<PageContent isLoading={loading}>
|
||||||
isLoading={loading}
|
|
||||||
header={
|
|
||||||
<PageHeader
|
|
||||||
title={`${titledCaseType} roles (${rows.length})`}
|
|
||||||
actions={
|
|
||||||
<>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={!isSmallScreen}
|
|
||||||
show={
|
|
||||||
<>
|
|
||||||
<Search
|
|
||||||
initialValue={searchValue}
|
|
||||||
onChange={setSearchValue}
|
|
||||||
/>
|
|
||||||
<PageHeader.Divider />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedRole(undefined);
|
|
||||||
setModalOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
New {type} role
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={isSmallScreen}
|
|
||||||
show={
|
|
||||||
<Search
|
|
||||||
initialValue={searchValue}
|
|
||||||
onChange={setSearchValue}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</PageHeader>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SearchHighlightProvider value={getSearchText(searchValue)}>
|
<SearchHighlightProvider value={getSearchText(searchValue)}>
|
||||||
<VirtualizedTable
|
<VirtualizedTable
|
||||||
rows={rows}
|
rows={rows}
|
||||||
|
Loading…
Reference in New Issue
Block a user