mirror of
https://github.com/Unleash/unleash.git
synced 2026-01-05 20:06:22 +01:00
Feat/feature flag splash (#11157)
This PR sets up the new splash overlay behind a feature flag and adds the video as an asset
This commit is contained in:
parent
8205a9d973
commit
a770549fd0
@ -11,7 +11,7 @@ import ToastRenderer from 'component/common/ToastRenderer/ToastRenderer';
|
||||
import { routes } from 'component/menu/routes';
|
||||
import { useAuthDetails } from 'hooks/api/getters/useAuth/useAuthDetails';
|
||||
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
|
||||
import { SplashPageRedirect } from 'component/splash/SplashPageRedirect/SplashPageRedirect';
|
||||
import { SplashOverlay } from 'component/splash/SplashOverlay/SplashOverlay';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
|
||||
import { MaintenanceBanner } from './maintenance/MaintenanceBanner.tsx';
|
||||
@ -113,7 +113,7 @@ export const App = () => {
|
||||
|
||||
<FeedbackNPS openUrl='http://feedback.unleash.run' />
|
||||
|
||||
<SplashPageRedirect />
|
||||
<SplashOverlay />
|
||||
</StyledContainer>
|
||||
</>
|
||||
</Demo>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Box,
|
||||
@ -9,12 +10,11 @@ import {
|
||||
} 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 PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
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 YOUTUBE_VIDEO_ID = '40IUj67e9Ew';
|
||||
const DOCS_URL = 'https://docs.getunleash.io/concepts/impact-metrics';
|
||||
|
||||
const DialogCard = styled(Box)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
@ -35,7 +35,7 @@ const HeaderRow = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: theme.spacing(2, 3),
|
||||
padding: theme.spacing(1, 3),
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
}));
|
||||
|
||||
@ -44,19 +44,18 @@ const StyledCloseButton = styled(IconButton)(({ theme }) => ({
|
||||
}));
|
||||
|
||||
const StyledLogo = styled(UnleashLogo)(({ theme }) => ({
|
||||
height: theme.spacing(5),
|
||||
height: theme.spacing(6),
|
||||
}));
|
||||
|
||||
const ContentContainer = styled(Box)(({ theme }) => ({
|
||||
padding: theme.spacing(3),
|
||||
padding: `${theme.spacing(3)} ${theme.spacing(3)} ${theme.spacing(3.5)} ${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,
|
||||
fontWeight: theme.typography.fontWeightBold,
|
||||
color: theme.palette.text.primary,
|
||||
marginBottom: theme.spacing(1),
|
||||
}));
|
||||
@ -69,7 +68,7 @@ const StyledDescription = styled(Typography)(({ theme }) => ({
|
||||
const LinksRow = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
gap: theme.spacing(3),
|
||||
marginBottom: theme.spacing(3),
|
||||
marginBottom: theme.spacing(4),
|
||||
}));
|
||||
|
||||
const StyledLink = styled('a')(({ theme }) => ({
|
||||
@ -85,11 +84,11 @@ const StyledLink = styled('a')(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const YouTubeContainer = styled(Box)(({ theme }) => ({
|
||||
const VideoContainer = styled(Box)(({ theme }) => ({
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
paddingBottom: '56.25%',
|
||||
marginBottom: theme.spacing(3),
|
||||
marginBottom: theme.spacing(4),
|
||||
borderRadius: theme.shape.borderRadiusLarge,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: theme.palette.background.elevation1,
|
||||
@ -103,6 +102,55 @@ const YouTubeContainer = styled(Box)(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const VideoThumbnail = styled('img')({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
});
|
||||
|
||||
const PlayOverlay = styled('button')({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
padding: 0,
|
||||
outline: 'none',
|
||||
'&:focus-visible > div': {
|
||||
outline: '2px solid white',
|
||||
outlineOffset: '2px',
|
||||
},
|
||||
});
|
||||
|
||||
const PlayButton = styled(Box)(({ theme }) => ({
|
||||
width: theme.spacing(10),
|
||||
height: theme.spacing(10),
|
||||
borderRadius: '50%',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: theme.shadows[4],
|
||||
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.05)',
|
||||
boxShadow: theme.shadows[8],
|
||||
},
|
||||
'& svg': {
|
||||
fontSize: theme.spacing(5),
|
||||
color: theme.palette.common.white,
|
||||
},
|
||||
}));
|
||||
|
||||
const ActionsRow = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
@ -117,12 +165,17 @@ export const ReleaseManagementSplash = ({
|
||||
onClose,
|
||||
}: ReleaseManagementSplashProps) => {
|
||||
const navigate = useNavigate();
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
|
||||
const handleGetStarted = () => {
|
||||
onClose();
|
||||
navigate('/release-templates');
|
||||
};
|
||||
|
||||
const handlePlayClick = () => {
|
||||
setIsPlaying(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogCard>
|
||||
<HeaderRow>
|
||||
@ -159,24 +212,34 @@ export const ReleaseManagementSplash = ({
|
||||
<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>
|
||||
<VideoContainer>
|
||||
{isPlaying ? (
|
||||
<iframe
|
||||
src={`https://www.youtube.com/embed/${YOUTUBE_VIDEO_ID}?autoplay=1`}
|
||||
title='Release management introduction video'
|
||||
allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'
|
||||
allowFullScreen
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<VideoThumbnail
|
||||
src={`https://img.youtube.com/vi/${YOUTUBE_VIDEO_ID}/hqdefault.jpg`}
|
||||
alt='Video thumbnail'
|
||||
/>
|
||||
<PlayOverlay
|
||||
onClick={handlePlayClick}
|
||||
aria-label='Play video'
|
||||
type='button'
|
||||
>
|
||||
<PlayButton>
|
||||
<PlayArrowIcon />
|
||||
</PlayButton>
|
||||
</PlayOverlay>
|
||||
</>
|
||||
)}
|
||||
</VideoContainer>
|
||||
|
||||
<ActionsRow>
|
||||
<Button
|
||||
@ -184,7 +247,7 @@ export const ReleaseManagementSplash = ({
|
||||
color='primary'
|
||||
onClick={handleGetStarted}
|
||||
>
|
||||
Get started with release management
|
||||
View the getting started guide
|
||||
</Button>
|
||||
<Button variant='text' onClick={onClose}>
|
||||
Cancel
|
||||
|
||||
@ -1,16 +1,12 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { Modal, Backdrop, styled } from '@mui/material';
|
||||
import Fade from '@mui/material/Fade';
|
||||
import { useFlag } from '@unleash/proxy-client-react';
|
||||
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 { splashIds, type SplashId } from 'component/splash/splash';
|
||||
import { ReleaseManagementSplash } from './ReleaseManagementSplash';
|
||||
|
||||
const TRANSITION_DURATION = 250;
|
||||
@ -27,10 +23,6 @@ const ModalContent = styled('div')({
|
||||
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);
|
||||
};
|
||||
@ -41,43 +33,55 @@ export const SplashOverlay = () => {
|
||||
const { splash, refetchSplash } = useAuthSplash();
|
||||
const { setSplashSeen } = useSplashApi();
|
||||
|
||||
const [closedSplash, setClosedSplash] = useState(false);
|
||||
|
||||
const releaseManagementV3Enabled = useFlag('releaseManagementV3Splash');
|
||||
|
||||
const splashId = searchParams.get('splash');
|
||||
const isKnownId = splashId ? isKnownSplashId(splashId) : false;
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || !splash || splashId) return;
|
||||
if (!user || !splash || splashId || user.isAPI || closedSplash) return;
|
||||
|
||||
if (user.isAPI) return;
|
||||
|
||||
const unseenSplashId = activeSplashIds.find(
|
||||
(id) => !hasSeenSplashId(id, splash),
|
||||
const hasSeenReleaseManagementV3 = Boolean(
|
||||
splash['release-management-v3'],
|
||||
);
|
||||
|
||||
if (unseenSplashId) {
|
||||
searchParams.set('splash', unseenSplashId);
|
||||
if (releaseManagementV3Enabled && !hasSeenReleaseManagementV3) {
|
||||
searchParams.set('splash', 'release-management-v3');
|
||||
setSearchParams(searchParams, { replace: true });
|
||||
}
|
||||
}, [user, splash, splashId, searchParams, setSearchParams]);
|
||||
}, [
|
||||
user,
|
||||
splash,
|
||||
splashId,
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
releaseManagementV3Enabled,
|
||||
closedSplash,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (splashId && isKnownId) {
|
||||
setSplashSeen(splashId)
|
||||
const handleClose = () => {
|
||||
const currentSplashId = splashId;
|
||||
const currentIsKnownId = isKnownId;
|
||||
|
||||
setClosedSplash(true);
|
||||
|
||||
searchParams.delete('splash');
|
||||
setSearchParams(searchParams, { replace: true });
|
||||
|
||||
if (currentSplashId && currentIsKnownId) {
|
||||
setSplashSeen(currentSplashId)
|
||||
.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':
|
||||
case 'release-management-v3':
|
||||
return <ReleaseManagementSplash onClose={handleClose} />;
|
||||
default:
|
||||
return null;
|
||||
|
||||
@ -2,7 +2,6 @@ import { useNavigate, Navigate } from 'react-router-dom';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import useSplashApi from 'hooks/api/actions/useSplashApi/useSplashApi';
|
||||
import { SplashPageOperators } from 'component/splash/SplashPageOperators';
|
||||
import { SplashPageReleaseManagement } from 'component/splash/SplashPageReleaseManagement';
|
||||
import { useEffect } from 'react';
|
||||
import { useAuthSplash } from 'hooks/api/getters/useAuth/useAuthSplash';
|
||||
import { splashIds, type SplashId } from 'component/splash/splash';
|
||||
|
||||
@ -1,45 +0,0 @@
|
||||
import { useAuthSplash } from 'hooks/api/getters/useAuth/useAuthSplash';
|
||||
import { useLocation, Navigate } from 'react-router-dom';
|
||||
import { matchPath } from 'react-router';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import type { IAuthSplash } from 'hooks/api/getters/useAuth/useAuthEndpoint';
|
||||
import { activeSplashIds, type SplashId } from 'component/splash/splash';
|
||||
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
|
||||
|
||||
export const SplashPageRedirect = () => {
|
||||
const { pathname } = useLocation();
|
||||
const { user } = useAuthUser();
|
||||
const { splash } = useAuthSplash();
|
||||
const { uiConfig, loading } = useUiConfig();
|
||||
|
||||
if (!user || !splash || !uiConfig || loading) {
|
||||
// Wait for everything to load.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (matchPath('/splash/:splashId', pathname)) {
|
||||
// We've already redirected to the splash page.
|
||||
return null;
|
||||
}
|
||||
|
||||
// Read-only API users should never see splash screens
|
||||
// since they don't have access to mark them as seen.
|
||||
if (user.isAPI) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the splash page to show (if any).
|
||||
const showSplashId = activeSplashIds.find((splashId) => {
|
||||
return !hasSeenSplashId(splashId, splash);
|
||||
});
|
||||
|
||||
if (!showSplashId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Navigate to={`/splash/${showSplashId}`} replace />;
|
||||
};
|
||||
|
||||
const hasSeenSplashId = (splashId: SplashId, splash: IAuthSplash): boolean => {
|
||||
return Boolean(splash[splashId]);
|
||||
};
|
||||
@ -1,7 +1,4 @@
|
||||
// All known splash IDs.
|
||||
export const splashIds = ['operators'] as const;
|
||||
|
||||
// Active splash IDs that may be shown to the user.
|
||||
export const activeSplashIds: SplashId[] = [];
|
||||
export const splashIds = ['operators', 'release-management-v3'] as const;
|
||||
|
||||
export type SplashId = (typeof splashIds)[number];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user