Skip to content

Commit cf8ee79

Browse files
fixed homepage layout when quick-access is on/visible (#2346)
* fixed homepage layout when quick-access is on/visible Signed-off-by: rohitratannagar <rohitratannagar2003@gmail.com> * adding changeset Signed-off-by: rohitratannagar <rohitratannagar2003@gmail.com> * made empty-cards identical Signed-off-by: rohitratannagar <rohitratannagar2003@gmail.com> * Add useContainerQuery hook and GridItem utility; update EntitySection, OnboardingSection, and TemplateSection to use container queries instead of viewport media queries. Signed-off-by: rohitratannagar <rohitratannagar2003@gmail.com> * fixed the styling of empty-cards Signed-off-by: rohitratannagar <rohitratannagar2003@gmail.com> * pascal naming & refactor entity section Signed-off-by: rohitratannagar <rohitratannagar2003@gmail.com> --------- Signed-off-by: rohitratannagar <rohitratannagar2003@gmail.com> Co-authored-by: Eswaraiah Sapram <esapram@redhat.com>
1 parent f3f900e commit cf8ee79

9 files changed

Lines changed: 431 additions & 138 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-dynamic-home-page': patch
3+
---
4+
5+
Enhance home page layout adaptability when QuickStart is displayed.
6+
7+
Updated the homepage layout logic to utilize container width monitoring rather than relying on viewport-based Grid breakpoints. This change ensures the illustration card seamlessly switches to a vertical stack when the QuickStart drawer is open, independent of the user's screen resolution.
8+
9+
Additionally, introduced scrollbars to the onboarding, software catalog, and template sections for improved navigation and usability.

workspaces/homepage/plugins/dynamic-home-page/src/components/EntitySection/EntityCard.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,18 @@ const EntityCard: FC<EntityCardProps> = ({
4646
<Card
4747
elevation={0}
4848
sx={{
49+
height: '100%',
4950
border: theme => `1px solid ${theme.palette.grey[400]}`,
5051
overflow: 'auto',
51-
maxHeight: '100%',
52+
display: 'flex',
53+
flexDirection: 'column',
5254
}}
5355
>
5456
<CardContent
5557
sx={{
58+
flex: 1,
59+
display: 'flex',
60+
flexDirection: 'column',
5661
pb: 2,
5762
'&:last-child': {
5863
pb: 2,

workspaces/homepage/plugins/dynamic-home-page/src/components/EntitySection/EntitySection.test.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ import { ThemeProvider, createTheme } from '@mui/material/styles';
2222
import { EntitySection } from './EntitySection';
2323
import { useEntities } from '../../hooks/useEntities';
2424

25+
// jsdom does not provide ResizeObserver; required by useContainerQuery in EntitySection
26+
class ResizeObserverMock {
27+
observe = jest.fn();
28+
disconnect = jest.fn();
29+
unobserve = jest.fn();
30+
}
31+
window.ResizeObserver = ResizeObserverMock as unknown as typeof ResizeObserver;
32+
2533
jest.mock('../../hooks/useEntities');
2634
const mockUseEntities = useEntities as jest.MockedFunction<typeof useEntities>;
2735

@@ -72,6 +80,10 @@ jest.mock('../../utils/utils', () => ({
7280

7381
jest.mock('../../images/homepage-entities-1.svg', () => 'mock-image.svg');
7482

83+
jest.mock('../../hooks/useContainerQuery', () => ({
84+
useContainerQuery: () => 'lg',
85+
}));
86+
7587
const theme = createTheme();
7688

7789
const renderComponent = () =>

workspaces/homepage/plugins/dynamic-home-page/src/components/EntitySection/EntitySection.tsx

Lines changed: 156 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616
import type { ReactNode } from 'react';
1717

18-
import { useState, useEffect, Fragment } from 'react';
18+
import { useState, useEffect, Fragment, useRef, useCallback } from 'react';
1919

2020
import {
2121
CodeSnippet,
@@ -34,7 +34,6 @@ import CloseIcon from '@mui/icons-material/Close';
3434
import CircularProgress from '@mui/material/CircularProgress';
3535
import CardContent from '@mui/material/CardContent';
3636
import { useTheme, styled } from '@mui/material/styles';
37-
import useMediaQuery from '@mui/material/useMediaQuery';
3837

3938
import EntityCard from './EntityCard';
4039
import { ViewMoreLink } from './ViewMoreLink';
@@ -46,6 +45,11 @@ import {
4645
} from '../../utils/utils';
4746
import { useTranslation } from '../../hooks/useTranslation';
4847
import { Trans } from '../Trans';
48+
import { containerGridItemSx } from '../../utils/GridItem';
49+
import {
50+
useContainerQuery,
51+
type ContainerSize,
52+
} from '../../hooks/useContainerQuery';
4953

5054
const StyledLink = styled(BackstageLink)(({ theme }) => ({
5155
textDecoration: 'none',
@@ -63,31 +67,44 @@ export const EntitySection = () => {
6367
const [isRemoveFirstCard, setIsRemoveFirstCard] = useState(false);
6468
const [showDiscoveryCard, setShowDiscoveryCard] = useState(true);
6569
const [imgLoaded, setImgLoaded] = useState(false);
66-
const [isMediumBreakpoint, setIsMediumBreakpoint] = useState(false);
6770

68-
const isMd = useMediaQuery(theme.breakpoints.only('md'));
71+
const containerRef = useRef<HTMLDivElement>(null);
72+
const containerSize = useContainerQuery(containerRef);
6973

70-
useEffect(() => {
71-
if (isMd) {
72-
setIsMediumBreakpoint(true);
73-
} else {
74-
setIsMediumBreakpoint(false);
75-
}
76-
}, [isMd]);
74+
const entityCardCount =
75+
containerSize === 'xs' || containerSize === 'sm' ? 2 : 4;
76+
77+
const illustrationWidthMap: Partial<Record<ContainerSize, number>> = {
78+
md: 180,
79+
lg: 220,
80+
xl: 266,
81+
};
82+
const illustrationWidth = illustrationWidthMap[containerSize] ?? 266;
83+
84+
const visibleEntitiesCount = (() => {
85+
const isWide =
86+
containerSize === 'xl' ||
87+
containerSize === 'lg' ||
88+
containerSize === 'md';
89+
90+
if (!isWide) return entityCardCount;
91+
92+
return isRemoveFirstCard ? entityCardCount : entityCardCount - 2;
93+
})();
7794

7895
useEffect(() => {
7996
const isUserDismissedEntityIllustration =
8097
hasEntityIllustrationUserDismissed(displayName);
8198
setIsRemoveFirstCard(isUserDismissedEntityIllustration);
8299
}, [displayName]);
83100

84-
const handleClose = () => {
101+
const handleClose = useCallback(() => {
85102
setShowDiscoveryCard(false);
86103
setTimeout(() => {
87104
addDismissedEntityIllustrationUsers(displayName);
88105
setIsRemoveFirstCard(true);
89106
}, 500);
90-
};
107+
}, [displayName]);
91108

92109
const { data, error, isLoading } = useEntities({
93110
kind: ['Component', 'API', 'Resource', 'System'],
@@ -120,102 +137,121 @@ export const EntitySection = () => {
120137
</WarningPanel>
121138
);
122139
} else {
123-
let entityCardCount = 2;
124-
if (isMediumBreakpoint) entityCardCount = 3;
125-
126140
content = (
127141
<Box sx={{ padding: '8px 8px 8px 0' }}>
128142
<Fragment>
129143
<Grid container spacing={1} alignItems="stretch">
130-
{!isRemoveFirstCard && !profileLoading && (
131-
<Grid item xs={12} md={6} lg={5} key="entities illustration">
132-
<Card
133-
elevation={0}
134-
sx={{
135-
border: `1px solid ${theme.palette.grey[400]}`,
136-
display: 'flex',
137-
flexDirection: 'row',
138-
alignItems: 'center',
139-
position: 'relative',
140-
transition:
141-
'opacity 0.5s ease-out, transform 0.5s ease-in-out',
142-
opacity: showDiscoveryCard ? 1 : 0,
143-
transform: showDiscoveryCard
144-
? 'translateX(0)'
145-
: 'translateX(-50px)',
146-
}}
147-
>
148-
{!imgLoaded && (
149-
<Skeleton
150-
variant="rectangular"
151-
height={300}
152-
sx={{
153-
borderRadius: 3,
154-
width: 'clamp(140px, 14vw, 266px)',
155-
}}
156-
/>
157-
)}
158-
<Box
159-
component="img"
160-
src={HomePageEntityIllustration}
161-
onLoad={() => setImgLoaded(true)}
162-
alt=""
163-
height={300}
144+
{/* hiding discovery card on small containers */}
145+
{!isRemoveFirstCard &&
146+
!profileLoading &&
147+
containerSize !== 'xs' &&
148+
containerSize !== 'sm' && (
149+
<Grid item sx={containerGridItemSx({ md: 4 })}>
150+
<Card
151+
elevation={0}
164152
sx={{
165-
width: 'clamp(140px, 14vw, 266px)',
153+
height: '100%',
154+
border: `1px solid ${theme.palette.grey[400]}`,
155+
display: 'flex',
156+
flexDirection: 'row',
157+
alignItems: 'center',
158+
position: 'relative',
159+
transition:
160+
'opacity 0.5s ease-out, transform 0.5s ease-in-out',
161+
opacity: showDiscoveryCard ? 1 : 0,
162+
transform: showDiscoveryCard
163+
? 'translateX(0)'
164+
: 'translateX(-50px)',
166165
}}
167-
/>
168-
<Box sx={{ p: 2 }}>
169-
<Box>
170-
<Typography variant="body2" paragraph>
171-
{t('entities.description')}
172-
</Typography>
173-
</Box>
174-
{entities?.length > 0 && (
175-
<IconButton
176-
onClick={handleClose}
177-
aria-label={t('entities.close')}
178-
style={{
179-
position: 'absolute',
180-
top: '8px',
181-
right: '8px',
166+
>
167+
<Box
168+
sx={{
169+
display: 'flex',
170+
flexDirection: 'row',
171+
alignItems: 'center',
172+
}}
173+
>
174+
{!imgLoaded && (
175+
<Skeleton
176+
variant="rectangular"
177+
height={300}
178+
sx={{
179+
borderRadius: 3,
180+
width: illustrationWidth,
181+
}}
182+
/>
183+
)}
184+
<Box
185+
component="img"
186+
src={HomePageEntityIllustration}
187+
onLoad={() => setImgLoaded(true)}
188+
alt=""
189+
height={300}
190+
sx={{
191+
width: illustrationWidth,
182192
}}
183-
>
184-
<CloseIcon style={{ width: '16px', height: '16px' }} />
185-
</IconButton>
186-
)}
187-
</Box>
188-
</Card>
189-
</Grid>
190-
)}
191-
{entities
192-
?.slice(0, isRemoveFirstCard ? 4 : entityCardCount)
193-
.map((item: any) => (
194-
<Grid
195-
item
196-
xs={12}
197-
md={6}
198-
lg={isRemoveFirstCard ? 3 : 3.5}
199-
key={item.metadata.name}
200-
>
201-
<EntityCard
202-
entity={item}
203-
title={item.metadata.title ?? item.metadata.name}
204-
version="latest"
205-
description={item.metadata.description ?? ''}
206-
tags={item.metadata?.tags ?? []}
207-
kind={item.kind}
208-
/>
193+
/>
194+
<Box sx={{ p: 2 }}>
195+
<Box sx={{ p: 2 }}>
196+
<Typography variant="body2" paragraph>
197+
{t('entities.description')}
198+
</Typography>
199+
</Box>
200+
{entities?.length > 0 && (
201+
<IconButton
202+
onClick={handleClose}
203+
aria-label={t('entities.close')}
204+
style={{
205+
position: 'absolute',
206+
top: '8px',
207+
right: '8px',
208+
}}
209+
>
210+
<CloseIcon
211+
style={{ width: '16px', height: '16px' }}
212+
/>
213+
</IconButton>
214+
)}
215+
</Box>
216+
</Box>
217+
</Card>
209218
</Grid>
210-
))}
219+
)}
220+
{entities?.slice(0, visibleEntitiesCount).map((item: any) => (
221+
<Grid
222+
item
223+
sx={containerGridItemSx({
224+
xs: 12,
225+
sm: 6,
226+
md: isRemoveFirstCard ? 3 : 4,
227+
})}
228+
key={item.metadata.name}
229+
>
230+
<EntityCard
231+
entity={item}
232+
title={item.metadata.title ?? item.metadata.name}
233+
version="latest"
234+
description={item.metadata.description ?? ''}
235+
tags={item.metadata?.tags ?? []}
236+
kind={item.kind}
237+
/>
238+
</Grid>
239+
))}
211240
{entities?.length === 0 && (
212-
<Grid item md={isRemoveFirstCard ? 12 : 7}>
241+
<Grid
242+
item
243+
sx={containerGridItemSx({
244+
sm: 12,
245+
md: isRemoveFirstCard ? 12 : 8,
246+
})}
247+
>
213248
<Box
214249
sx={{
250+
height: '100%',
251+
minHeight: 300,
215252
display: 'flex',
216253
alignItems: 'center',
217254
justifyContent: 'center',
218-
minHeight: 300,
219255
border: muiTheme =>
220256
`1px solid ${muiTheme.palette.grey[400]}`,
221257
borderRadius: 3,
@@ -255,7 +291,9 @@ export const EntitySection = () => {
255291
sx={{
256292
padding: '24px',
257293
border: muitheme => `1px solid ${muitheme.palette.grey[300]}`,
258-
overflow: 'auto',
294+
containerType: 'inline-size',
295+
display: 'flex',
296+
flexDirection: 'column',
259297
}}
260298
>
261299
<Typography
@@ -265,21 +303,32 @@ export const EntitySection = () => {
265303
alignItems: 'center',
266304
fontWeight: '500',
267305
fontSize: '1.5rem',
306+
flexShrink: 0,
268307
}}
269308
>
270309
{t('entities.title')}
271310
</Typography>
272-
{content}
273-
{entities?.length > 0 && (
274-
<Box sx={{ pt: 2 }}>
275-
<ViewMoreLink to="/catalog">
276-
<Trans
277-
message="entities.viewAll"
278-
params={{ count: data?.totalItems?.toString() || '' }}
279-
/>
280-
</ViewMoreLink>
281-
</Box>
282-
)}
311+
<Box
312+
ref={containerRef}
313+
sx={{
314+
flex: 1,
315+
minHeight: 0,
316+
overflowY: 'auto',
317+
mt: 1,
318+
}}
319+
>
320+
{content}
321+
{entities?.length > 0 && (
322+
<Box sx={{ pt: 2 }}>
323+
<ViewMoreLink to="/catalog">
324+
<Trans
325+
message="entities.viewAll"
326+
params={{ count: data?.totalItems?.toString() || '' }}
327+
/>
328+
</ViewMoreLink>
329+
</Box>
330+
)}
331+
</Box>
283332
</Card>
284333
);
285334
};

0 commit comments

Comments
 (0)