1
0
mirror of https://github.com/Unleash/unleash.git synced 2026-01-05 20:06:22 +01:00

feat: add splash overlay (#11147)

Adds a splash overlay that we can to App.tsx, it will look for active
splash ids defined in splash.tsx and append a query parameter that will
trigger the modal content. Once a splash is triggered it will be marked
as seen in the backend. This PR only adds the functionality, it does not
add it to the application just yet.
This commit is contained in:
Fredrik Strand Oseberg 2025-12-16 08:40:53 +01:00 committed by GitHub
parent 4c1ef6aa1a
commit ed6f728e7e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 303 additions and 1 deletions

View File

@ -0,0 +1,196 @@
import { useNavigate } from 'react-router-dom';
import {
Box,
Button,
IconButton,
styled,
Tooltip,
Typography,
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import ArticleIcon from '@mui/icons-material/Article';
import { ReactComponent as UnleashLogo } from 'assets/img/logoDarkWithText.svg';
const YOUTUBE_VIDEO_ID = 'PLACEHOLDER_VIDEO_ID';
const DOCS_URL = 'https://docs.getunleash.io/reference/release-plans';
const RELEASE_NOTES_URL = 'https://docs.getunleash.io/release-notes';
const DialogCard = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadiusLarge,
display: 'flex',
flexDirection: 'column',
boxShadow: theme.shadows[5],
maxWidth: theme.spacing(100),
width: '100%',
maxHeight: '90vh',
overflow: 'hidden',
margin: theme.spacing(2),
position: 'relative',
zIndex: 1,
}));
const HeaderRow = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: theme.spacing(2, 3),
borderBottom: `1px solid ${theme.palette.divider}`,
}));
const StyledCloseButton = styled(IconButton)(({ theme }) => ({
padding: theme.spacing(0.5),
}));
const StyledLogo = styled(UnleashLogo)(({ theme }) => ({
height: theme.spacing(5),
}));
const ContentContainer = styled(Box)(({ theme }) => ({
padding: theme.spacing(3),
overflowY: 'auto',
flex: 1,
}));
const StyledTitle = styled(Typography)(({ theme }) => ({
fontFamily: theme.typography.fontFamily,
fontSize: theme.typography.h1.fontSize,
fontWeight: theme.typography.fontWeightLight,
color: theme.palette.text.primary,
marginBottom: theme.spacing(1),
}));
const StyledDescription = styled(Typography)(({ theme }) => ({
color: theme.palette.text.secondary,
marginBottom: theme.spacing(2),
}));
const LinksRow = styled(Box)(({ theme }) => ({
display: 'flex',
gap: theme.spacing(3),
marginBottom: theme.spacing(3),
}));
const StyledLink = styled('a')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(0.5),
color: theme.palette.primary.main,
fontWeight: theme.typography.fontWeightBold,
textDecoration: 'none',
fontSize: theme.fontSizes.smallBody,
'&:hover, &:focus': {
textDecoration: 'underline',
},
}));
const YouTubeContainer = styled(Box)(({ theme }) => ({
position: 'relative',
width: '100%',
paddingBottom: '56.25%',
marginBottom: theme.spacing(3),
borderRadius: theme.shape.borderRadiusLarge,
overflow: 'hidden',
backgroundColor: theme.palette.background.elevation1,
'& iframe': {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
border: 0,
},
}));
const ActionsRow = styled(Box)(({ theme }) => ({
display: 'flex',
justifyContent: 'flex-end',
gap: theme.spacing(2),
}));
interface ReleaseManagementSplashProps {
onClose: () => void;
}
export const ReleaseManagementSplash = ({
onClose,
}: ReleaseManagementSplashProps) => {
const navigate = useNavigate();
const handleGetStarted = () => {
onClose();
navigate('/release-templates');
};
return (
<DialogCard>
<HeaderRow>
<StyledLogo aria-label='Unleash' />
<Tooltip title='Close' arrow>
<StyledCloseButton
onClick={onClose}
size='small'
aria-label='close'
>
<CloseIcon />
</StyledCloseButton>
</Tooltip>
</HeaderRow>
<ContentContainer>
<StyledTitle variant='h1'>
Introducing release management
</StyledTitle>
<StyledDescription>
Structure your feature rollouts with release plans,
milestones, and automated progressions. Add safeguards to
protect your deployments and roll out features with
confidence.
</StyledDescription>
<LinksRow>
<StyledLink
href={DOCS_URL}
target='_blank'
rel='noopener noreferrer'
>
<OpenInNewIcon fontSize='small' />
View documentation
</StyledLink>
<StyledLink
href={RELEASE_NOTES_URL}
target='_blank'
rel='noopener noreferrer'
>
<ArticleIcon fontSize='small' />
Release notes
</StyledLink>
</LinksRow>
<YouTubeContainer>
<iframe
src={`https://www.youtube.com/embed/${YOUTUBE_VIDEO_ID}`}
title='Release management introduction video'
allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'
allowFullScreen
/>
</YouTubeContainer>
<ActionsRow>
<Button
variant='contained'
color='primary'
onClick={handleGetStarted}
>
Get started with release management
</Button>
<Button variant='text' onClick={onClose}>
Cancel
</Button>
</ActionsRow>
</ContentContainer>
</DialogCard>
);
};

