From 8e67594f1b46f85c33b73e3fc1ee3deb0a968211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Fri, 14 Mar 2025 10:21:14 +0000 Subject: [PATCH] chore: change access overview to lists in accordions (#9535) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../users/AccessOverview/AccessOverview.tsx | 39 +++++---- .../AccessOverviewAccordion.tsx | 78 ++++++++++++++++++ .../AccessOverviewList.tsx | 73 +++++++++++++++++ .../AccessOverview/AccessOverviewTable.tsx | 81 ------------------- 4 files changed, 174 insertions(+), 97 deletions(-) create mode 100644 frontend/src/component/admin/users/AccessOverview/AccessOverviewAccordion/AccessOverviewAccordion.tsx create mode 100644 frontend/src/component/admin/users/AccessOverview/AccessOverviewAccordion/AccessOverviewList.tsx delete mode 100644 frontend/src/component/admin/users/AccessOverview/AccessOverviewTable.tsx diff --git a/frontend/src/component/admin/users/AccessOverview/AccessOverview.tsx b/frontend/src/component/admin/users/AccessOverview/AccessOverview.tsx index f012ffab32..3c217348e5 100644 --- a/frontend/src/component/admin/users/AccessOverview/AccessOverview.tsx +++ b/frontend/src/component/admin/users/AccessOverview/AccessOverview.tsx @@ -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 = () => { } > - - Root permissions for role {rootRole?.name} - - - - Project permissions for project {project} with project roles [ - {projectRoles?.map((role: any) => role.name).join(', ')}] - - - - Environment permissions for environment {environment} - - + + + Root permissions for role {rootRole?.name} + + + Project permissions + {project + ? ` for project ${project}${projectRoles?.length ? ` with project role${projectRoles.length !== 1 ? 's' : ''} ${projectRoles?.map((role: any) => role.name).join(', ')}` : ''}` + : ''} + + {environment && ( + + Environment permissions for {environment} + + )} + ); }; diff --git a/frontend/src/component/admin/users/AccessOverview/AccessOverviewAccordion/AccessOverviewAccordion.tsx b/frontend/src/component/admin/users/AccessOverview/AccessOverviewAccordion/AccessOverviewAccordion.tsx new file mode 100644 index 0000000000..72d51e9e6a --- /dev/null +++ b/frontend/src/component/admin/users/AccessOverview/AccessOverviewAccordion/AccessOverviewAccordion.tsx @@ -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) => ( + + }> + + {children} + + + { + permissions.filter(({ hasPermission }) => hasPermission) + .length + } + /{permissions.length} permissions + + + + + + +); diff --git a/frontend/src/component/admin/users/AccessOverview/AccessOverviewAccordion/AccessOverviewList.tsx b/frontend/src/component/admin/users/AccessOverview/AccessOverviewAccordion/AccessOverviewList.tsx new file mode 100644 index 0000000000..26a63cb4c5 --- /dev/null +++ b/frontend/src/component/admin/users/AccessOverview/AccessOverviewAccordion/AccessOverviewList.tsx @@ -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 ( + + + {permissions.map((permission) => ( +
  • +
    {permission.displayName}
    + +
  • + ))} +
    +
    + ); +}; + +const PermissionStatus = ({ hasPermission }: { hasPermission: boolean }) => ( + + {hasPermission ? ( + <> + + Has permission + + ) : ( + <> + + No permission + + )} + +); diff --git a/frontend/src/component/admin/users/AccessOverview/AccessOverviewTable.tsx b/frontend/src/component/admin/users/AccessOverview/AccessOverviewTable.tsx deleted file mode 100644 index 4bbc8ca95b..0000000000 --- a/frontend/src/component/admin/users/AccessOverview/AccessOverviewTable.tsx +++ /dev/null @@ -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 }) => ( - - ) : ( - - ) - } - /> - ), - }, - ], - [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(null); - - return ( - - - No permissions found. - } - /> - - ); -};