Skip to content

Commit 531b6b2

Browse files
feat: [UIE-8731] - Roles Table and Users Table ui fix
1 parent b23ebe6 commit 531b6b2

File tree

7 files changed

+111
-108
lines changed

7 files changed

+111
-108
lines changed

packages/manager/src/features/IAM/Roles/Roles.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CircleProgress, Paper } from '@linode/ui';
1+
import { CircleProgress, Paper, Typography } from '@linode/ui';
22
import React from 'react';
33

44
import { RolesTable } from 'src/features/IAM/Roles/RolesTable/RolesTable';
@@ -22,6 +22,7 @@ export const RolesLanding = () => {
2222

2323
return (
2424
<Paper sx={(theme) => ({ marginTop: theme.tokens.spacing.S16 })}>
25+
<Typography variant="h2">Roles</Typography>
2526
<RolesTable roles={roles} />
2627
</Paper>
2728
);

packages/manager/src/features/IAM/Roles/RolesTable/RolesTable.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,10 @@ export const RolesTable = ({ roles }: Props) => {
125125
container
126126
direction="row"
127127
spacing={2}
128-
sx={{ justifyContent: 'space-between' }}
128+
sx={(theme) => ({
129+
justifyContent: 'space-between',
130+
marginBottom: theme.tokens.spacing.S12,
131+
})}
129132
>
130133
<Grid
131134
container
@@ -212,7 +215,9 @@ export const RolesTable = ({ roles }: Props) => {
212215
<TableBody>
213216
{!rows?.length ? (
214217
<TableRow>
215-
<TableCell>No items to display.</TableCell>
218+
<TableCell style={{ justifyContent: 'center' }}>
219+
No items to display.
220+
</TableCell>
216221
</TableRow>
217222
) : (
218223
rows.map((roleRow) => (
@@ -225,7 +230,9 @@ export const RolesTable = ({ roles }: Props) => {
225230
selectable
226231
selected={selectedRows.includes(roleRow)}
227232
>
228-
<TableCell style={{ minWidth: '26%' }}>
233+
<TableCell
234+
style={{ minWidth: '26%', wordBreak: 'break-word' }}
235+
>
229236
{roleRow.name}
230237
</TableCell>
231238
<TableCell style={{ minWidth: '14%' }}>
@@ -242,7 +249,12 @@ export const RolesTable = ({ roles }: Props) => {
242249
</Typography>
243250
)}
244251
</TableCell>
245-
<TableCell style={{ minWidth: '10%' }}>
252+
<TableCell
253+
style={{
254+
minWidth: '10%',
255+
justifyContent: 'flex-end',
256+
}}
257+
>
246258
<RolesTableActionMenu
247259
onClick={() => {
248260
assignRoleRow(roleRow);

packages/manager/src/features/IAM/Roles/RolesTable/RolesTableActionMenu.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,11 @@ interface Props {
88

99
export const RolesTableActionMenu = ({ onClick }: Props) => {
1010
// This menu has evolved over time to where it isn't much of a menu at all, but rather a single action.
11-
return <InlineMenuAction actionText={'Assign Role'} onClick={onClick} />;
11+
return (
12+
<InlineMenuAction
13+
actionText={'Assign Role'}
14+
buttonHeight={40}
15+
onClick={onClick}
16+
/>
17+
);
1218
};

packages/manager/src/features/IAM/Shared/Permissions/Permissions.tsx

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { StyledLinkButton, TooltipIcon, Typography } from '@linode/ui';
2-
import { debounce } from '@mui/material';
3-
import { Grid } from '@mui/material';
2+
import Grid from '@mui/material/Grid';
43
import * as React from 'react';
54

65
import { useCalculateHiddenItems } from '../../hooks/useCalculateHiddenItems';
@@ -21,27 +20,10 @@ type Props = {
2120
};
2221

2322
export const Permissions = ({ permissions }: Props) => {
24-
const [showAll, setShowAll] = React.useState(false);
23+
const { containerRef, itemRefs, visibleIndexes, showAll, setShowAll } =
24+
useCalculateHiddenItems(permissions);
2525

26-
const { calculateHiddenItems, containerRef, itemRefs, numHiddenItems } =
27-
useCalculateHiddenItems(permissions, showAll);
28-
29-
const handleResize = React.useMemo(
30-
() => debounce(() => calculateHiddenItems(), 100),
31-
[calculateHiddenItems]
32-
);
33-
34-
React.useEffect(() => {
35-
// Ensure calculateHiddenItems runs after layout stabilization on initial render
36-
const rafId = requestAnimationFrame(() => calculateHiddenItems());
37-
38-
window.addEventListener('resize', handleResize);
39-
40-
return () => {
41-
cancelAnimationFrame(rafId);
42-
window.removeEventListener('resize', handleResize);
43-
};
44-
}, [calculateHiddenItems, handleResize]);
26+
const numHiddenItems = permissions.length - visibleIndexes.length;
4527

4628
// TODO: update the link for TooltipIcon when it's ready - UIE-8534
4729
return (
@@ -73,7 +55,9 @@ export const Permissions = ({ permissions }: Props) => {
7355
<StyledPermissionItem
7456
data-testid="permission"
7557
key={permission}
76-
ref={(el: HTMLSpanElement) => (itemRefs.current[index] = el)}
58+
ref={(el) =>
59+
(itemRefs.current[index] = el as HTMLDivElement | null)
60+
}
7761
>
7862
{permission}
7963
</StyledPermissionItem>
@@ -86,7 +70,7 @@ export const Permissions = ({ permissions }: Props) => {
8670
onClick={() => setShowAll(!showAll)}
8771
type="button"
8872
>
89-
{showAll ? 'Hide' : ` Expand (+${numHiddenItems})`}
73+
{showAll ? 'Hide' : `Expand (+${numHiddenItems})`}
9074
</StyledLinkButton>
9175
</StyledBox>
9276
)}

packages/manager/src/features/IAM/Users/UserRoles/AssignedEntities.tsx

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Box, Button, Chip, CloseIcon, Tooltip } from '@linode/ui';
2-
import { debounce, useTheme } from '@mui/material';
2+
import { useTheme } from '@mui/material';
33
import * as React from 'react';
44

55
import { useCalculateHiddenItems } from '../../hooks/useCalculateHiddenItems';
@@ -20,26 +20,11 @@ export const AssignedEntities = ({
2020
}: Props) => {
2121
const theme = useTheme();
2222

23-
const { calculateHiddenItems, containerRef, itemRefs, numHiddenItems } =
24-
useCalculateHiddenItems(role.entity_names!);
25-
26-
const handleResize = React.useMemo(
27-
() => debounce(() => calculateHiddenItems(), 100),
28-
[calculateHiddenItems]
23+
const { containerRef, itemRefs, visibleIndexes } = useCalculateHiddenItems(
24+
role.entity_names!
2925
);
3026

31-
React.useEffect(() => {
32-
// Ensure calculateHiddenItems runs after layout stabilization on initial render
33-
const rafId = requestAnimationFrame(() => calculateHiddenItems());
34-
35-
window.addEventListener('resize', handleResize);
36-
37-
return () => {
38-
cancelAnimationFrame(rafId);
39-
window.removeEventListener('resize', handleResize);
40-
};
41-
}, [calculateHiddenItems, handleResize]);
42-
27+
const hiddenCount = role.entity_names!.length - visibleIndexes.length;
4328
const combinedEntities: CombinedEntity[] = React.useMemo(
4429
() =>
4530
role.entity_names!.map((name, index) => ({
@@ -98,7 +83,7 @@ export const AssignedEntities = ({
9883
>
9984
{items}
10085
</div>
101-
{numHiddenItems > 0 && (
86+
{hiddenCount > 0 && (
10287
<Box
10388
sx={{
10489
alignItems: 'center',
@@ -122,7 +107,7 @@ export const AssignedEntities = ({
122107
padding: 0,
123108
}}
124109
>
125-
+{numHiddenItems}
110+
+{hiddenCount}
126111
</Button>
127112
</Tooltip>
128113
</Box>

packages/manager/src/features/IAM/Users/UsersTable/Users.tsx

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useAccountUsers, useProfile } from '@linode/queries';
22
import { getAPIFilterFromQuery } from '@linode/search';
3-
import { Box, Button, Paper, Typography } from '@linode/ui';
3+
import { Button, Paper, Stack, Typography } from '@linode/ui';
44
import { useMediaQuery } from '@mui/material';
55
import { useTheme } from '@mui/material/styles';
66
import React from 'react';
@@ -21,8 +21,6 @@ import { UsersLandingTableHead } from './UsersLandingTableHead';
2121

2222
import type { Filter } from '@linode/api-v4';
2323

24-
const XS_TO_SM_BREAKPOINT = 475;
25-
2624
export const UsersLanding = () => {
2725
const [isCreateDrawerOpen, setIsCreateDrawerOpen] =
2826
React.useState<boolean>(false);
@@ -101,24 +99,18 @@ export const UsersLanding = () => {
10199
order={order}
102100
/>
103101
)}
104-
<Paper sx={(theme) => ({ marginTop: theme.spacing(2) })}>
105-
<Box
106-
sx={(theme) => ({
107-
alignItems: 'center',
108-
display: 'flex',
109-
justifyContent: 'space-between',
110-
marginBottom: theme.spacing(2),
111-
[theme.breakpoints.down(XS_TO_SM_BREAKPOINT)]: {
112-
alignItems: 'flex-start',
113-
flexDirection: 'column',
114-
},
115-
})}
102+
<Paper sx={(theme) => ({ marginTop: theme.tokens.spacing.S16 })}>
103+
<Stack
104+
direction={isSmDown ? 'column' : 'row'}
105+
justifyContent="space-between"
106+
marginBottom={2}
107+
spacing={2}
116108
>
117109
{isProxyUser ? (
118110
<Typography
119111
sx={(theme) => ({
120112
[theme.breakpoints.down('md')]: {
121-
marginLeft: theme.spacing(1),
113+
marginLeft: theme.tokens.spacing.S8,
122114
},
123115
})}
124116
variant="h3"
@@ -130,7 +122,7 @@ export const UsersLanding = () => {
130122
clearable
131123
containerProps={{
132124
sx: {
133-
width: { md: '320px', xs: '100%' },
125+
width: '320px',
134126
},
135127
}}
136128
debounceTime={250}
@@ -147,12 +139,9 @@ export const UsersLanding = () => {
147139
buttonType="primary"
148140
disabled={isRestrictedUser}
149141
onClick={() => setIsCreateDrawerOpen(true)}
150-
sx={(theme) => ({
151-
[theme.breakpoints.down(XS_TO_SM_BREAKPOINT)]: {
152-
marginTop: theme.spacing(1),
153-
width: '100%',
154-
},
155-
})}
142+
sx={{
143+
maxWidth: '120px',
144+
}}
156145
tooltipText={
157146
isRestrictedUser
158147
? 'You cannot create other users as a restricted user.'
@@ -161,7 +150,7 @@ export const UsersLanding = () => {
161150
>
162151
Add a User
163152
</Button>
164-
</Box>
153+
</Stack>
165154
<Table aria-label="List of Users" sx={{ tableLayout: 'fixed' }}>
166155
<UsersLandingTableHead order={order} />
167156
<TableBody>
Lines changed: 59 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,75 @@
1-
import React from 'react';
1+
import type React from 'react';
2+
import { useEffect, useRef, useState } from 'react';
23

34
import type { PermissionType } from '@linode/api-v4';
45

56
/**
67
* Custom hook to calculate hidden items
78
*/
8-
export const useCalculateHiddenItems = (
9-
items: PermissionType[] | string[],
10-
showAll?: boolean
11-
) => {
12-
const [numHiddenItems, setNumHiddenItems] = React.useState<number>(0);
13-
14-
const containerRef = React.useRef<HTMLDivElement | null>(null);
15-
16-
const itemRefs = React.useRef<(HTMLDivElement | HTMLSpanElement)[]>([]);
179

18-
const calculateHiddenItems = React.useCallback(() => {
19-
if (showAll || !containerRef.current) {
20-
setNumHiddenItems(0);
21-
return;
22-
}
23-
24-
if (!itemRefs.current) {
25-
return;
26-
}
10+
type Props = {
11+
containerRef: React.RefObject<HTMLDivElement>;
12+
itemRefs: React.MutableRefObject<(HTMLDivElement | null)[]>;
13+
setShowAll: (value: boolean) => void;
14+
showAll: boolean;
15+
visibleIndexes: number[];
16+
};
2717

28-
const containerBottom = containerRef.current.getBoundingClientRect().bottom;
18+
export const useCalculateHiddenItems = (
19+
items: PermissionType[] | string[]
20+
): Props => {
21+
const containerRef = useRef<HTMLDivElement>(null);
22+
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
23+
const [visibleIndexes, setVisibleIndexes] = useState<number[]>([]);
24+
const [showAll, setShowAll] = useState(false);
2925

30-
const itemsArray = Array.from(itemRefs.current);
26+
useEffect(() => {
27+
if (!containerRef.current || items.length === 0) return;
3128

32-
const firstHiddenIndex = itemsArray.findIndex(
33-
(item: HTMLDivElement | HTMLSpanElement) => {
34-
if (!item) {
35-
return false;
29+
const observer = new IntersectionObserver(
30+
(entries) => {
31+
if (showAll) {
32+
setVisibleIndexes(items.map((_, i) => i));
33+
return;
3634
}
37-
const rect = item.getBoundingClientRect();
38-
return rect.top >= containerBottom;
35+
const visible = entries
36+
.filter(
37+
(entry) => entry.isIntersecting && entry.intersectionRatio >= 1
38+
)
39+
.map((entry) => Number(entry.target.getAttribute('data-index')));
40+
41+
setVisibleIndexes(visible);
42+
},
43+
{
44+
root: containerRef.current,
45+
threshold: 1.0,
3946
}
4047
);
41-
42-
const numHiddenItems =
43-
firstHiddenIndex !== -1 ? itemsArray.length - firstHiddenIndex : 0;
44-
45-
setNumHiddenItems(numHiddenItems);
48+
// observe all items
49+
itemRefs.current.forEach((el) => {
50+
if (el) observer.observe(el);
51+
});
52+
// force re-observe on resize
53+
const handleResize = () => {
54+
itemRefs.current.forEach((el) => {
55+
if (el) {
56+
observer.unobserve(el);
57+
observer.observe(el);
58+
}
59+
});
60+
};
61+
window.addEventListener('resize', handleResize);
62+
return () => {
63+
observer.disconnect();
64+
window.removeEventListener('resize', handleResize);
65+
};
4666
}, [items, showAll]);
4767

48-
return { calculateHiddenItems, containerRef, itemRefs, numHiddenItems };
68+
return {
69+
containerRef,
70+
itemRefs,
71+
showAll,
72+
setShowAll,
73+
visibleIndexes,
74+
};
4975
};

0 commit comments

Comments
 (0)