1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-03-18 00:19:49 +01:00

chore: change access overview to lists in accordions (#9535)

https://linear.app/unleash/issue/2-3343/accordions-not-a-must-have

https://linear.app/unleash/issue/2-3345/indicator-of-how-many-permissions

Changes our Access Overview from tables to lists in accordions. Also
includes the total permissions in the accordion summary.

Looking at the designs it seems like lists would make the most sense,
both visually and in terms of semantics. This will also allow us to
group the permissions both visually and semantically in a future task.


![image](https://github.com/user-attachments/assets/0692b4f3-0fc5-482c-b963-c731bf5113f5)

### Update

Also improved our project permissions label.


![image](https://github.com/user-attachments/assets/cbb2c298-1f85-4a78-b3ff-3140c567f756)


![image](https://github.com/user-attachments/assets/f3d5c623-4013-4a47-a4b1-5af2e63cb01e)

---------

Co-authored-by: Gastón Fournier <gaston@getunleash.io>
This commit is contained in:
Nuno Góis 2025-03-14 10:21:14 +00:00 committed by GitHub
parent 872162eb7c
commit 8e67594f1b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 174 additions and 97 deletions

View File

@ -2,7 +2,6 @@ import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import useUserInfo from 'hooks/api/getters/useUserInfo/useUserInfo';
import { AccessOverviewTable } from './AccessOverviewTable';
import { styled, useMediaQuery } from '@mui/material';
import { useEffect, useState } from 'react';
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
@ -12,6 +11,7 @@ import { StringParam, useQueryParams } from 'use-query-params';
import useProjects from 'hooks/api/getters/useProjects/useProjects';
import { AccessOverviewSelect } from './AccessOverviewSelect';
import { useUserAccessOverview } from 'hooks/api/getters/useUserAccessOverview/useUserAccessOverview';
import { AccessOverviewAccordion } from './AccessOverviewAccordion/AccessOverviewAccordion';
const StyledActionsContainer = styled('div')(({ theme }) => ({
display: 'flex',
@ -24,8 +24,10 @@ const StyledActionsContainer = styled('div')(({ theme }) => ({
},
}));
const StyledTitle = styled('h2')(({ theme }) => ({
margin: theme.spacing(2, 0),
const StyledAccessOverviewContainer = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
}));
export const AccessOverview = () => {
@ -104,19 +106,24 @@ export const AccessOverview = () => {
</PageHeader>
}
>
<StyledTitle>
Root permissions for role {rootRole?.name}
</StyledTitle>
<AccessOverviewTable permissions={overview?.root ?? []} />
<StyledTitle>
Project permissions for project {project} with project roles [
{projectRoles?.map((role: any) => role.name).join(', ')}]
</StyledTitle>
<AccessOverviewTable permissions={overview?.project ?? []} />
<StyledTitle>
Environment permissions for environment {environment}
</StyledTitle>
<AccessOverviewTable permissions={overview?.environment ?? []} />
<StyledAccessOverviewContainer>
<AccessOverviewAccordion permissions={overview?.root ?? []}>
Root permissions for role {rootRole?.name}
</AccessOverviewAccordion>
<AccessOverviewAccordion permissions={overview?.project ?? []}>
Project permissions
{project
? ` for project ${project}${projectRoles?.length ? ` with project role${projectRoles.length !== 1 ? 's' : ''} ${projectRoles?.map((role: any) => role.name).join(', ')}` : ''}`
: ''}
</AccessOverviewAccordion>
{environment && (
<AccessOverviewAccordion
permissions={overview?.environment ?? []}
>
Environment permissions for {environment}
</AccessOverviewAccordion>
)}
</StyledAccessOverviewContainer>
</PageContent>
);
};

View File

@ -0,0 +1,78 @@
import ExpandMore from '@mui/icons-material/ExpandMore';
import {
Accordion,
AccordionDetails,
AccordionSummary,
styled,
} from '@mui/material';
import type { IAccessOverviewPermission } from 'interfaces/permissions';
import { AccessOverviewList } from './AccessOverviewList';
const StyledAccordion = styled(Accordion)(({ theme }) => ({
border: `1px solid ${theme.palette.divider}`,
borderRadius: theme.shape.borderRadiusLarge,
overflow: 'hidden',
boxShadow: 'none',
margin: 0,
'&:before': {
display: 'none',
},
}));
const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
backgroundColor: theme.palette.background.elevation1,
'& .MuiAccordionSummary-content': {
justifyContent: 'space-between',
alignItems: 'center',
minHeight: '30px',
},
}));
const StyledTitleContainer = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'start',
flexDirection: 'column',
gap: theme.spacing(0.5),
}));
const StyledTitle = styled('span')(({ theme }) => ({
fontWeight: theme.fontWeight.bold,
}));
const StyledSecondaryLabel = styled('span')(({ theme }) => ({
color: theme.palette.text.secondary,
fontSize: theme.fontSizes.smallBody,
marginRight: theme.spacing(1),
}));
const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({
padding: 0,
}));
interface IAccessAccordionProps {
permissions: IAccessOverviewPermission[];
children: React.ReactNode;
}
export const AccessOverviewAccordion = ({
permissions,
children,
}: IAccessAccordionProps) => (
<StyledAccordion>
<StyledAccordionSummary expandIcon={<ExpandMore />}>
<StyledTitleContainer>
<StyledTitle>{children}</StyledTitle>
</StyledTitleContainer>
<StyledSecondaryLabel>
{
permissions.filter(({ hasPermission }) => hasPermission)
.length
}
/{permissions.length} permissions
</StyledSecondaryLabel>
</StyledAccordionSummary>
<StyledAccordionDetails>
<AccessOverviewList permissions={permissions} />
</StyledAccordionDetails>
</StyledAccordion>
);

