Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions workspaces/homepage/.changeset/red-insects-teach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@red-hat-developer-hub/backstage-plugin-dynamic-home-page': patch
---

Enhance home page layout adaptability when QuickStart is displayed.

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.

Additionally, introduced scrollbars to the onboarding, software catalog, and template sections for improved navigation and usability.
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,18 @@ const EntityCard: FC<EntityCardProps> = ({
<Card
elevation={0}
sx={{
height: '100%',
border: theme => `1px solid ${theme.palette.grey[400]}`,
overflow: 'auto',
maxHeight: '100%',
display: 'flex',
flexDirection: 'column',
}}
>
<CardContent
sx={{
flex: 1,
display: 'flex',
flexDirection: 'column',
pb: 2,
'&:last-child': {
pb: 2,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ import { ThemeProvider, createTheme } from '@mui/material/styles';
import { EntitySection } from './EntitySection';
import { useEntities } from '../../hooks/useEntities';

// jsdom does not provide ResizeObserver; required by useContainerQuery in EntitySection
class ResizeObserverMock {
observe = jest.fn();
disconnect = jest.fn();
unobserve = jest.fn();
}
window.ResizeObserver = ResizeObserverMock as unknown as typeof ResizeObserver;

jest.mock('../../hooks/useEntities');
const mockUseEntities = useEntities as jest.MockedFunction<typeof useEntities>;

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

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

jest.mock('../../hooks/useContainerQuery', () => ({
useContainerQuery: () => 'lg',
}));

const theme = createTheme();

const renderComponent = () =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/
import type { ReactNode } from 'react';

import { useState, useEffect, Fragment } from 'react';
import { useState, useEffect, Fragment, useRef } from 'react';

import {
CodeSnippet,
Expand All @@ -34,7 +34,6 @@ import CloseIcon from '@mui/icons-material/Close';
import CircularProgress from '@mui/material/CircularProgress';
import CardContent from '@mui/material/CardContent';
import { useTheme, styled } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';

import EntityCard from './EntityCard';
import { ViewMoreLink } from './ViewMoreLink';
Expand All @@ -46,6 +45,8 @@ import {
} from '../../utils/utils';
import { useTranslation } from '../../hooks/useTranslation';
import { Trans } from '../Trans';
import { containerGridItemSx } from '../../utils/GridItem';
import { useContainerQuery } from '../../hooks/useContainerQuery';

const StyledLink = styled(BackstageLink)(({ theme }) => ({
textDecoration: 'none',
Expand All @@ -63,17 +64,19 @@ export const EntitySection = () => {
const [isRemoveFirstCard, setIsRemoveFirstCard] = useState(false);
const [showDiscoveryCard, setShowDiscoveryCard] = useState(true);
const [imgLoaded, setImgLoaded] = useState(false);
const [isMediumBreakpoint, setIsMediumBreakpoint] = useState(false);

const isMd = useMediaQuery(theme.breakpoints.only('md'));
const containerRef = useRef<HTMLDivElement>(null);
const containerSize = useContainerQuery(containerRef);

useEffect(() => {
if (isMd) {
setIsMediumBreakpoint(true);
} else {
setIsMediumBreakpoint(false);
}
}, [isMd]);
const entityCardCount =
containerSize === 'xs' || containerSize === 'sm' ? 2 : 4;

const getIllustrationWidth = () => {
if (containerSize === 'md') return 180;
if (containerSize === 'lg') return 220;
return 266;
};
const illustrationWidth = getIllustrationWidth();

useEffect(() => {
const isUserDismissedEntityIllustration =
Expand Down Expand Up @@ -120,82 +123,109 @@ export const EntitySection = () => {
</WarningPanel>
);
} else {
let entityCardCount = 2;
if (isMediumBreakpoint) entityCardCount = 3;

content = (
<Box sx={{ padding: '8px 8px 8px 0' }}>
<Fragment>
<Grid container spacing={1} alignItems="stretch">
{!isRemoveFirstCard && !profileLoading && (
<Grid item xs={12} md={6} lg={5} key="entities illustration">
<Card
elevation={0}
sx={{
border: `1px solid ${theme.palette.grey[400]}`,
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
position: 'relative',
transition:
'opacity 0.5s ease-out, transform 0.5s ease-in-out',
opacity: showDiscoveryCard ? 1 : 0,
transform: showDiscoveryCard
? 'translateX(0)'
: 'translateX(-50px)',
}}
>
{!imgLoaded && (
<Skeleton
variant="rectangular"
height={300}
sx={{
borderRadius: 3,
width: 'clamp(140px, 14vw, 266px)',
}}
/>
)}
<Box
component="img"
src={HomePageEntityIllustration}
onLoad={() => setImgLoaded(true)}
alt=""
height={300}
{/* hiding discovery card on small containers */}
{!isRemoveFirstCard &&
!profileLoading &&
containerSize !== 'xs' &&
containerSize !== 'sm' && (
<Grid item sx={containerGridItemSx({ md: 4 })}>
<Card
elevation={0}
sx={{
width: 'clamp(140px, 14vw, 266px)',
height: '100%',
border: `1px solid ${theme.palette.grey[400]}`,
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
position: 'relative',
transition:
'opacity 0.5s ease-out, transform 0.5s ease-in-out',
opacity: showDiscoveryCard ? 1 : 0,
transform: showDiscoveryCard
? 'translateX(0)'
: 'translateX(-50px)',
}}
/>
<Box sx={{ p: 2 }}>
<Box>
<Typography variant="body2" paragraph>
{t('entities.description')}
</Typography>
</Box>
{entities?.length > 0 && (
<IconButton
onClick={handleClose}
aria-label={t('entities.close')}
style={{
position: 'absolute',
top: '8px',
right: '8px',
>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
}}
>
{!imgLoaded && (
<Skeleton
variant="rectangular"
height={300}
sx={{
borderRadius: 3,
width: illustrationWidth,
}}
/>
)}
<Box
component="img"
src={HomePageEntityIllustration}
onLoad={() => setImgLoaded(true)}
alt=""
height={300}
sx={{
width: illustrationWidth,
}}
>
<CloseIcon style={{ width: '16px', height: '16px' }} />
</IconButton>
)}
</Box>
</Card>
</Grid>
)}
/>
<Box sx={{ p: 2 }}>
<Box sx={{ p: 2 }}>
<Typography variant="body2" paragraph>
{t('entities.description')}
</Typography>
</Box>
{entities?.length > 0 && (
<IconButton
onClick={handleClose}
aria-label={t('entities.close')}
style={{
position: 'absolute',
top: '8px',
right: '8px',
}}
>
<CloseIcon
style={{ width: '16px', height: '16px' }}
/>
</IconButton>
)}
</Box>
</Box>
</Card>
</Grid>
)}
{entities
?.slice(0, isRemoveFirstCard ? 4 : entityCardCount)
?.slice(
0,
(() => {
const isWide =
containerSize === 'xl' ||
containerSize === 'lg' ||
containerSize === 'md';
if (!isWide) return entityCardCount;
return isRemoveFirstCard
? entityCardCount
: entityCardCount - 2;
})(),
)

.map((item: any) => (
<Grid
item
xs={12}
md={6}
lg={isRemoveFirstCard ? 3 : 3.5}
sx={containerGridItemSx({
xs: 12,
sm: 6,
md: isRemoveFirstCard ? 3 : 4,
})}
key={item.metadata.name}
>
<EntityCard
Expand All @@ -209,13 +239,21 @@ export const EntitySection = () => {
</Grid>
))}
{entities?.length === 0 && (
<Grid item md={isRemoveFirstCard ? 12 : 7}>
<Grid
item
sx={containerGridItemSx({
sm: isRemoveFirstCard ? 12 : 6,
md: isRemoveFirstCard ? 12 : 8,
lg: 6,
})}
>
<Box
sx={{
height: '100%',
minHeight: 300,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: 300,
border: muiTheme =>
`1px solid ${muiTheme.palette.grey[400]}`,
borderRadius: 3,
Expand Down Expand Up @@ -255,7 +293,9 @@ export const EntitySection = () => {
sx={{
padding: '24px',
border: muitheme => `1px solid ${muitheme.palette.grey[300]}`,
overflow: 'auto',
containerType: 'inline-size',
display: 'flex',
flexDirection: 'column',
}}
>
<Typography
Expand All @@ -265,21 +305,32 @@ export const EntitySection = () => {
alignItems: 'center',
fontWeight: '500',
fontSize: '1.5rem',
flexShrink: 0,
}}
>
{t('entities.title')}
</Typography>
{content}
{entities?.length > 0 && (
<Box sx={{ pt: 2 }}>
<ViewMoreLink to="/catalog">
<Trans
message="entities.viewAll"
params={{ count: data?.totalItems?.toString() || '' }}
/>
</ViewMoreLink>
</Box>
)}
<Box
ref={containerRef}
sx={{
flex: 1,
minHeight: 0,
overflowY: 'auto',
mt: 1,
}}
>
{content}
{entities?.length > 0 && (
<Box sx={{ pt: 2 }}>
<ViewMoreLink to="/catalog">
<Trans
message="entities.viewAll"
params={{ count: data?.totalItems?.toString() || '' }}
/>
</ViewMoreLink>
</Box>
)}
</Box>
</Card>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const OnboardingCard: FC<OnboardingCardProps> = ({
<Box>
<CardContent sx={{ backgroundColor: 'transparent' }}>
<Typography
variant="h3"
sx={{
fontSize: '1.75rem',
fontWeight: 500,
Expand Down Expand Up @@ -77,6 +78,8 @@ const OnboardingCard: FC<OnboardingCardProps> = ({
target={target}
aria-label={ariaLabel}
sx={{
width: 220,
minWidth: 220,
padding: theme => theme.spacing(1, 1.5),
fontSize: '16px',
'& .v5-MuiButton-endIcon': {
Expand Down
Loading