View File

@ -0,0 +1,104 @@
import { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Modal, Backdrop, styled } from '@mui/material';
import Fade from '@mui/material/Fade';
import { useAuthSplash } from 'hooks/api/getters/useAuth/useAuthSplash';
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
import useSplashApi from 'hooks/api/actions/useSplashApi/useSplashApi';
import {
activeSplashIds,
splashIds,
type SplashId,
} from 'component/splash/splash';
import type { IAuthSplash } from 'hooks/api/getters/useAuth/useAuthEndpoint';
import { ReleaseManagementSplash } from './ReleaseManagementSplash';
const TRANSITION_DURATION = 250;
const StyledBackdrop = styled(Backdrop)({
backgroundColor: 'rgba(0, 0, 0, 0.5)',
});
const ModalContent = styled('div')({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
outline: 'none',
});
const hasSeenSplashId = (splashId: SplashId, splash: IAuthSplash): boolean => {
return Boolean(splash[splashId]);
};
const isKnownSplashId = (value: string): value is SplashId => {
return (splashIds as Readonly<string[]>).includes(value);
};
export const SplashOverlay = () => {
const [searchParams, setSearchParams] = useSearchParams();
const { user } = useAuthUser();
const { splash, refetchSplash } = useAuthSplash();
const { setSplashSeen } = useSplashApi();
const splashId = searchParams.get('splash');
const isKnownId = splashId ? isKnownSplashId(splashId) : false;
useEffect(() => {
if (!user || !splash || splashId) return;
if (user.isAPI) return;
const unseenSplashId = activeSplashIds.find(
(id) => !hasSeenSplashId(id, splash),
);
if (unseenSplashId) {
searchParams.set('splash', unseenSplashId);
setSearchParams(searchParams, { replace: true });
}
}, [user, splash, splashId, searchParams, setSearchParams]);
useEffect(() => {
if (splashId && isKnownId) {
setSplashSeen(splashId)
.then(() => refetchSplash())
.catch(console.warn);
}
// eslint-disable-next-line react-hooks/exhaustive-deps refetch and setSplashSeen are not stable references
}, [splashId, isKnownId]);
const handleClose = () => {
searchParams.delete('splash');
setSearchParams(searchParams, { replace: true });
};
if (!splashId) return null;
const getSplashContent = () => {
switch (splashId) {
case 'release-management':
return <ReleaseManagementSplash onClose={handleClose} />;
default:
return null;
}
};
const content = getSplashContent();
if (!content) return null;
return (
<Modal
open={Boolean(splashId)}
onClose={handleClose}
closeAfterTransition
slots={{ backdrop: StyledBackdrop }}
slotProps={{ backdrop: { timeout: TRANSITION_DURATION } }}
sx={{ zIndex: (theme) => theme.zIndex.modal }}
>
<Fade timeout={TRANSITION_DURATION} in={Boolean(splashId)}>
<ModalContent>{content}</ModalContent>
</Fade>
</Modal>
);
};

View File

@ -62,11 +62,13 @@ class UserSplashController extends Controller {
seen: true,
};
const updatedSplash = await this.userSplashService.updateSplash(splash);
this.openApiService.respondWithValidation(
200,
res,
splashRequestSchema.$id,
await this.userSplashService.updateSplash(splash),
updatedSplash,
);
}
}