Skip to content

Commit

Permalink
feat: implement better roles sub-tabs (#4009)
Browse files Browse the repository at this point in the history
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:

![image](https://github.com/Unleash/unleash/assets/14320932/cc277920-557c-45a9-a560-6026167dab1d)

After:

![image](https://github.com/Unleash/unleash/assets/14320932/51d1b5b3-068a-4bf5-84ca-24fdf708f899)
  • Loading branch information
nunogois committed Jun 19, 2023
1 parent 0260088 commit 3a27f2a
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 100 deletions.
206 changes: 159 additions & 47 deletions frontend/src/component/admin/roles/Roles.tsx
@@ -1,49 +1,86 @@
import { useContext } from 'react';
import { useContext, useState } from 'react';
import AccessContext from 'contexts/AccessContext';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { ADMIN } from 'component/providers/AccessProvider/permissions';
import { RolesTable } from './RolesTable/RolesTable';
import { AdminAlert } from 'component/common/AdminAlert/AdminAlert';
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 { CenteredNavLink } from '../menu/CenteredNavLink';
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 }) => ({
'.page-header': {
padding: 0,
'& .page-header': {
padding: theme.spacing(0, 4),
[theme.breakpoints.down('md')]: {
padding: theme.spacing(1),
},
},
}));

const tabs = [
{
label: 'Root',
path: '/admin/roles',
},
{
label: 'Project',
path: '/admin/roles/project-roles',
},
];
const StyledHeader = styled('div')(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}));

const StyledTabsContainer = styled('div')({
flex: 1,
});

const StyledActions = styled('div')({
display: 'flex',
alignItems: 'center',
});

export const Roles = () => {
const { uiConfig } = useUiConfig();
const { hasAccess } = useContext(AccessContext);
const { pathname } = useLocation();

if (!uiConfig.flags.customRootRoles) {
return (
<div>
<ConditionallyRender
condition={hasAccess(ADMIN)}
show={<RolesTable type={PROJECT_ROLE_TYPE} />}
elseShow={<AdminAlert />}
/>
</div>
);
}
const { roles, projectRoles, loading } = useRoles();

const [searchValue, setSearchValue] = useState('');
const [modalOpen, setModalOpen] = useState(false);
const [selectedRole, setSelectedRole] = useState<IRole>();

const tabs = uiConfig.flags.customRootRoles
? [
{
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 (
<div>
Expand All @@ -53,36 +90,111 @@ export const Roles = () => {
<StyledPageContent
headerClass="page-header"
bodyClass="page-body"
isLoading={loading}
header={
<Tabs
value={pathname}
indicatorColor="primary"
textColor="primary"
variant="scrollable"
allowScrollButtonsMobile
>
{tabs.map(({ label, path }) => (
<Tab
key={label}
value={path}
label={
<CenteredNavLink to={path}>
<span>{label}</span>
</CenteredNavLink>
}
/>
))}
</Tabs>
<>
<StyledHeader>
<StyledTabsContainer>
<Tabs
value={pathname}
indicatorColor="primary"
textColor="primary"
variant="scrollable"
allowScrollButtonsMobile
>
{tabs.map(
({ label, path, total }) => (
<Tab
key={label}
value={path}
label={
<CenteredNavLink
to={path}
>
<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>
<Route
path="project-roles"
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>
</StyledPageContent>
}
Expand Down
68 changes: 15 additions & 53 deletions frontend/src/component/admin/roles/RolesTable/RolesTable.tsx
Expand Up @@ -5,14 +5,12 @@ import IRole, { PredefinedRoleType } from 'interfaces/role';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { Button, useMediaQuery } from '@mui/material';
import { useMediaQuery } from '@mui/material';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { useFlexLayout, useSortBy, useTable } from 'react-table';
import { sortTypes } from 'utils/sortTypes';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import theme from 'themes/theme';
import { Search } from 'component/common/Search/Search';
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
import { useSearch } from 'hooks/useSearch';
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
Expand All @@ -28,18 +26,27 @@ import { ROOT_ROLE_TYPE } from '@server/util/constants';

interface IRolesTableProps {
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 { roles, projectRoles, refetch, loading } = useRoles();
const { removeRole } = useRolesApi();

const [searchValue, setSearchValue] = useState('');
const [modalOpen, setModalOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
const [selectedRole, setSelectedRole] = useState<IRole>();

const onDeleteConfirm = async (role: IRole) => {
try {
Expand Down Expand Up @@ -154,53 +161,8 @@ export const RolesTable = ({ type = ROOT_ROLE_TYPE }: IRolesTableProps) => {
columns
);

const titledCaseType = type[0].toUpperCase() + type.slice(1);

return (
<PageContent
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>
}
>
<PageContent isLoading={loading}>
<SearchHighlightProvider value={getSearchText(searchValue)}>
<VirtualizedTable
rows={rows}
Expand Down

0 comments on commit 3a27f2a

Please sign in to comment.