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:
parent
4c1ef6aa1a
commit
ed6f728e7e
@ -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>
|
||||
);
|
||||
};
|
||||
104
frontend/src/component/splash/SplashOverlay/SplashOverlay.tsx
Normal file
104
frontend/src/component/splash/SplashOverlay/SplashOverlay.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user