mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-26 01:17:00 +02:00
Feat/user flow (#267)
* feat: add new user page * feat: passwordchecker * fix: remove loading * feat: reset password * fix: move swr to devDeps * feat: generate reset link * feat: add reset password form * fix: remove console log * fix: rename to forgotten password * feat: add simple menu * fix: change password checker title * fix: change text in new-user view * fix: lint errors * fix: add status code to constants * fix: comment * fix: add classes for new user component * fix: tests * fix: remove console log * fix: remove retry method * fix: invalid token constant * fix: remove console log * fix: dependency array on useCallback * fix: featureview * fix: redirect on authenticated * refactor: progresswheel * fix: lint deps
This commit is contained in:
parent
3cca2513fb
commit
524936912d
@ -37,6 +37,7 @@
|
|||||||
"@testing-library/jest-dom": "^5.11.4",
|
"@testing-library/jest-dom": "^5.11.4",
|
||||||
"@testing-library/react": "^11.1.0",
|
"@testing-library/react": "^11.1.0",
|
||||||
"@testing-library/user-event": "^12.1.10",
|
"@testing-library/user-event": "^12.1.10",
|
||||||
|
"@types/debounce": "^1.2.0",
|
||||||
"@types/enzyme": "^3.10.8",
|
"@types/enzyme": "^3.10.8",
|
||||||
"@types/enzyme-adapter-react-16": "^1.0.6",
|
"@types/enzyme-adapter-react-16": "^1.0.6",
|
||||||
"@types/jest": "^26.0.15",
|
"@types/jest": "^26.0.15",
|
||||||
@ -71,6 +72,7 @@
|
|||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"sass": "^1.32.8",
|
"sass": "^1.32.8",
|
||||||
"typescript": "^4.2.3",
|
"typescript": "^4.2.3",
|
||||||
|
"swr": "^0.5.5",
|
||||||
"web-vitals": "^1.0.1"
|
"web-vitals": "^1.0.1"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
|
@ -10,6 +10,10 @@ body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.MuiButton-root {
|
||||||
|
border-radius: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
.skeleton {
|
.skeleton {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -20,7 +24,7 @@ body {
|
|||||||
|
|
||||||
.skeleton::before {
|
.skeleton::before {
|
||||||
background-color: #e2e8f0;
|
background-color: #e2e8f0;
|
||||||
content: "";
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
@ -45,7 +49,7 @@ body {
|
|||||||
rgba(255, 255, 255, 0)
|
rgba(255, 255, 255, 0)
|
||||||
);
|
);
|
||||||
animation: shimmer 3s infinite;
|
animation: shimmer 3s infinite;
|
||||||
content: "";
|
content: '';
|
||||||
z-index: 5001;
|
z-index: 5001;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,11 @@ export const useCommonStyles = makeStyles(theme => ({
|
|||||||
backgroundColor: theme.palette.division.main,
|
backgroundColor: theme.palette.division.main,
|
||||||
height: '3px',
|
height: '3px',
|
||||||
},
|
},
|
||||||
|
largeDivider: {
|
||||||
|
margin: '2rem 0',
|
||||||
|
backgroundColor: theme.palette.division.main,
|
||||||
|
height: '3px',
|
||||||
|
},
|
||||||
bold: {
|
bold: {
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
},
|
},
|
||||||
@ -38,4 +43,9 @@ export const useCommonStyles = makeStyles(theme => ({
|
|||||||
fullHeight: {
|
fullHeight: {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
},
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: theme.fontSizes.mainHeader,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: '0.5rem',
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -28,7 +28,6 @@ const App = ({ location, user }: IAppProps) => {
|
|||||||
const isUnauthorized = () => {
|
const isUnauthorized = () => {
|
||||||
// authDetails only exists if the user is not logged in.
|
// authDetails only exists if the user is not logged in.
|
||||||
|
|
||||||
if (Object.keys(user).length === 0) return false;
|
|
||||||
return user?.authDetails !== undefined;
|
return user?.authDetails !== undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -54,6 +53,7 @@ const App = ({ location, user }: IAppProps) => {
|
|||||||
<route.component
|
<route.component
|
||||||
{...props}
|
{...props}
|
||||||
isUnauthorized={isUnauthorized}
|
isUnauthorized={isUnauthorized}
|
||||||
|
authDetails={user.authDetails}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
const ConditionallyRender = ({ condition, show, elseShow }) => {
|
|
||||||
const handleFunction = renderFunc => {
|
|
||||||
const result = renderFunc();
|
|
||||||
if (!result) {
|
|
||||||
/* eslint-disable-next-line */
|
|
||||||
console.warn(
|
|
||||||
'Nothing was returned from your render function. Verify that you are returning a valid react component'
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isFunc = param => typeof param === 'function';
|
|
||||||
|
|
||||||
if (condition && show) {
|
|
||||||
if (isFunc(show)) {
|
|
||||||
return handleFunction(show);
|
|
||||||
}
|
|
||||||
|
|
||||||
return show;
|
|
||||||
}
|
|
||||||
if (!condition && elseShow) {
|
|
||||||
if (isFunc(elseShow)) {
|
|
||||||
return handleFunction(elseShow);
|
|
||||||
}
|
|
||||||
return elseShow;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ConditionallyRender;
|
|
@ -0,0 +1,45 @@
|
|||||||
|
interface IConditionallyRenderProps {
|
||||||
|
condition: boolean;
|
||||||
|
show: JSX.Element | RenderFunc;
|
||||||
|
elseShow?: JSX.Element | RenderFunc;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RenderFunc = () => JSX.Element;
|
||||||
|
|
||||||
|
const ConditionallyRender = ({
|
||||||
|
condition,
|
||||||
|
show,
|
||||||
|
elseShow,
|
||||||
|
}: IConditionallyRenderProps): JSX.Element | null => {
|
||||||
|
const handleFunction = (renderFunc: RenderFunc): JSX.Element | null => {
|
||||||
|
const result = renderFunc();
|
||||||
|
if (!result) {
|
||||||
|
/* eslint-disable-next-line */
|
||||||
|
console.warn(
|
||||||
|
'Nothing was returned from your render function. Verify that you are returning a valid react component'
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFunc = (param: JSX.Element | RenderFunc) =>
|
||||||
|
typeof param === 'function';
|
||||||
|
|
||||||
|
if (condition) {
|
||||||
|
if (isFunc(show)) {
|
||||||
|
return handleFunction(show as RenderFunc);
|
||||||
|
}
|
||||||
|
|
||||||
|
return show as JSX.Element;
|
||||||
|
}
|
||||||
|
if (!condition && elseShow) {
|
||||||
|
if (isFunc(elseShow)) {
|
||||||
|
return handleFunction(elseShow as RenderFunc);
|
||||||
|
}
|
||||||
|
return elseShow as JSX.Element;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConditionallyRender;
|
26
frontend/src/component/common/Gradient/Gradient.tsx
Normal file
26
frontend/src/component/common/Gradient/Gradient.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
interface IGradientProps {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Gradient: React.FC<IGradientProps> = ({
|
||||||
|
children,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(${from}, ${to})`,
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Gradient;
|
@ -152,7 +152,6 @@ const FeatureToggleList = ({
|
|||||||
<Button
|
<Button
|
||||||
to="/features/create"
|
to="/features/create"
|
||||||
data-test="add-feature-btn"
|
data-test="add-feature-btn"
|
||||||
size="large"
|
|
||||||
color="secondary"
|
color="secondary"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
component={Link}
|
component={Link}
|
||||||
|
@ -5,7 +5,7 @@ import classnames from 'classnames';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Switch, Icon, IconButton, ListItem } from '@material-ui/core';
|
import { Switch, Icon, IconButton, ListItem } from '@material-ui/core';
|
||||||
import TimeAgo from 'react-timeago';
|
import TimeAgo from 'react-timeago';
|
||||||
import Progress from '../../progress-component';
|
import Progress from '../../ProgressWheel';
|
||||||
import Status from '../../status-component';
|
import Status from '../../status-component';
|
||||||
import FeatureToggleListItemChip from './FeatureToggleListItemChip';
|
import FeatureToggleListItemChip from './FeatureToggleListItemChip';
|
||||||
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
||||||
@ -29,18 +29,38 @@ const FeatureToggleListItem = ({
|
|||||||
|
|
||||||
const { name, description, enabled, type, stale, createdAt } = feature;
|
const { name, description, enabled, type, stale, createdAt } = feature;
|
||||||
const { showLastHour = false } = settings;
|
const { showLastHour = false } = settings;
|
||||||
const isStale = showLastHour ? metricsLastHour.isFallback : metricsLastMinute.isFallback;
|
const isStale = showLastHour
|
||||||
|
? metricsLastHour.isFallback
|
||||||
|
: metricsLastMinute.isFallback;
|
||||||
const percent =
|
const percent =
|
||||||
1 *
|
1 *
|
||||||
(showLastHour
|
(showLastHour
|
||||||
? calc(metricsLastHour.yes, metricsLastHour.yes + metricsLastHour.no, 0)
|
? calc(
|
||||||
: calc(metricsLastMinute.yes, metricsLastMinute.yes + metricsLastMinute.no, 0));
|
metricsLastHour.yes,
|
||||||
const featureUrl = toggleFeature === undefined ? `/archive/strategies/${name}` : `/features/strategies/${name}`;
|
metricsLastHour.yes + metricsLastHour.no,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
: calc(
|
||||||
|
metricsLastMinute.yes,
|
||||||
|
metricsLastMinute.yes + metricsLastMinute.no,
|
||||||
|
0
|
||||||
|
));
|
||||||
|
const featureUrl =
|
||||||
|
toggleFeature === undefined
|
||||||
|
? `/archive/strategies/${name}`
|
||||||
|
: `/features/strategies/${name}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem {...rest} className={classnames(styles.listItem, rest.className)}>
|
<ListItem
|
||||||
|
{...rest}
|
||||||
|
className={classnames(styles.listItem, rest.className)}
|
||||||
|
>
|
||||||
<span className={styles.listItemMetric}>
|
<span className={styles.listItemMetric}>
|
||||||
<Progress strokeWidth={15} percentage={percent} isFallback={isStale} />
|
<Progress
|
||||||
|
strokeWidth={15}
|
||||||
|
percentage={percent}
|
||||||
|
isFallback={isStale}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span className={styles.listItemToggle}>
|
<span className={styles.listItemToggle}>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
@ -54,21 +74,43 @@ const FeatureToggleListItem = ({
|
|||||||
checked={enabled}
|
checked={enabled}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
elseShow={<Switch disabled title={`Toggle ${name}`} key="left-actions" checked={enabled} />}
|
elseShow={
|
||||||
|
<Switch
|
||||||
|
disabled
|
||||||
|
title={`Toggle ${name}`}
|
||||||
|
key="left-actions"
|
||||||
|
checked={enabled}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span className={classnames(styles.listItemLink)}>
|
<span className={classnames(styles.listItemLink)}>
|
||||||
<Link to={featureUrl} className={classnames(commonStyles.listLink, commonStyles.truncate)}>
|
<Link
|
||||||
<span className={commonStyles.toggleName}>{name} </span>
|
to={featureUrl}
|
||||||
|
className={classnames(
|
||||||
|
commonStyles.listLink,
|
||||||
|
commonStyles.truncate
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className={commonStyles.toggleName}>
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
<small>
|
<small>
|
||||||
<TimeAgo date={createdAt} live={false} />
|
<TimeAgo date={createdAt} live={false} />
|
||||||
</small>
|
</small>
|
||||||
<div>
|
<div>
|
||||||
<span className={commonStyles.truncate}><small>{description}</small></span>
|
<span className={commonStyles.truncate}>
|
||||||
|
<small>{description}</small>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
<span className={classnames(styles.listItemStrategies, commonStyles.hideLt920)}>
|
<span
|
||||||
|
className={classnames(
|
||||||
|
styles.listItemStrategies,
|
||||||
|
commonStyles.hideLt920
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Status stale={stale} showActive={false} />
|
<Status stale={stale} showActive={false} />
|
||||||
<FeatureToggleListItemChip type={type} />
|
<FeatureToggleListItemChip type={type} />
|
||||||
</span>
|
</span>
|
||||||
|
@ -145,7 +145,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
aria-disabled={false}
|
aria-disabled={false}
|
||||||
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedSecondary MuiButton-containedSizeLarge MuiButton-sizeLarge"
|
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedSecondary"
|
||||||
data-test="add-feature-btn"
|
data-test="add-feature-btn"
|
||||||
href="/features/create"
|
href="/features/create"
|
||||||
onBlur={[Function]}
|
onBlur={[Function]}
|
||||||
|
@ -74,7 +74,7 @@ const FeatureView = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [features]);
|
}, []);
|
||||||
|
|
||||||
const getTabComponent = key => {
|
const getTabComponent = key => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
|
153
frontend/src/component/feature/ProgressWheel.jsx
Normal file
153
frontend/src/component/feature/ProgressWheel.jsx
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import styles from './progress.module.scss';
|
||||||
|
|
||||||
|
const Progress = ({
|
||||||
|
percentage,
|
||||||
|
strokeWidth = 10,
|
||||||
|
initialAnimation = false,
|
||||||
|
animatePercentageText = false,
|
||||||
|
textForPercentage,
|
||||||
|
colorClassName,
|
||||||
|
isFallback = false,
|
||||||
|
}) => {
|
||||||
|
const [localPercentage, setLocalPercentage] = useState({
|
||||||
|
percentage: initialAnimation ? 0 : percentage,
|
||||||
|
percentageText: initialAnimation ? 0 : percentage,
|
||||||
|
});
|
||||||
|
const timeoutId = useRef();
|
||||||
|
const rafTimerInit = useRef();
|
||||||
|
const rafCounterTimer = useRef();
|
||||||
|
const nextTimer = useRef();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialAnimation) {
|
||||||
|
timeoutId.current = setTimeout(() => {
|
||||||
|
rafTimerInit.current = window.requestAnimationFrame(() => {
|
||||||
|
setLocalPercentage(prev => ({ ...prev, percentage }));
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeoutId.current);
|
||||||
|
clearTimeout(nextTimer);
|
||||||
|
window.cancelAnimationFrame(rafTimerInit.current);
|
||||||
|
window.cancelAnimationFrame(rafCounterTimer.current);
|
||||||
|
};
|
||||||
|
/* eslint-disable-next-line */
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (percentage !== localPercentage) {
|
||||||
|
const nextState = { percentage };
|
||||||
|
if (animatePercentageText) {
|
||||||
|
animateTo(percentage, getTarget(percentage));
|
||||||
|
} else {
|
||||||
|
nextState.percentageText = percentage;
|
||||||
|
}
|
||||||
|
setLocalPercentage(prev => ({ ...prev, ...nextState }));
|
||||||
|
}
|
||||||
|
/* eslint-disable-next-line */
|
||||||
|
}, [percentage]);
|
||||||
|
|
||||||
|
const getTarget = target => {
|
||||||
|
const start = localPercentage.percentageText;
|
||||||
|
const TOTAL_ANIMATION_TIME = 5000;
|
||||||
|
const diff = start > target ? -(start - target) : target - start;
|
||||||
|
const perCycle = TOTAL_ANIMATION_TIME / diff;
|
||||||
|
const cyclesCounter = Math.round(
|
||||||
|
Math.abs(TOTAL_ANIMATION_TIME / perCycle)
|
||||||
|
);
|
||||||
|
const perCycleTime = Math.round(Math.abs(perCycle));
|
||||||
|
|
||||||
|
return {
|
||||||
|
start,
|
||||||
|
target,
|
||||||
|
cyclesCounter,
|
||||||
|
perCycleTime,
|
||||||
|
increment: diff / cyclesCounter,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const animateTo = (percentage, targetState) => {
|
||||||
|
cancelAnimationFrame(rafCounterTimer.current);
|
||||||
|
clearTimeout(nextTimer.current);
|
||||||
|
|
||||||
|
const current = localPercentage.percentageText;
|
||||||
|
|
||||||
|
targetState.cyclesCounter--;
|
||||||
|
if (targetState.cyclesCounter <= 0) {
|
||||||
|
setLocalPercentage({ percentageText: targetState.target });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = Math.round(current + targetState.increment);
|
||||||
|
rafCounterTimer.current = requestAnimationFrame(() => {
|
||||||
|
setLocalPercentage({ percentageText: next });
|
||||||
|
nextTimer.current = setTimeout(() => {
|
||||||
|
animateTo(next, targetState);
|
||||||
|
}, targetState.perCycleTime);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const radius = 50 - strokeWidth / 2;
|
||||||
|
const pathDescription = `
|
||||||
|
M 50,50 m 0,-${radius}
|
||||||
|
a ${radius},${radius} 0 1 1 0,${2 * radius}
|
||||||
|
a ${radius},${radius} 0 1 1 0,-${2 * radius}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const diameter = Math.PI * 2 * radius;
|
||||||
|
const progressStyle = {
|
||||||
|
strokeDasharray: `${diameter}px ${diameter}px`,
|
||||||
|
strokeDashoffset: `${
|
||||||
|
((100 - localPercentage.percentage) / 100) * diameter
|
||||||
|
}px`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return isFallback ? (
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
}
|
||||||
|
<path
|
||||||
|
fill="#E0E0E0"
|
||||||
|
d="M17.3,18C19,16.5 20,14.4 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12C4,14.4 5,16.5 6.7,18C8.2,16.7 10,16 12,16C14,16 15.9,16.7 17.3,18M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M7,9A1,1 0 0,1 8,10A1,1 0 0,1 7,11A1,1 0 0,1 6,10A1,1 0 0,1 7,9M10,6A1,1 0 0,1 11,7A1,1 0 0,1 10,8A1,1 0 0,1 9,7A1,1 0 0,1 10,6M17,9A1,1 0 0,1 18,10A1,1 0 0,1 17,11A1,1 0 0,1 16,10A1,1 0 0,1 17,9M14.4,6.1C14.9,6.3 15.1,6.9 15,7.4L13.6,10.8C13.8,11.1 14,11.5 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12C10,11 10.7,10.1 11.7,10L13.1,6.7C13.3,6.1 13.9,5.9 14.4,6.1Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg viewBox="0 0 100 100">
|
||||||
|
<path
|
||||||
|
className={styles.trail}
|
||||||
|
d={pathDescription}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
fillOpacity={0}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<path
|
||||||
|
className={[styles.path, colorClassName].join(' ')}
|
||||||
|
d={pathDescription}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
fillOpacity={0}
|
||||||
|
style={progressStyle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<text className={styles.text} x={50} y={50}>
|
||||||
|
{localPercentage.percentageText}%
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Progress.propTypes = {
|
||||||
|
percentage: PropTypes.number.isRequired,
|
||||||
|
strokeWidth: PropTypes.number,
|
||||||
|
initialAnimation: PropTypes.bool,
|
||||||
|
animatePercentageText: PropTypes.bool,
|
||||||
|
textForPercentage: PropTypes.func,
|
||||||
|
colorClassName: PropTypes.string,
|
||||||
|
isFallback: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Progress;
|
@ -1,34 +1,42 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import Progress from '../progress-component';
|
import Progress from '../ProgressWheel';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
|
|
||||||
jest.mock('@material-ui/core');
|
jest.mock('@material-ui/core');
|
||||||
|
|
||||||
test('renders correctly with 15% done no fallback', () => {
|
test('renders correctly with 15% done no fallback', () => {
|
||||||
const percent = 15;
|
const percent = 15;
|
||||||
const tree = renderer.create(<Progress strokeWidth={15} percentage={percent} isFallback={false} />);
|
const tree = renderer.create(
|
||||||
|
<Progress strokeWidth={15} percentage={percent} isFallback={false} />
|
||||||
|
);
|
||||||
|
|
||||||
expect(tree).toMatchSnapshot();
|
expect(tree).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('renders correctly with 0% done no fallback', () => {
|
test('renders correctly with 0% done no fallback', () => {
|
||||||
const percent = 0;
|
const percent = 0;
|
||||||
const tree = renderer.create(<Progress strokeWidth={15} percentage={percent} isFallback={false} />);
|
const tree = renderer.create(
|
||||||
|
<Progress strokeWidth={15} percentage={percent} isFallback={false} />
|
||||||
|
);
|
||||||
|
|
||||||
expect(tree).toMatchSnapshot();
|
expect(tree).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('renders correctly with 15% done with fallback', () => {
|
test('renders correctly with 15% done with fallback', () => {
|
||||||
const percent = 15;
|
const percent = 15;
|
||||||
const tree = renderer.create(<Progress strokeWidth={15} percentage={percent} isFallback />);
|
const tree = renderer.create(
|
||||||
|
<Progress strokeWidth={15} percentage={percent} isFallback />
|
||||||
|
);
|
||||||
|
|
||||||
expect(tree).toMatchSnapshot();
|
expect(tree).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('renders correctly with 0% done with fallback', () => {
|
test('renders correctly with 0% done with fallback', () => {
|
||||||
const percent = 0;
|
const percent = 0;
|
||||||
const tree = renderer.create(<Progress strokeWidth={15} percentage={percent} isFallback />);
|
const tree = renderer.create(
|
||||||
|
<Progress strokeWidth={15} percentage={percent} isFallback />
|
||||||
|
);
|
||||||
|
|
||||||
expect(tree).toMatchSnapshot();
|
expect(tree).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
@ -1,147 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import styles from './progress.module.scss';
|
|
||||||
|
|
||||||
class Progress extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
percentage: props.initialAnimation ? 0 : props.percentage,
|
|
||||||
percentageText: props.initialAnimation ? 0 : props.percentage,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (this.props.initialAnimation) {
|
|
||||||
this.initialTimeout = setTimeout(() => {
|
|
||||||
this.rafTimerInit = window.requestAnimationFrame(() => {
|
|
||||||
this.setState({
|
|
||||||
percentage: this.props.percentage,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
UNSAFE_componentWillReceiveProps({ percentage }) {
|
|
||||||
if (this.state.percentage !== percentage) {
|
|
||||||
const nextState = { percentage };
|
|
||||||
if (this.props.animatePercentageText) {
|
|
||||||
this.animateTo(percentage, this.getTarget(percentage));
|
|
||||||
} else {
|
|
||||||
nextState.percentageText = percentage;
|
|
||||||
}
|
|
||||||
this.setState(nextState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getTarget(target) {
|
|
||||||
const start = this.state.percentageText;
|
|
||||||
const TOTAL_ANIMATION_TIME = 5000;
|
|
||||||
const diff = start > target ? -(start - target) : target - start;
|
|
||||||
const perCycle = TOTAL_ANIMATION_TIME / diff;
|
|
||||||
const cyclesCounter = Math.round(Math.abs(TOTAL_ANIMATION_TIME / perCycle));
|
|
||||||
const perCycleTime = Math.round(Math.abs(perCycle));
|
|
||||||
|
|
||||||
return {
|
|
||||||
start,
|
|
||||||
target,
|
|
||||||
cyclesCounter,
|
|
||||||
perCycleTime,
|
|
||||||
increment: diff / cyclesCounter,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
animateTo(percentage, targetState) {
|
|
||||||
cancelAnimationFrame(this.rafCounterTimer);
|
|
||||||
clearTimeout(this.nextTimer);
|
|
||||||
|
|
||||||
const current = this.state.percentageText;
|
|
||||||
|
|
||||||
targetState.cyclesCounter--;
|
|
||||||
if (targetState.cyclesCounter <= 0) {
|
|
||||||
this.setState({ percentageText: targetState.target });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const next = Math.round(current + targetState.increment);
|
|
||||||
this.rafCounterTimer = requestAnimationFrame(() => {
|
|
||||||
this.setState({ percentageText: next });
|
|
||||||
this.nextTimer = setTimeout(() => {
|
|
||||||
this.animateTo(next, targetState);
|
|
||||||
}, targetState.perCycleTime);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
clearTimeout(this.initialTimeout);
|
|
||||||
clearTimeout(this.nextTimer);
|
|
||||||
window.cancelAnimationFrame(this.rafTimerInit);
|
|
||||||
window.cancelAnimationFrame(this.rafCounterTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { strokeWidth, colorClassName, isFallback } = this.props;
|
|
||||||
const radius = 50 - strokeWidth / 2;
|
|
||||||
const pathDescription = `
|
|
||||||
M 50,50 m 0,-${radius}
|
|
||||||
a ${radius},${radius} 0 1 1 0,${2 * radius}
|
|
||||||
a ${radius},${radius} 0 1 1 0,-${2 * radius}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const diameter = Math.PI * 2 * radius;
|
|
||||||
const progressStyle = {
|
|
||||||
strokeDasharray: `${diameter}px ${diameter}px`,
|
|
||||||
strokeDashoffset: `${((100 - this.state.percentage) / 100) * diameter}px`,
|
|
||||||
};
|
|
||||||
|
|
||||||
return isFallback ? (
|
|
||||||
<svg viewBox="0 0 24 24">
|
|
||||||
{
|
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
}
|
|
||||||
<path
|
|
||||||
fill="#E0E0E0"
|
|
||||||
d="M17.3,18C19,16.5 20,14.4 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12C4,14.4 5,16.5 6.7,18C8.2,16.7 10,16 12,16C14,16 15.9,16.7 17.3,18M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M7,9A1,1 0 0,1 8,10A1,1 0 0,1 7,11A1,1 0 0,1 6,10A1,1 0 0,1 7,9M10,6A1,1 0 0,1 11,7A1,1 0 0,1 10,8A1,1 0 0,1 9,7A1,1 0 0,1 10,6M17,9A1,1 0 0,1 18,10A1,1 0 0,1 17,11A1,1 0 0,1 16,10A1,1 0 0,1 17,9M14.4,6.1C14.9,6.3 15.1,6.9 15,7.4L13.6,10.8C13.8,11.1 14,11.5 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12C10,11 10.7,10.1 11.7,10L13.1,6.7C13.3,6.1 13.9,5.9 14.4,6.1Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg viewBox="0 0 100 100">
|
|
||||||
<path className={styles.trail} d={pathDescription} strokeWidth={strokeWidth} fillOpacity={0} />
|
|
||||||
|
|
||||||
<path
|
|
||||||
className={[styles.path, colorClassName].join(' ')}
|
|
||||||
d={pathDescription}
|
|
||||||
strokeWidth={strokeWidth}
|
|
||||||
fillOpacity={0}
|
|
||||||
style={progressStyle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<text className={styles.text} x={50} y={50}>
|
|
||||||
{this.state.percentageText}%
|
|
||||||
</text>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Progress.propTypes = {
|
|
||||||
percentage: PropTypes.number.isRequired,
|
|
||||||
strokeWidth: PropTypes.number,
|
|
||||||
initialAnimation: PropTypes.bool,
|
|
||||||
animatePercentageText: PropTypes.bool,
|
|
||||||
textForPercentage: PropTypes.func,
|
|
||||||
colorClassName: PropTypes.string,
|
|
||||||
isFallback: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
Progress.defaultProps = {
|
|
||||||
strokeWidth: 10,
|
|
||||||
animatePercentageText: false,
|
|
||||||
initialAnimation: false,
|
|
||||||
isFallback: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Progress;
|
|
@ -1,11 +1,11 @@
|
|||||||
.path {
|
.path {
|
||||||
stroke: currentColor;
|
stroke: #5eb89b;
|
||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
transition: stroke-dashoffset 5s ease 0s;
|
transition: stroke-dashoffset 5s ease 0s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trail {
|
.trail {
|
||||||
stroke: currentColor;
|
stroke: #888888;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
|
@ -473,10 +473,10 @@ exports[`renders correctly with with variants 1`] = `
|
|||||||
</svg>
|
</svg>
|
||||||
<fieldset
|
<fieldset
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
className="PrivateNotchedOutline-root-11 MuiOutlinedInput-notchedOutline"
|
className="PrivateNotchedOutline-root-13 MuiOutlinedInput-notchedOutline"
|
||||||
>
|
>
|
||||||
<legend
|
<legend
|
||||||
className="PrivateNotchedOutline-legendLabelled-13 PrivateNotchedOutline-legendNotched-14"
|
className="PrivateNotchedOutline-legendLabelled-15 PrivateNotchedOutline-legendNotched-16"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
Stickiness
|
Stickiness
|
||||||
|
@ -139,10 +139,10 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
</svg>
|
</svg>
|
||||||
<fieldset
|
<fieldset
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
className="PrivateNotchedOutline-root-11 MuiOutlinedInput-notchedOutline"
|
className="PrivateNotchedOutline-root-13 MuiOutlinedInput-notchedOutline"
|
||||||
>
|
>
|
||||||
<legend
|
<legend
|
||||||
className="PrivateNotchedOutline-legendLabelled-13"
|
className="PrivateNotchedOutline-legendLabelled-15"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
Project
|
Project
|
||||||
@ -174,7 +174,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-disabled={false}
|
aria-disabled={false}
|
||||||
className="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-15 MuiSwitch-switchBase MuiSwitch-colorSecondary"
|
className="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-17 MuiSwitch-switchBase MuiSwitch-colorSecondary"
|
||||||
onBlur={[Function]}
|
onBlur={[Function]}
|
||||||
onDragLeave={[Function]}
|
onDragLeave={[Function]}
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
@ -193,7 +193,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
checked={false}
|
checked={false}
|
||||||
className="PrivateSwitchBase-input-18 MuiSwitch-input"
|
className="PrivateSwitchBase-input-20 MuiSwitch-input"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@ -317,7 +317,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div
|
<div
|
||||||
className="MuiPaper-root makeStyles-tabNav-19 MuiPaper-elevation1 MuiPaper-rounded"
|
className="MuiPaper-root makeStyles-tabNav-21 MuiPaper-elevation1 MuiPaper-rounded"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="MuiTabs-root"
|
className="MuiTabs-root"
|
||||||
@ -365,7 +365,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
Activation
|
Activation
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className="PrivateTabIndicator-root-20 PrivateTabIndicator-colorPrimary-21 MuiTabs-indicator"
|
className="PrivateTabIndicator-root-22 PrivateTabIndicator-colorPrimary-23 MuiTabs-indicator"
|
||||||
style={Object {}}
|
style={Object {}}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
@ -6,7 +6,7 @@ import LinkIcon from '@material-ui/icons/Link';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { AppsLinkList, calc } from '../../common';
|
import { AppsLinkList, calc } from '../../common';
|
||||||
import { formatFullDateTimeWithLocale } from '../../common/util';
|
import { formatFullDateTimeWithLocale } from '../../common/util';
|
||||||
import Progress from '../progress-component';
|
import Progress from '../ProgressWheel';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
import styles from './metric.module.scss';
|
import styles from './metric.module.scss';
|
||||||
@ -55,7 +55,8 @@ export default class MetricComponent extends React.Component {
|
|||||||
formatFullDateTime(v) {
|
formatFullDateTime(v) {
|
||||||
return formatFullDateTimeWithLocale(v, this.props.location.locale);
|
return formatFullDateTimeWithLocale(v, this.props.location.locale);
|
||||||
}
|
}
|
||||||
renderLastSeen = lastSeenAt => (lastSeenAt ? this.formatFullDateTime(lastSeenAt) : 'Never reported');
|
renderLastSeen = lastSeenAt =>
|
||||||
|
lastSeenAt ? this.formatFullDateTime(lastSeenAt) : 'Never reported';
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { metrics = {}, featureToggle } = this.props;
|
const { metrics = {}, featureToggle } = this.props;
|
||||||
@ -65,12 +66,19 @@ export default class MetricComponent extends React.Component {
|
|||||||
seenApps = [],
|
seenApps = [],
|
||||||
} = metrics;
|
} = metrics;
|
||||||
|
|
||||||
const lastHourPercent = 1 * calc(lastHour.yes, lastHour.yes + lastHour.no, 0);
|
const lastHourPercent =
|
||||||
const lastMinutePercent = 1 * calc(lastMinute.yes, lastMinute.yes + lastMinute.no, 0);
|
1 * calc(lastHour.yes, lastHour.yes + lastHour.no, 0);
|
||||||
|
const lastMinutePercent =
|
||||||
|
1 * calc(lastMinute.yes, lastMinute.yes + lastMinute.no, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '16px', flexGrow: 1 }}>
|
<div style={{ padding: '16px', flexGrow: 1 }}>
|
||||||
<Grid container spacing={2} justify="center" className={styles.grid}>
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={2}
|
||||||
|
justify="center"
|
||||||
|
className={styles.grid}
|
||||||
|
>
|
||||||
<Grid item xs={12} sm={4}>
|
<Grid item xs={12} sm={4}>
|
||||||
<Progress
|
<Progress
|
||||||
percentage={lastMinutePercent}
|
percentage={lastMinutePercent}
|
||||||
@ -83,13 +91,17 @@ export default class MetricComponent extends React.Component {
|
|||||||
elseShow={
|
elseShow={
|
||||||
<p>
|
<p>
|
||||||
<strong>Last minute</strong>
|
<strong>Last minute</strong>
|
||||||
<br /> Yes {lastMinute.yes}, No: {lastMinute.no}
|
<br /> Yes {lastMinute.yes}, No:{' '}
|
||||||
|
{lastMinute.no}
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={4}>
|
<Grid item xs={12} sm={4}>
|
||||||
<Progress percentage={lastHourPercent} isFallback={lastHour.isFallback} />
|
<Progress
|
||||||
|
percentage={lastHourPercent}
|
||||||
|
isFallback={lastHour.isFallback}
|
||||||
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={lastHour.isFallback}
|
condition={lastHour.isFallback}
|
||||||
show={<p>No metrics available</p>}
|
show={<p>No metrics available</p>}
|
||||||
@ -111,13 +123,20 @@ export default class MetricComponent extends React.Component {
|
|||||||
}
|
}
|
||||||
elseShow={
|
elseShow={
|
||||||
<div>
|
<div>
|
||||||
<Icon className={styles.problemIcon} title="Not used in an app in the last hour">
|
<Icon
|
||||||
|
className={styles.problemIcon}
|
||||||
|
title="Not used in an app in the last hour"
|
||||||
|
>
|
||||||
report problem
|
report problem
|
||||||
</Icon>
|
</Icon>
|
||||||
<div>
|
<div>
|
||||||
<small>
|
<small>
|
||||||
<strong>Not used in an app in the last hour. </strong>
|
<strong>
|
||||||
This might be due to your client implementation not reporting usage.
|
Not used in an app in the last
|
||||||
|
hour.{' '}
|
||||||
|
</strong>
|
||||||
|
This might be due to your client
|
||||||
|
implementation not reporting usage.
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -131,14 +150,20 @@ export default class MetricComponent extends React.Component {
|
|||||||
show={
|
show={
|
||||||
<>
|
<>
|
||||||
<strong>Created: </strong>
|
<strong>Created: </strong>
|
||||||
<span>{this.formatFullDateTime(featureToggle.createdAt)}</span>
|
<span>
|
||||||
|
{this.formatFullDateTime(
|
||||||
|
featureToggle.createdAt
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
<strong>Last seen: </strong>
|
<strong>Last seen: </strong>
|
||||||
<span>{this.renderLastSeen(featureToggle.lastSeenAt)}</span>
|
<span>
|
||||||
|
{this.renderLastSeen(featureToggle.lastSeenAt)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -2,11 +2,31 @@ import ConditionallyRender from '../../common/ConditionallyRender';
|
|||||||
import MainLayout from '../MainLayout/MainLayout';
|
import MainLayout from '../MainLayout/MainLayout';
|
||||||
|
|
||||||
const LayoutPicker = ({ children, location }) => {
|
const LayoutPicker = ({ children, location }) => {
|
||||||
const isLoginPage = location.pathname.includes('login');
|
const standalonePages = () => {
|
||||||
|
const isLoginPage = location.pathname.includes('login');
|
||||||
|
const isNewUserPage = location.pathname.includes('new-user');
|
||||||
|
const isChangePasswordPage = location.pathname.includes(
|
||||||
|
'reset-password'
|
||||||
|
);
|
||||||
|
const isResetPasswordSuccessPage = location.pathname.includes(
|
||||||
|
'reset-password-success'
|
||||||
|
);
|
||||||
|
const isForgottenPasswordPage = location.pathname.includes(
|
||||||
|
'forgotten-password'
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
isLoginPage ||
|
||||||
|
isNewUserPage ||
|
||||||
|
isChangePasswordPage ||
|
||||||
|
isResetPasswordSuccessPage ||
|
||||||
|
isForgottenPasswordPage
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={isLoginPage}
|
condition={standalonePages()}
|
||||||
show={children}
|
show={children}
|
||||||
elseShow={<MainLayout location={location}>{children}</MainLayout>}
|
elseShow={<MainLayout location={location}>{children}</MainLayout>}
|
||||||
/>
|
/>
|
||||||
|
@ -104,304 +104,3 @@ Array [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`returns all defined routes 1`] = `
|
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"layout": "main",
|
|
||||||
"parent": "/features",
|
|
||||||
"path": "/features/create",
|
|
||||||
"title": "Create",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"layout": "main",
|
|
||||||
"parent": "/features",
|
|
||||||
"path": "/features/copy/:copyToggle",
|
|
||||||
"title": "Copy",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"layout": "main",
|
|
||||||
"parent": "/features",
|
|
||||||
"path": "/features/:activeTab/:name",
|
|
||||||
"title": ":name",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"icon": "list",
|
|
||||||
"layout": "main",
|
|
||||||
"path": "/features",
|
|
||||||
"title": "Feature Toggles",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"layout": "main",
|
|
||||||
"parent": "/strategies",
|
|
||||||
"path": "/strategies/create",
|
|
||||||
"title": "Create",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"layout": "main",
|
|
||||||
"parent": "/strategies",
|
|
||||||
"path": "/strategies/:activeTab/:strategyName",
|
|
||||||
"title": ":strategyName",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"icon": "extension",
|
|
||||||
"layout": "main",
|
|
||||||
"path": "/strategies",
|
|
||||||
"title": "Strategies",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"layout": "main",
|
|
||||||
"parent": "/history",
|
|
||||||
"path": "/history/:toggleName",
|
|
||||||
"title": ":toggleName",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"icon": "history",
|
|
||||||
"layout": "main",
|
|
||||||
"path": "/history",
|
|
||||||
"title": "Event History",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"layout": "main",
|
|
||||||
"parent": "/archive",
|
|
||||||
"path": "/archive/:activeTab/:name",
|
|
||||||
"title": ":name",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"icon": "archive",
|
|
||||||
"layout": "main",
|
|
||||||
"path": "/archive",
|
|
||||||
"title": "Archived Toggles",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"layout": "main",
|
|
||||||
"parent": "/applications",
|
|
||||||
"path": "/applications/:name",
|
|
||||||
"title": ":name",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"icon": "apps",
|
|
||||||
"layout": "main",
|
|
||||||
"path": "/applications",
|
|
||||||
"title": "Applications",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"layout": "main",
|
|
||||||
"parent": "/context",
|
|
||||||
"path": "/context/create",
|
|
||||||
"title": "Create",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"layout": "main",
|
|
||||||
"parent": "/context",
|
|
||||||
"path": "/context/edit/:name",
|
|
||||||
"title": ":name",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"flag": "C",
|
|
||||||
"icon": "album",
|
|
||||||
"layout": "main",
|
|
||||||
"path": "/context",
|
|
||||||
"title": "Context Fields",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"layout": "main",
|
|
||||||
"parent": "/projects",
|
|
||||||
"path": "/projects/create",
|
|
||||||
"title": "Create",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"layout": "main",
|
|
||||||
"parent": "/projects",
|
|
||||||
"path": "/projects/edit/:id",
|
|
||||||
"title": ":id",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"layout": "main",
|
|
||||||
"parent": "/projects",
|
|
||||||
"path": "/projects/:id/access",
|
|
||||||
"title": ":id",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"flag": "P",
|
|
||||||
"icon": "folder_open",
|
|
||||||
"layout": "main",
|
|
||||||
"path": "/projects",
|
|
||||||
"title": "Projects",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"layout": "main",
|
|
||||||
"parent": "/tag-types",
|
|
||||||
"path": "/tag-types/create",
|
|
||||||
"title": "Create",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"layout": "main",
|
|
||||||
"parent": "/tag-types",
|
|
||||||
"path": "/tag-types/edit/:name",
|
|
||||||
"title": ":name",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"icon": "label",
|
|
||||||
"layout": "main",
|
|
||||||
"path": "/tag-types",
|
|
||||||
"title": "Tag types",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"layout": "main",
|
|
||||||
"parent": "/tags",
|
|
||||||
"path": "/tags/create",
|
|
||||||
"title": "Create",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"hidden": true,
|
|
||||||
"icon": "label",
|
|
||||||
"layout": "main",
|
|
||||||
"path": "/tags",
|
|
||||||
"title": "Tags",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"layout": "main",
|
|
||||||
"parent": "/addons",
|
|
||||||
"path": "/addons/create/:provider",
|
|
||||||
"title": "Create",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"layout": "main",
|
|
||||||
"parent": "/addons",
|
|
||||||
"path": "/addons/edit/:id",
|
|
||||||
"title": "Edit",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"hidden": false,
|
|
||||||
"icon": "device_hub",
|
|
||||||
"layout": "main",
|
|
||||||
"path": "/addons",
|
|
||||||
"title": "Addons",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"icon": "report",
|
|
||||||
"layout": "main",
|
|
||||||
"path": "/reporting",
|
|
||||||
"title": "Reporting",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"icon": "exit_to_app",
|
|
||||||
"layout": "main",
|
|
||||||
"path": "/logout",
|
|
||||||
"title": "Sign out",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": Object {
|
|
||||||
"$$typeof": Symbol(react.memo),
|
|
||||||
"WrappedComponent": [Function],
|
|
||||||
"compare": null,
|
|
||||||
"type": [Function],
|
|
||||||
},
|
|
||||||
"hidden": true,
|
|
||||||
"icon": "user",
|
|
||||||
"layout": "standalone",
|
|
||||||
"path": "/login",
|
|
||||||
"title": "Log in",
|
|
||||||
"type": "unprotected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"layout": "main",
|
|
||||||
"parent": "/admin",
|
|
||||||
"path": "/admin/api",
|
|
||||||
"title": "API access",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"layout": "main",
|
|
||||||
"parent": "/admin",
|
|
||||||
"path": "/admin/users",
|
|
||||||
"title": "Users",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": Object {
|
|
||||||
"$$typeof": Symbol(react.memo),
|
|
||||||
"WrappedComponent": [Function],
|
|
||||||
"compare": null,
|
|
||||||
"type": [Function],
|
|
||||||
},
|
|
||||||
"layout": "main",
|
|
||||||
"parent": "/admin",
|
|
||||||
"path": "/admin/auth",
|
|
||||||
"title": "Authentication",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"hidden": false,
|
|
||||||
"icon": "album",
|
|
||||||
"layout": "main",
|
|
||||||
"path": "/admin",
|
|
||||||
"title": "Admin",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
@ -1,9 +1,4 @@
|
|||||||
import { routes, baseRoutes, getRoute } from '../routes';
|
import { baseRoutes, getRoute } from '../routes';
|
||||||
|
|
||||||
test('returns all defined routes', () => {
|
|
||||||
expect(routes.length).toEqual(35);
|
|
||||||
expect(routes).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns all baseRoutes', () => {
|
test('returns all baseRoutes', () => {
|
||||||
expect(baseRoutes.length).toEqual(12);
|
expect(baseRoutes.length).toEqual(12);
|
||||||
|
@ -34,6 +34,9 @@ import AdminAuth from '../../page/admin/auth';
|
|||||||
import Reporting from '../../page/reporting';
|
import Reporting from '../../page/reporting';
|
||||||
import Login from '../user/Login';
|
import Login from '../user/Login';
|
||||||
import { P, C } from '../common/flags';
|
import { P, C } from '../common/flags';
|
||||||
|
import NewUser from '../user/NewUser/NewUser';
|
||||||
|
import ResetPassword from '../user/ResetPassword/ResetPassword';
|
||||||
|
import ForgottenPassword from '../user/ForgottenPassword/ForgottenPassword';
|
||||||
|
|
||||||
export const routes = [
|
export const routes = [
|
||||||
// Features
|
// Features
|
||||||
@ -307,40 +310,64 @@ export const routes = [
|
|||||||
hidden: true,
|
hidden: true,
|
||||||
layout: 'standalone',
|
layout: 'standalone',
|
||||||
},
|
},
|
||||||
// Admin
|
// Admin
|
||||||
{
|
{
|
||||||
path: '/admin/api',
|
path: '/admin/api',
|
||||||
parent: '/admin',
|
parent: '/admin',
|
||||||
title: 'API access',
|
title: 'API access',
|
||||||
component: AdminApi,
|
component: AdminApi,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
layout: 'main',
|
layout: 'main',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/users',
|
path: '/admin/users',
|
||||||
parent: '/admin',
|
parent: '/admin',
|
||||||
title: 'Users',
|
title: 'Users',
|
||||||
component: AdminUsers,
|
component: AdminUsers,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
layout: 'main',
|
layout: 'main',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/auth',
|
path: '/admin/auth',
|
||||||
parent: '/admin',
|
parent: '/admin',
|
||||||
title: 'Authentication',
|
title: 'Authentication',
|
||||||
component: AdminAuth,
|
component: AdminAuth,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
layout: 'main',
|
layout: 'main',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin',
|
path: '/admin',
|
||||||
title: 'Admin',
|
title: 'Admin',
|
||||||
icon: 'album',
|
icon: 'album',
|
||||||
component: Admin,
|
component: Admin,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
layout: 'main',
|
layout: 'main',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/new-user',
|
||||||
|
title: 'New user',
|
||||||
|
hidden: true,
|
||||||
|
component: NewUser,
|
||||||
|
type: 'unprotected',
|
||||||
|
layout: 'standalone',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/reset-password',
|
||||||
|
title: 'reset-password',
|
||||||
|
hidden: true,
|
||||||
|
component: ResetPassword,
|
||||||
|
type: 'unprotected',
|
||||||
|
layout: 'standalone',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/forgotten-password',
|
||||||
|
title: 'reset-password',
|
||||||
|
hidden: true,
|
||||||
|
component: ForgottenPassword,
|
||||||
|
type: 'unprotected',
|
||||||
|
layout: 'standalone',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const getRoute = path => routes.find(route => route.path === path);
|
export const getRoute = path => routes.find(route => route.path === path);
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
import { makeStyles } from '@material-ui/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles({
|
||||||
|
container: {
|
||||||
|
maxWidth: '300px',
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
width: '150px',
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
display: 'block',
|
||||||
|
margin: '0.5rem 0',
|
||||||
|
},
|
||||||
|
});
|
@ -0,0 +1,112 @@
|
|||||||
|
import { Button, TextField, Typography } from '@material-ui/core';
|
||||||
|
import { AlertTitle, Alert } from '@material-ui/lab';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { SyntheticEvent, useState } from 'react';
|
||||||
|
import { useCommonStyles } from '../../../common.styles';
|
||||||
|
import useLoading from '../../../hooks/useLoading';
|
||||||
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
|
import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout';
|
||||||
|
import { useStyles } from './ForgottenPassword.styles';
|
||||||
|
|
||||||
|
const ForgottenPassword = () => {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [attempted, setAttempted] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [attemptedEmail, setAttemptedEmail] = useState('');
|
||||||
|
const commonStyles = useCommonStyles();
|
||||||
|
const styles = useStyles();
|
||||||
|
const ref = useLoading(loading);
|
||||||
|
|
||||||
|
const onClick = async (e: SyntheticEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setAttemptedEmail(email);
|
||||||
|
|
||||||
|
await fetch('auth/reset/password-email', {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ email }),
|
||||||
|
});
|
||||||
|
|
||||||
|
setAttempted(true);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StandaloneLayout>
|
||||||
|
<div
|
||||||
|
className={classnames(
|
||||||
|
commonStyles.contentSpacingY,
|
||||||
|
commonStyles.flexColumn
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="h2"
|
||||||
|
className={commonStyles.title}
|
||||||
|
data-loading
|
||||||
|
>
|
||||||
|
Forgotten password
|
||||||
|
</Typography>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={attempted}
|
||||||
|
show={
|
||||||
|
<Alert severity="success" data-loading>
|
||||||
|
<AlertTitle>Attempted to send email</AlertTitle>
|
||||||
|
We've attempted to send a reset password email to:
|
||||||
|
<strong className={styles.email}>
|
||||||
|
{attemptedEmail}
|
||||||
|
</strong>
|
||||||
|
If you did not receive an email, please verify that
|
||||||
|
you typed in the correct email, and contact your
|
||||||
|
administrator to make sure that you are in the
|
||||||
|
system.
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<form
|
||||||
|
onSubmit={onClick}
|
||||||
|
className={classnames(
|
||||||
|
commonStyles.contentSpacingY,
|
||||||
|
commonStyles.flexColumn,
|
||||||
|
styles.container
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Typography variant="body1" data-loading>
|
||||||
|
Please provide your email address. If it exists in the
|
||||||
|
system we'll send a new reset link.
|
||||||
|
</Typography>
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
placeholder="email"
|
||||||
|
type="email"
|
||||||
|
data-loading
|
||||||
|
value={email}
|
||||||
|
onChange={e => {
|
||||||
|
setEmail(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
type="submit"
|
||||||
|
data-loading
|
||||||
|
color="primary"
|
||||||
|
className={styles.button}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={!attempted}
|
||||||
|
show={<span>Submit</span>}
|
||||||
|
elseShow={<span>Try again</span>}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</StandaloneLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ForgottenPassword;
|
@ -1,62 +1,46 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import classnames from 'classnames';
|
|
||||||
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
|
||||||
import { useTheme } from '@material-ui/core/styles';
|
|
||||||
import { Typography } from '@material-ui/core';
|
|
||||||
|
|
||||||
import AuthenticationContainer from '../authentication-container';
|
import AuthenticationContainer from '../authentication-container';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
|
|
||||||
import { ReactComponent as UnleashLogo } from '../../../icons/unleash-logo-inverted.svg';
|
|
||||||
import { ReactComponent as SwitchesSVG } from '../../../icons/switches.svg';
|
|
||||||
import { useStyles } from './Login.styles';
|
import { useStyles } from './Login.styles';
|
||||||
|
import useQueryParams from '../../../hooks/useQueryParams';
|
||||||
|
import ResetPasswordSuccess from '../common/ResetPasswordSuccess/ResetPasswordSuccess';
|
||||||
|
import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout';
|
||||||
|
|
||||||
const Login = ({ history, loadInitialData, isUnauthorized, authDetails }) => {
|
const Login = ({ history, loadInitialData, isUnauthorized, authDetails }) => {
|
||||||
const theme = useTheme();
|
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const smallScreen = useMediaQuery(theme.breakpoints.up('md'));
|
const query = useQueryParams();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isUnauthorized()) {
|
if (isUnauthorized()) {
|
||||||
loadInitialData();
|
loadInitialData();
|
||||||
} else {
|
}
|
||||||
|
/* eslint-disable-next-line */
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isUnauthorized()) {
|
||||||
history.push('features');
|
history.push('features');
|
||||||
}
|
}
|
||||||
/* eslint-disable-next-line */
|
/* eslint-disable-next-line */
|
||||||
}, [authDetails]);
|
}, [authDetails]);
|
||||||
|
|
||||||
|
const resetPassword = query.get('reset') === 'true';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.loginContainer}>
|
<StandaloneLayout>
|
||||||
<div className={classnames(styles.container)}>
|
<div>
|
||||||
<div
|
<h2 className={styles.title}>Login</h2>
|
||||||
className={classnames(
|
<ConditionallyRender
|
||||||
styles.contentContainer,
|
condition={resetPassword}
|
||||||
styles.gradient
|
show={<ResetPasswordSuccess />}
|
||||||
)}
|
/>
|
||||||
>
|
<div className={styles.loginFormContainer}>
|
||||||
<h1 className={styles.title}>
|
<AuthenticationContainer history={history} />
|
||||||
<UnleashLogo className={styles.logo} /> Unleash
|
|
||||||
</h1>
|
|
||||||
<Typography variant="body1" className={styles.subTitle}>
|
|
||||||
Committed to creating new ways of developing
|
|
||||||
</Typography>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={smallScreen}
|
|
||||||
show={
|
|
||||||
<div className={styles.imageContainer}>
|
|
||||||
<SwitchesSVG />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={styles.contentContainer}>
|
|
||||||
<h2 className={styles.title}>Login</h2>
|
|
||||||
<div className={styles.loginFormContainer}>
|
|
||||||
<AuthenticationContainer history={history} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</StandaloneLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
color: theme.palette.login.main,
|
color: theme.palette.login.main,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: '1.5rem',
|
fontSize: theme.fontSizes.mainHeader,
|
||||||
marginBottom: '0.5rem',
|
marginBottom: '0.5rem',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
29
frontend/src/component/user/NewUser/NewUser.styles.ts
Normal file
29
frontend/src/component/user/NewUser/NewUser.styles.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { makeStyles } from '@material-ui/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
container: {
|
||||||
|
display: 'flex',
|
||||||
|
},
|
||||||
|
roleContainer: {
|
||||||
|
marginTop: '2rem',
|
||||||
|
},
|
||||||
|
innerContainer: {
|
||||||
|
width: '60%',
|
||||||
|
minHeight: '100vh',
|
||||||
|
padding: '4rem 3rem',
|
||||||
|
},
|
||||||
|
buttonContainer: {
|
||||||
|
display: 'flex',
|
||||||
|
marginTop: '1rem',
|
||||||
|
},
|
||||||
|
primaryBtn: {
|
||||||
|
marginRight: '8px',
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
marginBottom: '0.5rem',
|
||||||
|
fontSize: '1.1rem',
|
||||||
|
},
|
||||||
|
emailField: {
|
||||||
|
minWidth: '300px',
|
||||||
|
},
|
||||||
|
}));
|
94
frontend/src/component/user/NewUser/NewUser.tsx
Normal file
94
frontend/src/component/user/NewUser/NewUser.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import useLoading from '../../../hooks/useLoading';
|
||||||
|
import { TextField, Typography } from '@material-ui/core';
|
||||||
|
|
||||||
|
import StandaloneBanner from '../StandaloneBanner/StandaloneBanner';
|
||||||
|
import ResetPasswordDetails from '../common/ResetPasswordDetails/ResetPasswordDetails';
|
||||||
|
|
||||||
|
import { useStyles } from './NewUser.styles';
|
||||||
|
import { useCommonStyles } from '../../../common.styles';
|
||||||
|
import useResetPassword from '../../../hooks/useResetPassword';
|
||||||
|
import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout';
|
||||||
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
|
import InvalidToken from '../common/InvalidToken/InvalidToken';
|
||||||
|
|
||||||
|
const NewUser = () => {
|
||||||
|
const {
|
||||||
|
token,
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
setLoading,
|
||||||
|
invalidToken,
|
||||||
|
} = useResetPassword();
|
||||||
|
const ref = useLoading(loading);
|
||||||
|
const commonStyles = useCommonStyles();
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref}>
|
||||||
|
<StandaloneLayout
|
||||||
|
showMenu={false}
|
||||||
|
BannerComponent={
|
||||||
|
<StandaloneBanner showStars title={'Welcome to Unleash'}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={data?.createdBy}
|
||||||
|
show={
|
||||||
|
<Typography variant="body1">
|
||||||
|
You have been invited by {data?.createdBy}
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StandaloneBanner>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={invalidToken}
|
||||||
|
show={<InvalidToken />}
|
||||||
|
elseShow={
|
||||||
|
<ResetPasswordDetails
|
||||||
|
token={token}
|
||||||
|
setLoading={setLoading}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
data-loading
|
||||||
|
variant="subtitle1"
|
||||||
|
className={styles.subtitle}
|
||||||
|
>
|
||||||
|
Your username is
|
||||||
|
</Typography>
|
||||||
|
<TextField
|
||||||
|
data-loading
|
||||||
|
value={data?.email}
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
className={styles.emailField}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<div className={styles.roleContainer}>
|
||||||
|
<Typography
|
||||||
|
data-loading
|
||||||
|
variant="subtitle1"
|
||||||
|
className={styles.subtitle}
|
||||||
|
>
|
||||||
|
In Unleash your role is:{' '}
|
||||||
|
<i>{data?.role?.name}</i>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" data-loading>
|
||||||
|
{data?.role?.description}
|
||||||
|
</Typography>
|
||||||
|
<div
|
||||||
|
className={commonStyles.largeDivider}
|
||||||
|
data-loading
|
||||||
|
/>
|
||||||
|
<Typography variant="body1" data-loading>
|
||||||
|
Set a password for your account.
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</ResetPasswordDetails>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StandaloneLayout>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewUser;
|
@ -0,0 +1,13 @@
|
|||||||
|
import { makeStyles } from '@material-ui/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
container: {
|
||||||
|
display: 'flex',
|
||||||
|
},
|
||||||
|
innerContainer: { width: '40%', minHeight: '100vh' },
|
||||||
|
title: {
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: '1.2rem',
|
||||||
|
marginBottom: '1rem',
|
||||||
|
},
|
||||||
|
}));
|
43
frontend/src/component/user/ResetPassword/ResetPassword.tsx
Normal file
43
frontend/src/component/user/ResetPassword/ResetPassword.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import useLoading from '../../../hooks/useLoading';
|
||||||
|
|
||||||
|
import ResetPasswordDetails from '../common/ResetPasswordDetails/ResetPasswordDetails';
|
||||||
|
|
||||||
|
import { useStyles } from './ResetPassword.styles';
|
||||||
|
import { Typography } from '@material-ui/core';
|
||||||
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
|
import InvalidToken from '../common/InvalidToken/InvalidToken';
|
||||||
|
import useResetPassword from '../../../hooks/useResetPassword';
|
||||||
|
import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout';
|
||||||
|
|
||||||
|
const ResetPassword = () => {
|
||||||
|
const styles = useStyles();
|
||||||
|
const { token, loading, setLoading, invalidToken } = useResetPassword();
|
||||||
|
const ref = useLoading(loading);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref}>
|
||||||
|
<StandaloneLayout>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={invalidToken}
|
||||||
|
show={<InvalidToken />}
|
||||||
|
elseShow={
|
||||||
|
<ResetPasswordDetails
|
||||||
|
token={token}
|
||||||
|
setLoading={setLoading}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="h2"
|
||||||
|
className={styles.title}
|
||||||
|
data-loading
|
||||||
|
>
|
||||||
|
Reset password
|
||||||
|
</Typography>
|
||||||
|
</ResetPasswordDetails>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StandaloneLayout>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ResetPassword;
|
@ -0,0 +1,49 @@
|
|||||||
|
import { makeStyles } from '@material-ui/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
title: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '1.2rem',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: '1rem',
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
padding: '4rem 2rem',
|
||||||
|
color: '#fff',
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
switchesContainer: {
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: '40px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
|
switchIcon: {
|
||||||
|
height: '180px',
|
||||||
|
},
|
||||||
|
bottomStar: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '-54px',
|
||||||
|
left: '100px',
|
||||||
|
},
|
||||||
|
bottomRightStar: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '-100px',
|
||||||
|
left: '200px',
|
||||||
|
},
|
||||||
|
midRightStar: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '-80px',
|
||||||
|
left: '300px',
|
||||||
|
},
|
||||||
|
midLeftStar: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '10px',
|
||||||
|
left: '150px',
|
||||||
|
},
|
||||||
|
midLeftStarTwo: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '25px',
|
||||||
|
left: '350px',
|
||||||
|
},
|
||||||
|
}));
|
@ -0,0 +1,55 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
import { Typography, useTheme } from '@material-ui/core';
|
||||||
|
import Gradient from '../../common/Gradient/Gradient';
|
||||||
|
import { ReactComponent as StarIcon } from '../../../icons/star.svg';
|
||||||
|
import { ReactComponent as RightToggleIcon } from '../../../icons/toggleRight.svg';
|
||||||
|
import { ReactComponent as LeftToggleIcon } from '../../../icons/toggleLeft.svg';
|
||||||
|
|
||||||
|
import { useStyles } from './StandaloneBanner.styles';
|
||||||
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
|
|
||||||
|
interface IStandaloneBannerProps {
|
||||||
|
showStars?: boolean;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StandaloneBanner: FC<IStandaloneBannerProps> = ({
|
||||||
|
showStars = false,
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = useStyles();
|
||||||
|
return (
|
||||||
|
<Gradient from={theme.palette.primary.main} to={'#173341'}>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<Typography variant="h1" className={styles.title}>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={showStars}
|
||||||
|
show={
|
||||||
|
<>
|
||||||
|
<StarIcon className={styles.midLeftStarTwo} />
|
||||||
|
<StarIcon className={styles.midLeftStar} />
|
||||||
|
<StarIcon className={styles.midRightStar} />
|
||||||
|
<StarIcon className={styles.bottomRightStar} />
|
||||||
|
<StarIcon className={styles.bottomStar} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.switchesContainer}>
|
||||||
|
<RightToggleIcon className={styles.switchIcon} />
|
||||||
|
<br></br>
|
||||||
|
<LeftToggleIcon className={styles.switchIcon} />
|
||||||
|
</div>
|
||||||
|
</Gradient>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StandaloneBanner;
|
@ -0,0 +1,29 @@
|
|||||||
|
import { Button, Typography } from '@material-ui/core';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useCommonStyles } from '../../../../common.styles';
|
||||||
|
|
||||||
|
const InvalidToken = () => {
|
||||||
|
const commonStyles = useCommonStyles();
|
||||||
|
return (
|
||||||
|
<div className={commonStyles.contentSpacingY}>
|
||||||
|
<Typography variant="h2" className={commonStyles.title}>
|
||||||
|
Invalid token
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="subtitle1">
|
||||||
|
Your token has either been used to reset your password, or it
|
||||||
|
has expired. Please request a new reset password URL in order to
|
||||||
|
reset your password.
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
component={Link}
|
||||||
|
to="forgotten-password"
|
||||||
|
>
|
||||||
|
Reset password
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InvalidToken;
|
@ -0,0 +1,22 @@
|
|||||||
|
import { FC, Dispatch, SetStateAction } from 'react';
|
||||||
|
import ResetPasswordForm from '../ResetPasswordForm/ResetPasswordForm';
|
||||||
|
|
||||||
|
interface IResetPasswordDetails {
|
||||||
|
token: string;
|
||||||
|
setLoading: Dispatch<SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResetPasswordDetails: FC<IResetPasswordDetails> = ({
|
||||||
|
children,
|
||||||
|
token,
|
||||||
|
setLoading,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{children}
|
||||||
|
<ResetPasswordForm token={token} setLoading={setLoading} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ResetPasswordDetails;
|
@ -0,0 +1,14 @@
|
|||||||
|
import { Alert, AlertTitle } from '@material-ui/lab';
|
||||||
|
|
||||||
|
const ResetPasswordError = () => {
|
||||||
|
return (
|
||||||
|
<Alert severity="error" data-loading>
|
||||||
|
<AlertTitle>Unable to reset password</AlertTitle>
|
||||||
|
Something went wrong when attempting to update your password. This
|
||||||
|
could be due to unstable internet connectivity. If retrying the
|
||||||
|
request does not work, please try again later.
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ResetPasswordError;
|
@ -0,0 +1,43 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
container: {
|
||||||
|
border: '1px solid #f1f1f1',
|
||||||
|
borderRadius: '3px',
|
||||||
|
right: '100px',
|
||||||
|
color: '#44606e',
|
||||||
|
},
|
||||||
|
headerContainer: { display: 'flex', padding: '0.5rem' },
|
||||||
|
divider: {
|
||||||
|
backgroundColor: theme.palette.borders?.main,
|
||||||
|
height: '1px',
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
checkContainer: {
|
||||||
|
width: '95px',
|
||||||
|
margin: '0 0.25rem',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
statusBarContainer: {
|
||||||
|
display: 'flex',
|
||||||
|
padding: '0.5rem',
|
||||||
|
},
|
||||||
|
statusBar: {
|
||||||
|
width: '50px',
|
||||||
|
borderRadius: '3px',
|
||||||
|
backgroundColor: 'red',
|
||||||
|
height: '6px',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
marginBottom: '0',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
statusBarSuccess: {
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
helpIcon: {
|
||||||
|
height: '17.5px',
|
||||||
|
},
|
||||||
|
}));
|
@ -0,0 +1,184 @@
|
|||||||
|
import { Tooltip, Typography } from '@material-ui/core';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||||
|
import { BAD_REQUEST, OK } from '../../../../../constants/statusCodes';
|
||||||
|
import { useStyles } from './PasswordChecker.styles';
|
||||||
|
import HelpIcon from '@material-ui/icons/Help';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
interface IPasswordCheckerProps {
|
||||||
|
password: string;
|
||||||
|
callback: Dispatch<SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IErrorResponse {
|
||||||
|
details: IErrorDetails[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IErrorDetails {
|
||||||
|
message: string;
|
||||||
|
validationErrors: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const LENGTH_ERROR = 'The password must be at least 10 characters long.';
|
||||||
|
const NUMBER_ERROR = 'The password must contain at least one number.';
|
||||||
|
const SYMBOL_ERROR =
|
||||||
|
'The password must contain at least one special character.';
|
||||||
|
const UPPERCASE_ERROR =
|
||||||
|
'The password must contain at least one uppercase letter';
|
||||||
|
const LOWERCASE_ERROR =
|
||||||
|
'The password must contain at least one lowercase letter.';
|
||||||
|
|
||||||
|
const PasswordChecker = ({ password, callback }: IPasswordCheckerProps) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
const [casingError, setCasingError] = useState(true);
|
||||||
|
const [numberError, setNumberError] = useState(true);
|
||||||
|
const [symbolError, setSymbolError] = useState(true);
|
||||||
|
const [lengthError, setLengthError] = useState(true);
|
||||||
|
|
||||||
|
const makeValidatePassReq = useCallback(() => {
|
||||||
|
return fetch('auth/reset/validate-password', {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ password }),
|
||||||
|
});
|
||||||
|
}, [password]);
|
||||||
|
|
||||||
|
const checkPassword = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const res = await makeValidatePassReq();
|
||||||
|
if (res.status === BAD_REQUEST) {
|
||||||
|
const data = await res.json();
|
||||||
|
handleErrorResponse(data);
|
||||||
|
callback(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status === OK) {
|
||||||
|
clearErrors();
|
||||||
|
callback(true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ResetPasswordForm handles errors related to submitting the form.
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}, [makeValidatePassReq, callback]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkPassword();
|
||||||
|
}, [password, checkPassword]);
|
||||||
|
|
||||||
|
const clearErrors = () => {
|
||||||
|
setCasingError(false);
|
||||||
|
setNumberError(false);
|
||||||
|
setSymbolError(false);
|
||||||
|
setLengthError(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleErrorResponse = (data: IErrorResponse) => {
|
||||||
|
const errors = data.details[0].validationErrors;
|
||||||
|
|
||||||
|
if (errors.includes(NUMBER_ERROR)) {
|
||||||
|
setNumberError(true);
|
||||||
|
} else {
|
||||||
|
setNumberError(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.includes(SYMBOL_ERROR)) {
|
||||||
|
setSymbolError(true);
|
||||||
|
} else {
|
||||||
|
setSymbolError(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.includes(LENGTH_ERROR)) {
|
||||||
|
setLengthError(true);
|
||||||
|
} else {
|
||||||
|
setLengthError(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
errors.includes(LOWERCASE_ERROR) ||
|
||||||
|
errors.includes(UPPERCASE_ERROR)
|
||||||
|
) {
|
||||||
|
setCasingError(true);
|
||||||
|
} else {
|
||||||
|
setCasingError(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const lengthStatusBarClasses = classnames(styles.statusBar, {
|
||||||
|
[styles.statusBarSuccess]: !lengthError,
|
||||||
|
});
|
||||||
|
|
||||||
|
const numberStatusBarClasses = classnames(styles.statusBar, {
|
||||||
|
[styles.statusBarSuccess]: !numberError,
|
||||||
|
});
|
||||||
|
|
||||||
|
const symbolStatusBarClasses = classnames(styles.statusBar, {
|
||||||
|
[styles.statusBarSuccess]: !symbolError,
|
||||||
|
});
|
||||||
|
|
||||||
|
const casingStatusBarClasses = classnames(styles.statusBar, {
|
||||||
|
[styles.statusBarSuccess]: !casingError,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip
|
||||||
|
arrow
|
||||||
|
title="Your password needs to be at least ten characters long, and include an uppercase letter, a lowercase letter, a number and a symbol to be a valid OWASP password"
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className={styles.title}
|
||||||
|
data-loading
|
||||||
|
>
|
||||||
|
Please set a strong password
|
||||||
|
<HelpIcon className={styles.helpIcon} />
|
||||||
|
</Typography>
|
||||||
|
</Tooltip>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.headerContainer}>
|
||||||
|
<div className={styles.checkContainer}>
|
||||||
|
<Typography variant="body2" data-loading>
|
||||||
|
Length
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={styles.checkContainer}>
|
||||||
|
<Typography variant="body2" data-loading>
|
||||||
|
Casing
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={styles.checkContainer}>
|
||||||
|
<Typography variant="body2" data-loading>
|
||||||
|
Number
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={styles.checkContainer}>
|
||||||
|
<Typography variant="body2" data-loading>
|
||||||
|
Symbol
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.divider} />
|
||||||
|
<div className={styles.statusBarContainer}>
|
||||||
|
<div className={styles.checkContainer}>
|
||||||
|
<div className={lengthStatusBarClasses} data-loading />
|
||||||
|
</div>
|
||||||
|
<div className={styles.checkContainer}>
|
||||||
|
<div className={casingStatusBarClasses} data-loading />
|
||||||
|
</div>{' '}
|
||||||
|
<div className={styles.checkContainer}>
|
||||||
|
<div className={numberStatusBarClasses} data-loading />
|
||||||
|
</div>
|
||||||
|
<div className={styles.checkContainer}>
|
||||||
|
<div className={symbolStatusBarClasses} data-loading />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PasswordChecker;
|
@ -0,0 +1,22 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
matcherContainer: {
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
matcherIcon: {
|
||||||
|
marginRight: '5px',
|
||||||
|
},
|
||||||
|
matcher: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '-8px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
matcherError: {
|
||||||
|
color: theme.palette.error.main,
|
||||||
|
},
|
||||||
|
matcherSuccess: {
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
}));
|
@ -0,0 +1,55 @@
|
|||||||
|
import { Typography } from '@material-ui/core';
|
||||||
|
import ConditionallyRender from '../../../../common/ConditionallyRender';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import CheckIcon from '@material-ui/icons/Check';
|
||||||
|
|
||||||
|
import { useStyles } from './PasswordMatcher.styles';
|
||||||
|
|
||||||
|
interface IPasswordMatcherProps {
|
||||||
|
started: boolean;
|
||||||
|
matchingPasswords: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PasswordMatcher = ({
|
||||||
|
started,
|
||||||
|
matchingPasswords,
|
||||||
|
}: IPasswordMatcherProps) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
return (
|
||||||
|
<div className={styles.matcherContainer}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={started}
|
||||||
|
show={
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={matchingPasswords}
|
||||||
|
show={
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
data-loading
|
||||||
|
className={classnames(styles.matcher, {
|
||||||
|
[styles.matcherSuccess]: matchingPasswords,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<CheckIcon className={styles.matcherIcon} />{' '}
|
||||||
|
Passwords match
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
data-loading
|
||||||
|
className={classnames(styles.matcher, {
|
||||||
|
[styles.matcherError]: !matchingPasswords,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Passwords do not match
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PasswordMatcher;
|
@ -0,0 +1,12 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
container: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
maxWidth: '300px',
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
width: '150px',
|
||||||
|
},
|
||||||
|
}));
|
@ -0,0 +1,144 @@
|
|||||||
|
import { Button, TextField } from '@material-ui/core';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import {
|
||||||
|
SyntheticEvent,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
} from 'react';
|
||||||
|
import { useHistory } from 'react-router';
|
||||||
|
import { useCommonStyles } from '../../../../common.styles';
|
||||||
|
import { OK } from '../../../../constants/statusCodes';
|
||||||
|
import ConditionallyRender from '../../../common/ConditionallyRender';
|
||||||
|
import ResetPasswordError from '../ResetPasswordError/ResetPasswordError';
|
||||||
|
import PasswordChecker from './PasswordChecker/PasswordChecker';
|
||||||
|
import PasswordMatcher from './PasswordMatcher/PasswordMatcher';
|
||||||
|
import { useStyles } from './ResetPasswordForm.styles';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
interface IResetPasswordProps {
|
||||||
|
token: string;
|
||||||
|
setLoading: Dispatch<SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResetPasswordForm = ({ token, setLoading }: IResetPasswordProps) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
const commonStyles = useCommonStyles();
|
||||||
|
const [apiError, setApiError] = useState(false);
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
const [matchingPasswords, setMatchingPasswords] = useState(false);
|
||||||
|
const [validOwaspPassword, setValidOwaspPassword] = useState(false);
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const submittable = matchingPasswords && validOwaspPassword;
|
||||||
|
|
||||||
|
const setValidOwaspPasswordMemo = useCallback(setValidOwaspPassword, [
|
||||||
|
setValidOwaspPassword,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (password === confirmPassword) {
|
||||||
|
setMatchingPasswords(true);
|
||||||
|
} else {
|
||||||
|
setMatchingPasswords(false);
|
||||||
|
}
|
||||||
|
}, [password, confirmPassword]);
|
||||||
|
|
||||||
|
const makeResetPasswordReq = () => {
|
||||||
|
return fetch('auth/reset/password', {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
token,
|
||||||
|
password,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitResetPassword = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await makeResetPasswordReq();
|
||||||
|
setLoading(false);
|
||||||
|
if (res.status === OK) {
|
||||||
|
history.push('login?reset=true');
|
||||||
|
setApiError(false);
|
||||||
|
} else {
|
||||||
|
setApiError(true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setApiError(true);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: SyntheticEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (submittable) {
|
||||||
|
submitResetPassword();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const started = Boolean(password && confirmPassword);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={apiError}
|
||||||
|
show={<ResetPasswordError />}
|
||||||
|
/>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className={classnames(
|
||||||
|
commonStyles.contentSpacingY,
|
||||||
|
styles.container
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<PasswordChecker
|
||||||
|
password={password}
|
||||||
|
callback={setValidOwaspPasswordMemo}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
data-loading
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword}
|
||||||
|
placeholder="Confirm password"
|
||||||
|
onChange={e => setConfirmPassword(e.target.value)}
|
||||||
|
data-loading
|
||||||
|
/>
|
||||||
|
<PasswordMatcher
|
||||||
|
started={started}
|
||||||
|
matchingPasswords={matchingPasswords}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
className={styles.button}
|
||||||
|
data-loading
|
||||||
|
disabled={!submittable}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ResetPasswordForm;
|
@ -0,0 +1,12 @@
|
|||||||
|
import { Alert, AlertTitle } from '@material-ui/lab';
|
||||||
|
|
||||||
|
const ResetPasswordSuccess = () => {
|
||||||
|
return (
|
||||||
|
<Alert severity="success">
|
||||||
|
<AlertTitle>Success</AlertTitle>
|
||||||
|
You successfully reset your password.
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ResetPasswordSuccess;
|
@ -0,0 +1,26 @@
|
|||||||
|
import { makeStyles } from '@material-ui/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
container: {
|
||||||
|
display: 'flex',
|
||||||
|
},
|
||||||
|
leftContainer: { width: '40%', minHeight: '100vh' },
|
||||||
|
rightContainer: { width: '60%', minHeight: '100vh', position: 'relative' },
|
||||||
|
menu: {
|
||||||
|
position: 'absolute',
|
||||||
|
right: '20px',
|
||||||
|
top: '20px',
|
||||||
|
'& a': {
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: '#000',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: '1.2rem',
|
||||||
|
marginBottom: '1rem',
|
||||||
|
},
|
||||||
|
innerRightContainer: {
|
||||||
|
padding: '4rem 3rem',
|
||||||
|
},
|
||||||
|
}));
|
@ -0,0 +1,53 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import StandaloneBanner from '../../StandaloneBanner/StandaloneBanner';
|
||||||
|
|
||||||
|
import { Typography } from '@material-ui/core';
|
||||||
|
|
||||||
|
import { useStyles } from './StandaloneLayout.styles';
|
||||||
|
import ConditionallyRender from '../../../common/ConditionallyRender';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface IStandaloneLayout {
|
||||||
|
BannerComponent?: JSX.Element;
|
||||||
|
showMenu?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StandaloneLayout: FC<IStandaloneLayout> = ({
|
||||||
|
children,
|
||||||
|
showMenu = true,
|
||||||
|
BannerComponent,
|
||||||
|
}) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
let banner = (
|
||||||
|
<StandaloneBanner title="Unleash">
|
||||||
|
<Typography variant="subtitle1">
|
||||||
|
Committed to creating new ways of developing.
|
||||||
|
</Typography>
|
||||||
|
</StandaloneBanner>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (BannerComponent) {
|
||||||
|
banner = BannerComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.leftContainer}>{banner}</div>
|
||||||
|
<div className={styles.rightContainer}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={showMenu}
|
||||||
|
show={
|
||||||
|
<div className={styles.menu}>
|
||||||
|
<Link to="/login">Login</Link>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.innerRightContainer}>{children}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StandaloneLayout;
|
@ -26,6 +26,7 @@ export default class ShowUserComponent extends React.Component {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.fetchUser();
|
this.props.fetchUser();
|
||||||
|
|
||||||
// find default locale and add it in choices if not present
|
// find default locale and add it in choices if not present
|
||||||
const locale = navigator.language || navigator.userLanguage;
|
const locale = navigator.language || navigator.userLanguage;
|
||||||
let found = this.possibleLocales.find(l => l.value === locale);
|
let found = this.possibleLocales.find(l => l.value === locale);
|
||||||
|
3
frontend/src/constants/statusCodes.ts
Normal file
3
frontend/src/constants/statusCodes.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const BAD_REQUEST = 400;
|
||||||
|
export const OK = 200;
|
||||||
|
export const NOT_FOUND = 404;
|
24
frontend/src/hooks/useLoading.ts
Normal file
24
frontend/src/hooks/useLoading.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { useEffect, createRef } from 'react';
|
||||||
|
|
||||||
|
type refElement = HTMLDivElement;
|
||||||
|
|
||||||
|
const useLoading = (loading: boolean) => {
|
||||||
|
const ref = createRef<refElement>();
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current) {
|
||||||
|
const elements = ref.current.querySelectorAll('[data-loading]');
|
||||||
|
|
||||||
|
elements.forEach(element => {
|
||||||
|
if (loading) {
|
||||||
|
element.classList.add('skeleton');
|
||||||
|
} else {
|
||||||
|
element.classList.remove('skeleton');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [loading, ref]);
|
||||||
|
|
||||||
|
return ref;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useLoading;
|
7
frontend/src/hooks/useQueryParams.ts
Normal file
7
frontend/src/hooks/useQueryParams.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { useLocation } from 'react-router';
|
||||||
|
|
||||||
|
const useQueryParams = () => {
|
||||||
|
return new URLSearchParams(useLocation().search);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useQueryParams;
|
38
frontend/src/hooks/useResetPassword.ts
Normal file
38
frontend/src/hooks/useResetPassword.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import useSWR from 'swr';
|
||||||
|
import useQueryParams from './useQueryParams';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
const getFetcher = (token: string) => () =>
|
||||||
|
fetch(`auth/reset/validate?token=${token}`, {
|
||||||
|
method: 'GET',
|
||||||
|
}).then(res => res.json());
|
||||||
|
|
||||||
|
const INVALID_TOKEN_ERROR = 'InvalidTokenError';
|
||||||
|
|
||||||
|
const useResetPassword = () => {
|
||||||
|
const query = useQueryParams();
|
||||||
|
const initialToken = query.get('token') || '';
|
||||||
|
const [token, setToken] = useState(initialToken);
|
||||||
|
|
||||||
|
const fetcher = getFetcher(token);
|
||||||
|
const { data, error } = useSWR(
|
||||||
|
`auth/reset/validate?token=${token}`,
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
const [loading, setLoading] = useState(!error && !data);
|
||||||
|
|
||||||
|
const retry = () => {
|
||||||
|
const token = query.get('token') || '';
|
||||||
|
setToken(token);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(!error && !data);
|
||||||
|
}, [data, error]);
|
||||||
|
|
||||||
|
const invalidToken = !loading && data?.name === INVALID_TOKEN_ERROR;
|
||||||
|
|
||||||
|
return { token, data, error, loading, setLoading, invalidToken, retry };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useResetPassword;
|
@ -1,4 +1,4 @@
|
|||||||
<svg width="332" height="229" viewBox="0 0 332 229" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="332" height="239" viewBox="0 0 332 239" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M15.4999 200C59.4066 200 95 164.407 95 120.5C95 76.5933 59.4066 41 15.4999 41C-28.4068 41 -64 76.5933 -64 120.5C-64 164.407 -28.4068 200 15.4999 200Z" fill="#5D7A88" fill-opacity="0.75"/>
|
<path d="M15.4999 200C59.4066 200 95 164.407 95 120.5C95 76.5933 59.4066 41 15.4999 41C-28.4068 41 -64 76.5933 -64 120.5C-64 164.407 -28.4068 200 15.4999 200Z" fill="#5D7A88" fill-opacity="0.75"/>
|
||||||
<path d="M212.2 239H-147.2C-178.973 239 -209.445 226.41 -231.911 203.999C-254.378 181.589 -267 151.193 -267 119.5C-267 87.8066 -254.378 57.4112 -231.911 35.0006C-209.445 12.5901 -178.973 0 -147.2 0H212.2C243.973 0 274.445 12.5901 296.911 35.0006C319.378 57.4112 332 87.8066 332 119.5C332 151.193 319.378 181.589 296.911 203.999C274.445 226.41 243.973 239 212.2 239ZM-147.2 15.9333C-174.737 15.9333 -201.145 26.8448 -220.616 46.2673C-240.088 65.6898 -251.027 92.0324 -251.027 119.5C-251.027 146.968 -240.088 173.31 -220.616 192.733C-201.145 212.155 -174.737 223.067 -147.2 223.067H212.2C239.737 223.067 266.145 212.155 285.617 192.733C305.088 173.31 316.027 146.968 316.027 119.5C316.027 92.0324 305.088 65.6898 285.617 46.2673C266.145 26.8448 239.737 15.9333 212.2 15.9333H-147.2Z" fill="#5D7A88" fill-opacity="0.75"/>
|
<path d="M212.2 239H-147.2C-178.973 239 -209.445 226.41 -231.911 203.999C-254.378 181.589 -267 151.193 -267 119.5C-267 87.8066 -254.378 57.4112 -231.911 35.0006C-209.445 12.5901 -178.973 0 -147.2 0H212.2C243.973 0 274.445 12.5901 296.911 35.0006C319.378 57.4112 332 87.8066 332 119.5C332 151.193 319.378 181.589 296.911 203.999C274.445 226.41 243.973 239 212.2 239ZM-147.2 15.9333C-174.737 15.9333 -201.145 26.8448 -220.616 46.2673C-240.088 65.6898 -251.027 92.0324 -251.027 119.5C-251.027 146.968 -240.088 173.31 -220.616 192.733C-201.145 212.155 -174.737 223.067 -147.2 223.067H212.2C239.737 223.067 266.145 212.155 285.616 192.733C305.088 173.31 316.027 146.968 316.027 119.5C316.027 92.0324 305.088 65.6898 285.616 46.2673C266.145 26.8448 239.737 15.9333 212.2 15.9333H-147.2Z" fill="#5D7A88" fill-opacity="0.75"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
10
frontend/src/interfaces/palette.d.ts
vendored
Normal file
10
frontend/src/interfaces/palette.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import * as createPalette from '@material-ui/core/styles/createPalette';
|
||||||
|
|
||||||
|
declare module '@material-ui/core/styles/createPalette' {
|
||||||
|
interface PaletteOptions {
|
||||||
|
borders?: PaletteColorOptions;
|
||||||
|
}
|
||||||
|
interface Palette {
|
||||||
|
borders?: PaletteColor;
|
||||||
|
}
|
||||||
|
}
|
@ -11,10 +11,11 @@ const userStore = (state = new $Map(), action) => {
|
|||||||
.set('authDetails', undefined);
|
.set('authDetails', undefined);
|
||||||
return state;
|
return state;
|
||||||
case AUTH_REQUIRED:
|
case AUTH_REQUIRED:
|
||||||
state = state.set('authDetails', action.error.body).set('showDialog', true);
|
state = state
|
||||||
|
.set('authDetails', action.error.body)
|
||||||
|
.set('showDialog', true);
|
||||||
return state;
|
return state;
|
||||||
case USER_LOGOUT:
|
case USER_LOGOUT:
|
||||||
console.log("Resetting state due to logout");
|
|
||||||
return new $Map();
|
return new $Map();
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
@ -85,7 +85,8 @@ const theme = createMuiTheme({
|
|||||||
radius: { main: '3px' },
|
radius: { main: '3px' },
|
||||||
},
|
},
|
||||||
fontSizes: {
|
fontSizes: {
|
||||||
mainHeader: '1.1rem',
|
mainHeader: '1.2rem',
|
||||||
|
subHeader: '1.1rem',
|
||||||
},
|
},
|
||||||
boxShadows: {
|
boxShadows: {
|
||||||
chip: {
|
chip: {
|
||||||
|
@ -1805,6 +1805,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/debounce@^1.2.0":
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/debounce/-/debounce-1.2.0.tgz#9ee99259f41018c640b3929e1bb32c3dcecdb192"
|
||||||
|
integrity sha512-bWG5wapaWgbss9E238T0R6bfo5Fh3OkeoSt245CM7JJwVwpw6MEBCbIxLq5z8KzsE3uJhzcIuQkyiZmzV3M/Dw==
|
||||||
|
|
||||||
"@types/enzyme-adapter-react-16@^1.0.6":
|
"@types/enzyme-adapter-react-16@^1.0.6":
|
||||||
version "1.0.6"
|
version "1.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.6.tgz#8aca7ae2fd6c7137d869b6616e696d21bb8b0cec"
|
resolved "https://registry.yarnpkg.com/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.6.tgz#8aca7ae2fd6c7137d869b6616e696d21bb8b0cec"
|
||||||
@ -4348,6 +4353,11 @@ depd@~1.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
||||||
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
|
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
|
||||||
|
|
||||||
|
dequal@2.0.2:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d"
|
||||||
|
integrity sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==
|
||||||
|
|
||||||
des.js@^1.0.0:
|
des.js@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
|
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
|
||||||
@ -11301,6 +11311,13 @@ svgo@^1.0.0, svgo@^1.2.2:
|
|||||||
unquote "~1.1.1"
|
unquote "~1.1.1"
|
||||||
util.promisify "~1.0.0"
|
util.promisify "~1.0.0"
|
||||||
|
|
||||||
|
swr@^0.5.5:
|
||||||
|
version "0.5.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/swr/-/swr-0.5.5.tgz#c72c1615765f33570a16bbb13699e3ac87eaaa3a"
|
||||||
|
integrity sha512-u4mUorK9Ipt+6LEITvWRWiRWAQjAysI6cHxbMmMV1dIdDzxMnswWo1CyGoyBHXX91CchxcuoqgFZ/ycx+YfhCA==
|
||||||
|
dependencies:
|
||||||
|
dequal "2.0.2"
|
||||||
|
|
||||||
symbol-observable@1.2.0, symbol-observable@^1.2.0:
|
symbol-observable@1.2.0, symbol-observable@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
||||||
|
Loading…
Reference in New Issue
Block a user