View File

@ -0,0 +1,73 @@
import Check from '@mui/icons-material/Check';
import Close from '@mui/icons-material/Close';
import { Box, styled } from '@mui/material';
import type { IAccessOverviewPermission } from 'interfaces/permissions';
const StyledList = styled('ul')(({ theme }) => ({
listStyle: 'none',
padding: 0,
margin: 0,
fontSize: theme.fontSizes.smallBody,
'& li': {
display: 'flex',
justifyContent: 'space-between',
padding: theme.spacing(2),
'&:not(:last-child)': {
borderBottom: `1px solid ${theme.palette.divider}`,
},
},
}));
const StyledPermissionStatus = styled('div', {
shouldForwardProp: (prop) => prop !== 'hasPermission',
})<{ hasPermission: boolean }>(({ theme, hasPermission }) => ({
display: 'flex',
gap: theme.spacing(1),
alignItems: 'center',
width: theme.spacing(17.5),
color: hasPermission
? theme.palette.text.primary
: theme.palette.text.secondary,
'& > svg': {
color: hasPermission
? theme.palette.success.main
: theme.palette.error.main,
},
}));
export const AccessOverviewList = ({
permissions,
}: {
permissions: IAccessOverviewPermission[];
}) => {
return (
<Box sx={{ maxHeight: 500, overflow: 'auto' }}>
<StyledList>
{permissions.map((permission) => (
<li key={permission.name}>
<div>{permission.displayName}</div>
<PermissionStatus
hasPermission={permission.hasPermission}
/>
</li>
))}
</StyledList>
</Box>
);
};
const PermissionStatus = ({ hasPermission }: { hasPermission: boolean }) => (
<StyledPermissionStatus hasPermission={hasPermission}>
{hasPermission ? (
<>
<Check />
Has permission
</>
) : (
<>
<Close />
No permission
</>
)}
</StyledPermissionStatus>
);

View File

@ -1,81 +0,0 @@
import { useMemo, useRef } from 'react';
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useFlexLayout, useSortBy, useTable } from 'react-table';
import { sortTypes } from 'utils/sortTypes';
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
import Check from '@mui/icons-material/Check';
import Close from '@mui/icons-material/Close';
import { Box } from '@mui/material';
import type { IAccessOverviewPermission } from 'interfaces/permissions';
export const AccessOverviewTable = ({
permissions,
}: {
permissions: IAccessOverviewPermission[];
}) => {
const columns = useMemo(
() => [
{
Header: 'Permission',
accessor: 'name',
minWidth: 100,
},
{
Header: 'Description',
accessor: 'displayName',
minWidth: 180,
},
{
Header: 'Has permission',
accessor: 'hasPermission',
Cell: ({ value }: { value: boolean }) => (
<IconCell
icon={
value ? (
<Check color='success' />
) : (
<Close color='error' />
)
}
/>
),
},
],
[permissions],
);
const initialState = {
sortBy: [{ id: 'name', desc: true }],
};
const { headerGroups, rows, prepareRow } = useTable(
{
columns: columns as any,
data: permissions ?? [],
initialState,
sortTypes,
},
useSortBy,
useFlexLayout,
);
const parentRef = useRef<HTMLElement | null>(null);
return (
<Box sx={{ maxHeight: 500, overflow: 'auto' }} ref={parentRef}>
<VirtualizedTable
rows={rows}
headerGroups={headerGroups}
prepareRow={prepareRow}
parentRef={parentRef}
/>
<ConditionallyRender
condition={rows.length === 0}
show={
<TablePlaceholder>No permissions found.</TablePlaceholder>
}
/>
</Box>
);
};