mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-24 01:18:01 +02:00
Feat/splash (#491)
* splash screen * add styles for controllers * feat: animated circles * fix: remove unused code * fix: folder structure * create splash screens for envs * add styles and ui changes * fix: revert App.tsx * add splash state to store * add splash to app.tsx + add a loader * fix: mobile view + desktop view * fix: render splash condition + styling fix * fix: change splash display to full screen * Update src/hooks/api/actions/useSplashApi/useSplashApi.ts Co-authored-by: Fredrik Strand Oseberg <fredrik.no@gmail.com> * fix: change function type Co-authored-by: Fredrik Strand Oseberg <fredrik.no@gmail.com> * fix: disable incrementing counter when matching the components length. * fix: add SWR configuration * fix: spelling mistakes in splash screen * fix: add keys and adjust styling * fix: tests * fix: tests * fix: default command timeout Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
This commit is contained in:
parent
00b9a6c38d
commit
c34d8439bd
@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"projectId": "tc2qff"
|
"projectId": "tc2qff",
|
||||||
|
"defaultCommandTimeout": 12000
|
||||||
}
|
}
|
||||||
|
@ -67,6 +67,16 @@ describe('feature toggle', () => {
|
|||||||
cy.get('[data-test="LOGIN_PASSWORD_ID"]').type('qY70$NDcJNXA');
|
cy.get('[data-test="LOGIN_PASSWORD_ID"]').type('qY70$NDcJNXA');
|
||||||
|
|
||||||
cy.get("[data-test='LOGIN_BUTTON']").click();
|
cy.get("[data-test='LOGIN_BUTTON']").click();
|
||||||
|
|
||||||
|
cy.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: `${
|
||||||
|
Cypress.config().baseUrl
|
||||||
|
}/api/admin/features/${featureToggleName}`,
|
||||||
|
headers: {
|
||||||
|
Authorization: authToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
cy.get('[data-test=LOGIN_EMAIL_ID]').type('test@unleash-e2e.com');
|
cy.get('[data-test=LOGIN_EMAIL_ID]').type('test@unleash-e2e.com');
|
||||||
cy.get('[data-test=LOGIN_BUTTON]').click();
|
cy.get('[data-test=LOGIN_BUTTON]').click();
|
||||||
@ -74,6 +84,12 @@ describe('feature toggle', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Creates a feature toggle', () => {
|
it('Creates a feature toggle', () => {
|
||||||
|
if (
|
||||||
|
document.querySelectorAll("[data-test='CLOSE_SPLASH']").length > 0
|
||||||
|
) {
|
||||||
|
cy.get("[data-test='CLOSE_SPLASH']").click();
|
||||||
|
}
|
||||||
|
|
||||||
cy.get('[data-test=NAVIGATE_TO_CREATE_FEATURE').click();
|
cy.get('[data-test=NAVIGATE_TO_CREATE_FEATURE').click();
|
||||||
|
|
||||||
cy.intercept('POST', '/api/admin/features').as('createFeature');
|
cy.intercept('POST', '/api/admin/features').as('createFeature');
|
||||||
|
BIN
frontend/src/assets/img/env-splash-2.png
Normal file
BIN
frontend/src/assets/img/env-splash-2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 454 KiB |
19
frontend/src/assets/img/splash_env1.svg
Normal file
19
frontend/src/assets/img/splash_env1.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 81 KiB |
2
frontend/src/assets/img/splash_env2.svg
Normal file
2
frontend/src/assets/img/splash_env2.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 67 KiB |
BIN
frontend/src/assets/img/unleash_logo_icon_dark _ alpha.gif
Normal file
BIN
frontend/src/assets/img/unleash_logo_icon_dark _ alpha.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 88 KiB |
@ -10,25 +10,36 @@ import { routes } from './menu/routes';
|
|||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
import IAuthStatus from '../interfaces/user';
|
import IAuthStatus from '../interfaces/user';
|
||||||
import { useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import NotFound from './common/NotFound/NotFound';
|
import NotFound from './common/NotFound/NotFound';
|
||||||
import Feedback from './common/Feedback';
|
import Feedback from './common/Feedback';
|
||||||
import useToast from '../hooks/useToast';
|
import useToast from '../hooks/useToast';
|
||||||
import SWRProvider from './providers/SWRProvider/SWRProvider';
|
import SWRProvider from './providers/SWRProvider/SWRProvider';
|
||||||
|
import ConditionallyRender from './common/ConditionallyRender';
|
||||||
|
import EnvironmentSplash from './common/EnvironmentSplash/EnvironmentSplash';
|
||||||
|
import Loader from './common/Loader/Loader';
|
||||||
|
import useUser from '../hooks/api/getters/useUser/useUser';
|
||||||
|
|
||||||
interface IAppProps extends RouteComponentProps {
|
interface IAppProps extends RouteComponentProps {
|
||||||
user: IAuthStatus;
|
user: IAuthStatus;
|
||||||
fetchUiBootstrap: any;
|
fetchUiBootstrap: any;
|
||||||
feedback: any;
|
feedback: any;
|
||||||
}
|
}
|
||||||
|
const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
|
||||||
const App = ({ location, user, fetchUiBootstrap, feedback }: IAppProps) => {
|
|
||||||
const { toast, setToastData } = useToast();
|
const { toast, setToastData } = useToast();
|
||||||
|
// because we need the userId when the component load.
|
||||||
|
const { splash, user: userFromUseUser } = useUser();
|
||||||
|
const [showSplash, setShowSplash] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUiBootstrap();
|
fetchUiBootstrap();
|
||||||
/* eslint-disable-next-line */
|
/* eslint-disable-next-line */
|
||||||
}, [user.authDetails?.type]);
|
}, [user.authDetails?.type]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShowSplash(!splash?.environments && !isUnauthorized());
|
||||||
|
/* eslint-disable-next-line */
|
||||||
|
}, [splash]);
|
||||||
|
|
||||||
const renderMainLayoutRoutes = () => {
|
const renderMainLayoutRoutes = () => {
|
||||||
return routes.filter(route => route.layout === 'main').map(renderRoute);
|
return routes.filter(route => route.layout === 'main').map(renderRoute);
|
||||||
};
|
};
|
||||||
@ -79,8 +90,17 @@ const App = ({ location, user, fetchUiBootstrap, feedback }: IAppProps) => {
|
|||||||
setToastData={setToastData}
|
setToastData={setToastData}
|
||||||
isUnauthorized={isUnauthorized}
|
isUnauthorized={isUnauthorized}
|
||||||
>
|
>
|
||||||
{' '}
|
<ConditionallyRender
|
||||||
|
condition={!isUnauthorized() && !userFromUseUser?.id}
|
||||||
|
show={<Loader />}
|
||||||
|
elseShow={
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={showSplash}
|
||||||
|
show={
|
||||||
|
<EnvironmentSplash onFinish={setShowSplash} />
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
<LayoutPicker location={location}>
|
<LayoutPicker location={location}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<ProtectedRoute
|
<ProtectedRoute
|
||||||
@ -92,7 +112,10 @@ const App = ({ location, user, fetchUiBootstrap, feedback }: IAppProps) => {
|
|||||||
/>
|
/>
|
||||||
{renderMainLayoutRoutes()}
|
{renderMainLayoutRoutes()}
|
||||||
{renderStandaloneRoutes()}
|
{renderStandaloneRoutes()}
|
||||||
<Route path="/404" component={NotFound} />
|
<Route
|
||||||
|
path="/404"
|
||||||
|
component={NotFound}
|
||||||
|
/>
|
||||||
<Redirect to="/404" />
|
<Redirect to="/404" />
|
||||||
</Switch>
|
</Switch>
|
||||||
<Feedback
|
<Feedback
|
||||||
@ -100,8 +123,13 @@ const App = ({ location, user, fetchUiBootstrap, feedback }: IAppProps) => {
|
|||||||
openUrl="http://feedback.unleash.run"
|
openUrl="http://feedback.unleash.run"
|
||||||
/>
|
/>
|
||||||
</LayoutPicker>
|
</LayoutPicker>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{toast}
|
{toast}
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</SWRProvider>
|
</SWRProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
splashContainer: {
|
||||||
|
position: 'fixed',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: '20px',
|
||||||
|
lineHeight: '1.3',
|
||||||
|
[theme.breakpoints.down('xs')]: {
|
||||||
|
marginTop: '1rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
topDescription: {
|
||||||
|
padding: '0px 40px',
|
||||||
|
marginBottom: '15px',
|
||||||
|
fontSize: '17px',
|
||||||
|
[theme.breakpoints.down('xs')]: {
|
||||||
|
padding: '0 20px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bottomDescription: {
|
||||||
|
padding: '0px 20px',
|
||||||
|
fontSize: '17px',
|
||||||
|
marginTop: '15px',
|
||||||
|
[theme.breakpoints.down('xs')]: {
|
||||||
|
padding: '0 20px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
fontSize: '150px',
|
||||||
|
display: 'block',
|
||||||
|
margin: 'auto',
|
||||||
|
[theme.breakpoints.down('xs')]: {
|
||||||
|
fontSize: '90px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
logo: {
|
||||||
|
width: '70%',
|
||||||
|
height: '60%',
|
||||||
|
display: 'block',
|
||||||
|
margin: 'auto',
|
||||||
|
marginTop: '2rem',
|
||||||
|
[theme.breakpoints.down('xs')]: {
|
||||||
|
width: '80%',
|
||||||
|
height: '80%',
|
||||||
|
marginTop: '0rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
linkList: {
|
||||||
|
padding: '30px 25px',
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
}));
|
@ -0,0 +1,204 @@
|
|||||||
|
import Splash from '../Splash/Splash';
|
||||||
|
import EnvironmentSplashPage from './EnvironmentSplashPage/EnvironmentSplashPage';
|
||||||
|
import { VpnKey, CloudCircle } from '@material-ui/icons';
|
||||||
|
import { useStyles } from './EnvironmentSplash.styles';
|
||||||
|
import { ReactComponent as Logo1 } from '../../../assets/img/splash_env1.svg';
|
||||||
|
import { ReactComponent as Logo2 } from '../../../assets/img/splash_env2.svg';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import useSplashApi from '../../../hooks/api/actions/useSplashApi/useSplashApi';
|
||||||
|
|
||||||
|
interface IEnvironmentSplashProps {
|
||||||
|
onFinish: (status: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EnvironmentSplash = ({ onFinish }: IEnvironmentSplashProps) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
const { setSplashSeen } = useSplashApi();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSplashSeen('environments');
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Splash
|
||||||
|
onFinish={onFinish}
|
||||||
|
components={[
|
||||||
|
<EnvironmentSplashPage
|
||||||
|
key={1}
|
||||||
|
title={
|
||||||
|
<h2 className={styles.title}>
|
||||||
|
Environments are coming to Unleash!
|
||||||
|
</h2>
|
||||||
|
}
|
||||||
|
topDescription={
|
||||||
|
<p className={styles.topDescription}>
|
||||||
|
We are bringing native environment support to
|
||||||
|
Unleash.{' '}
|
||||||
|
<b>
|
||||||
|
Your current configurations won’t be
|
||||||
|
affected,
|
||||||
|
</b>{' '}
|
||||||
|
but you’ll have the option of adding strategies
|
||||||
|
to specific environments going forward.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
bottomDescription={
|
||||||
|
<p className={styles.bottomDescription}>
|
||||||
|
By default you will get access to three
|
||||||
|
environments: <b>default</b>, <b>development</b>{' '}
|
||||||
|
and<b> production</b>. All of your current
|
||||||
|
configurations will live in the default
|
||||||
|
environment and{' '}
|
||||||
|
<b>
|
||||||
|
nothing will change until you make a
|
||||||
|
conscious decision to change.
|
||||||
|
</b>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
image={<CloudCircle className={styles.icon} />}
|
||||||
|
/>,
|
||||||
|
<EnvironmentSplashPage
|
||||||
|
key={2}
|
||||||
|
title={
|
||||||
|
<h2 className={styles.title}>
|
||||||
|
Strategies live in environments
|
||||||
|
</h2>
|
||||||
|
}
|
||||||
|
topDescription={
|
||||||
|
<p className={styles.topDescription}>
|
||||||
|
A feature toggle lives as an entity across
|
||||||
|
multiple environments, but your strategies will
|
||||||
|
live in a specific environment. This allows you
|
||||||
|
to have different configuration per environment
|
||||||
|
for a feature toggle.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
image={<Logo1 className={styles.logo} />}
|
||||||
|
/>,
|
||||||
|
<EnvironmentSplashPage
|
||||||
|
key={3}
|
||||||
|
title={
|
||||||
|
<h2 className={styles.title}>
|
||||||
|
Environments are turned on per project
|
||||||
|
</h2>
|
||||||
|
}
|
||||||
|
topDescription={
|
||||||
|
<p className={styles.topDescription}>
|
||||||
|
In order to enable an environment for a feature
|
||||||
|
toggle you must first enable the environment in
|
||||||
|
your project. Navigate to your project settings
|
||||||
|
and enable the environments you want to be
|
||||||
|
available. The toggles in that project will get
|
||||||
|
access to all of the project’s enabled
|
||||||
|
environments.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
image={<Logo2 className={styles.logo} />}
|
||||||
|
/>,
|
||||||
|
<EnvironmentSplashPage
|
||||||
|
key={4}
|
||||||
|
title={
|
||||||
|
<h2 className={styles.title}>
|
||||||
|
API Keys control which environment you get the
|
||||||
|
configuration from
|
||||||
|
</h2>
|
||||||
|
}
|
||||||
|
topDescription={
|
||||||
|
<p className={styles.topDescription}>
|
||||||
|
When you have set up environments for your
|
||||||
|
feature toggles and added strategies to the
|
||||||
|
specific environments, you must create
|
||||||
|
environment-specific API keys — one for each
|
||||||
|
environment.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
bottomDescription={
|
||||||
|
<p className={styles.bottomDescription}>
|
||||||
|
Environment-specific API keys lets the SDK
|
||||||
|
receive configuration only for the specified
|
||||||
|
environment.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
image={<VpnKey className={styles.icon} />}
|
||||||
|
/>,
|
||||||
|
<EnvironmentSplashPage
|
||||||
|
key={5}
|
||||||
|
title={
|
||||||
|
<h2 className={styles.title}>Want to know more?</h2>
|
||||||
|
}
|
||||||
|
topDescription={
|
||||||
|
<div className={styles.topDescription}>
|
||||||
|
If you’d like some more info on environments,
|
||||||
|
check out some of the resources below! The
|
||||||
|
documentation or the video walkthrough is a
|
||||||
|
great place to start. If you’d like to try it
|
||||||
|
out in a risk-free setting first, how about
|
||||||
|
heading to the demo instance?
|
||||||
|
<ul className={styles.linkList}>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://www.loom.com/share/95239e875bbc4e09a5c5833e1942e4b0?t=0"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className={styles.link}
|
||||||
|
>
|
||||||
|
Video walkthrough
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://app.unleash-hosted.com/demo/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className={styles.link}
|
||||||
|
>
|
||||||
|
The Unleash demo instance
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://docs.getunleash.io/user_guide/environments"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className={styles.link}
|
||||||
|
>
|
||||||
|
Environments reference documentation
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://www.getunleash.io/blog/simplify-rollout-management-with-the-new-environments-feature"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className={styles.link}
|
||||||
|
>
|
||||||
|
Blog post introducing environments
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
bottomDescription={
|
||||||
|
<p className={styles.bottomDescription}>
|
||||||
|
If you have any questions or need help, feel
|
||||||
|
free to ping us on{' '}
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href="https://slack.unleash.run/"
|
||||||
|
rel="noreferrer"
|
||||||
|
className={styles.link}
|
||||||
|
>
|
||||||
|
slack!
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EnvironmentSplash;
|
@ -0,0 +1,24 @@
|
|||||||
|
interface EnvironmentSplashPageProps {
|
||||||
|
title: React.ReactNode;
|
||||||
|
topDescription: React.ReactNode;
|
||||||
|
image?: React.ReactNode;
|
||||||
|
bottomDescription?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EnvironmentSplashPage = ({
|
||||||
|
title,
|
||||||
|
topDescription,
|
||||||
|
image,
|
||||||
|
bottomDescription,
|
||||||
|
}: EnvironmentSplashPageProps) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{title}
|
||||||
|
{topDescription}
|
||||||
|
{image}
|
||||||
|
{bottomDescription}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EnvironmentSplashPage;
|
14
frontend/src/component/common/Loader/Loader.styles.ts
Normal file
14
frontend/src/component/common/Loader/Loader.styles.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
loader: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
img: {
|
||||||
|
width: '100px',
|
||||||
|
height: '100px',
|
||||||
|
},
|
||||||
|
}));
|
14
frontend/src/component/common/Loader/Loader.tsx
Normal file
14
frontend/src/component/common/Loader/Loader.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import logo from '../../../assets/img/unleash_logo_icon_dark _ alpha.gif';
|
||||||
|
import { useStyles } from './Loader.styles';
|
||||||
|
|
||||||
|
const Loader = () => {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.loader}>
|
||||||
|
<img className={styles.img} src={logo} alt="loading..." />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Loader;
|
96
frontend/src/component/common/Splash/Splash.styles.ts
Normal file
96
frontend/src/component/common/Splash/Splash.styles.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
splashMainContainer: {
|
||||||
|
backgroundColor: theme.palette.primary.light,
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '3rem 0',
|
||||||
|
[theme.breakpoints.down('xs')]: {
|
||||||
|
padding: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
splashContainer: {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
position: 'relative',
|
||||||
|
minHeight: '650px',
|
||||||
|
width: '600px',
|
||||||
|
padding: '2rem 1.5rem',
|
||||||
|
borderRadius: '5px',
|
||||||
|
color: '#fff',
|
||||||
|
display: 'flex',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
flexDirection: 'column',
|
||||||
|
[theme.breakpoints.down('xs')]: {
|
||||||
|
top: '0px',
|
||||||
|
left: '0px',
|
||||||
|
right: '0px',
|
||||||
|
bottom: '0px',
|
||||||
|
padding: '2rem 0',
|
||||||
|
zIndex: '500',
|
||||||
|
position: 'fixed',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
borderRadius: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
closeButtonContainer: {
|
||||||
|
display: 'inline-flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
color: '#fff',
|
||||||
|
position: 'absolute',
|
||||||
|
right: '-10px',
|
||||||
|
top: '5px',
|
||||||
|
},
|
||||||
|
closeButton: {
|
||||||
|
textDecoration: 'none',
|
||||||
|
right: '10px',
|
||||||
|
color: '#fff',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'inherit',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
controllers: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
height: 'inherit',
|
||||||
|
marginBottom: 5,
|
||||||
|
marginTop: 'auto',
|
||||||
|
},
|
||||||
|
circlesContainer: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
circles: {
|
||||||
|
display: 'inline-flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginTop: 20,
|
||||||
|
marginBottom: 15,
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
buttonsContainer: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
textDecoration: 'none',
|
||||||
|
width: '100px',
|
||||||
|
color: '#fff',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'inherit',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nextButton: {
|
||||||
|
textDecoration: 'none',
|
||||||
|
width: '100px',
|
||||||
|
color: theme.palette.primary.light,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
114
frontend/src/component/common/Splash/Splash.tsx
Normal file
114
frontend/src/component/common/Splash/Splash.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { Fragment } from 'react';
|
||||||
|
import { Button, IconButton } from '@material-ui/core';
|
||||||
|
import { useStyles } from './Splash.styles';
|
||||||
|
import {
|
||||||
|
FiberManualRecord,
|
||||||
|
FiberManualRecordOutlined,
|
||||||
|
CloseOutlined,
|
||||||
|
} from '@material-ui/icons';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import ConditionallyRender from '../ConditionallyRender';
|
||||||
|
import { CLOSE_SPLASH } from '../../../testIds';
|
||||||
|
|
||||||
|
interface ISplashProps {
|
||||||
|
components: React.ReactNode[];
|
||||||
|
onFinish: (status: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Splash: React.FC<ISplashProps> = ({
|
||||||
|
components,
|
||||||
|
onFinish,
|
||||||
|
}: ISplashProps) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
const [counter, setCounter] = useState(0);
|
||||||
|
|
||||||
|
const onNext = () => {
|
||||||
|
if (counter === components.length - 1) {
|
||||||
|
onFinish(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCounter(counter + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBack = () => {
|
||||||
|
setCounter(counter - 1);
|
||||||
|
};
|
||||||
|
const onClose = () => {
|
||||||
|
onFinish(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculatePosition = () => {
|
||||||
|
if (counter === 0) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
return counter * 24;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderCircles = () => {
|
||||||
|
return components.map((_, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
// Use index as key because the amount of pages will never dynamically change.
|
||||||
|
return (
|
||||||
|
<Fragment key={index}>
|
||||||
|
<FiberManualRecordOutlined />
|
||||||
|
<FiberManualRecord
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
transition: 'transform 0.3s ease',
|
||||||
|
left: '0',
|
||||||
|
transform: `translateX(${calculatePosition()}px)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <FiberManualRecordOutlined />;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.splashMainContainer}>
|
||||||
|
<div className={styles.splashContainer}>
|
||||||
|
<div className={styles.closeButtonContainer}>
|
||||||
|
<IconButton
|
||||||
|
className={styles.closeButton}
|
||||||
|
onClick={onClose}
|
||||||
|
data-test={CLOSE_SPLASH}
|
||||||
|
>
|
||||||
|
<CloseOutlined />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
{components[counter]}
|
||||||
|
<div className={styles.controllers}>
|
||||||
|
<div className={styles.circlesContainer}>
|
||||||
|
<div className={styles.circles}>{renderCircles()}</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.buttonsContainer}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={counter > 0}
|
||||||
|
show={
|
||||||
|
<Button
|
||||||
|
className={styles.button}
|
||||||
|
disabled={counter === 0}
|
||||||
|
onClick={onBack}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button className={styles.nextButton} onClick={onNext}>
|
||||||
|
{counter === components.length - 1
|
||||||
|
? 'Finish'
|
||||||
|
: 'Next'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Splash;
|
27
frontend/src/hooks/api/actions/useSplashApi/useSplashApi.ts
Normal file
27
frontend/src/hooks/api/actions/useSplashApi/useSplashApi.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import useAPI from '../useApi/useApi';
|
||||||
|
|
||||||
|
const useSplashApi = () => {
|
||||||
|
const { makeRequest, createRequest } = useAPI({
|
||||||
|
propagateErrors: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const setSplashSeen = async (splashId: string) => {
|
||||||
|
const path = `api/admin/splash/${splashId}`;
|
||||||
|
const req = createRequest(path, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await makeRequest(req.caller, req.id);
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
console.log('An exception was caught and handled.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
setSplashSeen,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useSplashApi;
|
@ -6,7 +6,13 @@ import handleErrorResponses from '../httpErrorResponseHandler';
|
|||||||
|
|
||||||
export const USER_CACHE_KEY = `api/admin/user`;
|
export const USER_CACHE_KEY = `api/admin/user`;
|
||||||
|
|
||||||
const useUser = (options: SWRConfiguration = {}) => {
|
const useUser = (
|
||||||
|
options: SWRConfiguration = {
|
||||||
|
revalidateIfStale: false,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
}
|
||||||
|
) => {
|
||||||
const fetcher = () => {
|
const fetcher = () => {
|
||||||
const path = formatApiPath(`api/admin/user`);
|
const path = formatApiPath(`api/admin/user`);
|
||||||
return fetch(path, {
|
return fetch(path, {
|
||||||
@ -31,6 +37,7 @@ const useUser = (options: SWRConfiguration = {}) => {
|
|||||||
user: data?.user || {},
|
user: data?.user || {},
|
||||||
permissions: (data?.permissions || []) as IPermission[],
|
permissions: (data?.permissions || []) as IPermission[],
|
||||||
feedback: data?.feedback || [],
|
feedback: data?.feedback || [],
|
||||||
|
splash: data?.splash || {},
|
||||||
authDetails: data || {},
|
authDetails: data || {},
|
||||||
error,
|
error,
|
||||||
loading,
|
loading,
|
||||||
|
@ -3,6 +3,11 @@ export interface IAuthStatus {
|
|||||||
showDialog: boolean;
|
showDialog: boolean;
|
||||||
profile?: IUser;
|
profile?: IUser;
|
||||||
permissions: IPermission[];
|
permissions: IPermission[];
|
||||||
|
splash: ISplash;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISplash {
|
||||||
|
[key: string]: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPermission {
|
export interface IPermission {
|
||||||
|
@ -9,6 +9,7 @@ const userStore = (state = new $Map({ permissions: [] }), action) => {
|
|||||||
.set('profile', action.value.user)
|
.set('profile', action.value.user)
|
||||||
.set('permissions', action.value.permissions || [])
|
.set('permissions', action.value.permissions || [])
|
||||||
.set('feedback', action.value.feedback || [])
|
.set('feedback', action.value.feedback || [])
|
||||||
|
.set('splash', action.value.splash || {})
|
||||||
.set('showDialog', false)
|
.set('showDialog', false)
|
||||||
.set('authDetails', undefined);
|
.set('authDetails', undefined);
|
||||||
return state;
|
return state;
|
||||||
|
@ -34,3 +34,6 @@ export const UPDATE_STRATEGY_BUTTON_ID = 'UPDATE_STRATEGY_BUTTON_ID';
|
|||||||
export const DELETE_STRATEGY_ID = 'DELETE_STRATEGY_ID';
|
export const DELETE_STRATEGY_ID = 'DELETE_STRATEGY_ID';
|
||||||
export const STRATEGY_INPUT_LIST = 'STRATEGY_INPUT_LIST';
|
export const STRATEGY_INPUT_LIST = 'STRATEGY_INPUT_LIST';
|
||||||
export const ADD_TO_STRATEGY_INPUT_LIST = 'ADD_TO_STRATEGY_INPUT_LIST';
|
export const ADD_TO_STRATEGY_INPUT_LIST = 'ADD_TO_STRATEGY_INPUT_LIST';
|
||||||
|
|
||||||
|
/* SPLASH */
|
||||||
|
export const CLOSE_SPLASH = 'CLOSE_SPLASH';
|
||||||
|
Loading…
Reference in New Issue
Block a user