mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-09 00:18:00 +01: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/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"@types/debounce": "^1.2.0",
|
||||
"@types/enzyme": "^3.10.8",
|
||||
"@types/enzyme-adapter-react-16": "^1.0.6",
|
||||
"@types/jest": "^26.0.15",
|
||||
@ -71,6 +72,7 @@
|
||||
"redux-thunk": "^2.3.0",
|
||||
"sass": "^1.32.8",
|
||||
"typescript": "^4.2.3",
|
||||
"swr": "^0.5.5",
|
||||
"web-vitals": "^1.0.1"
|
||||
},
|
||||
"jest": {
|
||||
|
@ -10,6 +10,10 @@ body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.MuiButton-root {
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@ -20,7 +24,7 @@ body {
|
||||
|
||||
.skeleton::before {
|
||||
background-color: #e2e8f0;
|
||||
content: "";
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
@ -45,7 +49,7 @@ body {
|
||||
rgba(255, 255, 255, 0)
|
||||
);
|
||||
animation: shimmer 3s infinite;
|
||||
content: "";
|
||||
content: '';
|
||||
z-index: 5001;
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,11 @@ export const useCommonStyles = makeStyles(theme => ({
|
||||
backgroundColor: theme.palette.division.main,
|
||||
height: '3px',
|
||||
},
|
||||
largeDivider: {
|
||||
margin: '2rem 0',
|
||||
backgroundColor: theme.palette.division.main,
|
||||
height: '3px',
|
||||
},
|
||||
bold: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
@ -38,4 +43,9 @@ export const useCommonStyles = makeStyles(theme => ({
|
||||
fullHeight: {
|
||||
height: '100%',
|
||||
},
|
||||
title: {
|
||||
fontSize: theme.fontSizes.mainHeader,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '0.5rem',
|
||||
},
|
||||
}));
|
||||
|
@ -28,7 +28,6 @@ const App = ({ location, user }: IAppProps) => {
|
||||
const isUnauthorized = () => {
|
||||
// authDetails only exists if the user is not logged in.
|
||||
|
||||
if (Object.keys(user).length === 0) return false;
|
||||
return user?.authDetails !== undefined;
|
||||
};
|
||||
|
||||
@ -54,6 +53,7 @@ const App = ({ location, user }: IAppProps) => {
|
||||
<route.component
|
||||
{...props}
|
||||
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
|
||||
to="/features/create"
|
||||
data-test="add-feature-btn"
|
||||
size="large"
|
||||
color="secondary"
|
||||
variant="contained"
|
||||
component={Link}
|
||||
|
@ -5,7 +5,7 @@ import classnames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Switch, Icon, IconButton, ListItem } from '@material-ui/core';
|
||||
import TimeAgo from 'react-timeago';
|
||||
import Progress from '../../progress-component';
|
||||
import Progress from '../../ProgressWheel';
|
||||
import Status from '../../status-component';
|
||||
import FeatureToggleListItemChip from './FeatureToggleListItemChip';
|
||||
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
||||
@ -29,18 +29,38 @@ const FeatureToggleListItem = ({
|
||||
|
||||
const { name, description, enabled, type, stale, createdAt } = feature;
|
||||
const { showLastHour = false } = settings;
|
||||
const isStale = showLastHour ? metricsLastHour.isFallback : metricsLastMinute.isFallback;
|
||||
const isStale = showLastHour
|
||||
? metricsLastHour.isFallback
|
||||
: metricsLastMinute.isFallback;
|
||||
const percent =
|
||||
1 *
|
||||
(showLastHour
|
||||
? calc(metricsLastHour.yes, metricsLastHour.yes + metricsLastHour.no, 0)
|
||||
: calc(metricsLastMinute.yes, metricsLastMinute.yes + metricsLastMinute.no, 0));
|
||||
const featureUrl = toggleFeature === undefined ? `/archive/strategies/${name}` : `/features/strategies/${name}`;
|
||||
? calc(
|
||||
metricsLastHour.yes,
|
||||
metricsLastHour.yes + metricsLastHour.no,
|
||||
0
|
||||
)
|
||||
: calc(
|
||||
metricsLastMinute.yes,
|
||||
metricsLastMinute.yes + metricsLastMinute.no,
|
||||
0
|
||||
));
|
||||
const featureUrl =
|
||||
toggleFeature === undefined
|
||||
? `/archive/strategies/${name}`
|
||||
: `/features/strategies/${name}`;
|
||||
|
||||
return (
|
||||
<ListItem {...rest} className={classnames(styles.listItem, rest.className)}>
|
||||
<ListItem
|
||||
{...rest}
|
||||
className={classnames(styles.listItem, rest.className)}
|
||||
>
|
||||
<span className={styles.listItemMetric}>
|
||||
<Progress strokeWidth={15} percentage={percent} isFallback={isStale} />
|
||||
<Progress
|
||||
strokeWidth={15}
|
||||
percentage={percent}
|
||||
isFallback={isStale}
|
||||
/>
|
||||
</span>
|
||||
<span className={styles.listItemToggle}>
|
||||
<ConditionallyRender
|
||||
@ -54,21 +74,43 @@ const FeatureToggleListItem = ({
|
||||
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 className={classnames(styles.listItemLink)}>
|
||||
<Link to={featureUrl} className={classnames(commonStyles.listLink, commonStyles.truncate)}>
|
||||
<span className={commonStyles.toggleName}>{name} </span>
|
||||
<Link
|
||||
to={featureUrl}
|
||||
className={classnames(
|
||||
commonStyles.listLink,
|
||||
commonStyles.truncate
|
||||
)}
|
||||
>
|
||||
<span className={commonStyles.toggleName}>
|
||||
{name}
|
||||
</span>
|
||||
<small>
|
||||
<TimeAgo date={createdAt} live={false} />
|
||||
</small>
|
||||
<div>
|
||||
<span className={commonStyles.truncate}><small>{description}</small></span>
|
||||
<span className={commonStyles.truncate}>
|
||||
<small>{description}</small>
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</span>
|
||||
<span className={classnames(styles.listItemStrategies, commonStyles.hideLt920)}>
|
||||
<span
|
||||
className={classnames(
|
||||
styles.listItemStrategies,
|
||||
commonStyles.hideLt920
|
||||
)}
|
||||
>
|
||||
<Status stale={stale} showActive={false} />
|
||||
<FeatureToggleListItemChip type={type} />
|
||||
</span>
|
||||
|
@ -145,7 +145,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
</div>
|
||||
<a
|
||||
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"
|
||||
href="/features/create"
|
||||
onBlur={[Function]}
|
||||
|
@ -74,7 +74,7 @@ const FeatureView = ({
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [features]);
|
||||
}, []);
|
||||
|
||||
const getTabComponent = 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 Progress from '../progress-component';
|
||||
import Progress from '../ProgressWheel';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
jest.mock('@material-ui/core');
|
||||
|
||||
test('renders correctly with 15% done no fallback', () => {
|
||||
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();
|
||||
});
|
||||
|
||||
test('renders correctly with 0% done no fallback', () => {
|
||||
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();
|
||||
});
|
||||
|
||||
test('renders correctly with 15% done with fallback', () => {
|
||||
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();
|
||||
});
|
||||
|
||||
test('renders correctly with 0% done with fallback', () => {
|
||||
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();
|
||||
});
|
||||
|
@ -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 {
|
||||
stroke: currentColor;
|
||||
stroke: #5eb89b;
|
||||
stroke-linecap: round;
|
||||
transition: stroke-dashoffset 5s ease 0s;
|
||||
}
|
||||
|
||||
.trail {
|
||||
stroke: currentColor;
|
||||
stroke: #888888;
|
||||
}
|
||||
|
||||
.text {
|
||||
|
@ -473,10 +473,10 @@ exports[`renders correctly with with variants 1`] = `
|
||||
</svg>
|
||||
<fieldset
|
||||
aria-hidden={true}
|
||||
className="PrivateNotchedOutline-root-11 MuiOutlinedInput-notchedOutline"
|
||||
className="PrivateNotchedOutline-root-13 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
className="PrivateNotchedOutline-legendLabelled-13 PrivateNotchedOutline-legendNotched-14"
|
||||
className="PrivateNotchedOutline-legendLabelled-15 PrivateNotchedOutline-legendNotched-16"
|
||||
>
|
||||
<span>
|
||||
Stickiness
|
||||
|
@ -139,10 +139,10 @@ exports[`renders correctly with one feature 1`] = `
|
||||
</svg>
|
||||
<fieldset
|
||||
aria-hidden={true}
|
||||
className="PrivateNotchedOutline-root-11 MuiOutlinedInput-notchedOutline"
|
||||
className="PrivateNotchedOutline-root-13 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
className="PrivateNotchedOutline-legendLabelled-13"
|
||||
className="PrivateNotchedOutline-legendLabelled-15"
|
||||
>
|
||||
<span>
|
||||
Project
|
||||
@ -174,7 +174,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
>
|
||||
<span
|
||||
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]}
|
||||
onDragLeave={[Function]}
|
||||
onFocus={[Function]}
|
||||
@ -193,7 +193,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
>
|
||||
<input
|
||||
checked={false}
|
||||
className="PrivateSwitchBase-input-18 MuiSwitch-input"
|
||||
className="PrivateSwitchBase-input-20 MuiSwitch-input"
|
||||
disabled={false}
|
||||
onChange={[Function]}
|
||||
type="checkbox"
|
||||
@ -317,7 +317,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
</div>
|
||||
<hr />
|
||||
<div
|
||||
className="MuiPaper-root makeStyles-tabNav-19 MuiPaper-elevation1 MuiPaper-rounded"
|
||||
className="MuiPaper-root makeStyles-tabNav-21 MuiPaper-elevation1 MuiPaper-rounded"
|
||||
>
|
||||
<div
|
||||
className="MuiTabs-root"
|
||||
@ -365,7 +365,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
Activation
|
||||
</span>
|
||||
<span
|
||||
className="PrivateTabIndicator-root-20 PrivateTabIndicator-colorPrimary-21 MuiTabs-indicator"
|
||||
className="PrivateTabIndicator-root-22 PrivateTabIndicator-colorPrimary-23 MuiTabs-indicator"
|
||||
style={Object {}}
|
||||
/>
|
||||
</button>
|
||||
|
@ -6,7 +6,7 @@ import LinkIcon from '@material-ui/icons/Link';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { AppsLinkList, calc } from '../../common';
|
||||
import { formatFullDateTimeWithLocale } from '../../common/util';
|
||||
import Progress from '../progress-component';
|
||||
import Progress from '../ProgressWheel';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
import styles from './metric.module.scss';
|
||||
@ -55,7 +55,8 @@ export default class MetricComponent extends React.Component {
|
||||
formatFullDateTime(v) {
|
||||
return formatFullDateTimeWithLocale(v, this.props.location.locale);
|
||||
}
|
||||
renderLastSeen = lastSeenAt => (lastSeenAt ? this.formatFullDateTime(lastSeenAt) : 'Never reported');
|
||||
renderLastSeen = lastSeenAt =>
|
||||
lastSeenAt ? this.formatFullDateTime(lastSeenAt) : 'Never reported';
|
||||
|
||||
render() {
|
||||
const { metrics = {}, featureToggle } = this.props;
|
||||
@ -65,12 +66,19 @@ export default class MetricComponent extends React.Component {
|
||||
seenApps = [],
|
||||
} = metrics;
|
||||
|
||||
const lastHourPercent = 1 * calc(lastHour.yes, lastHour.yes + lastHour.no, 0);
|
||||
const lastMinutePercent = 1 * calc(lastMinute.yes, lastMinute.yes + lastMinute.no, 0);
|
||||
const lastHourPercent =
|
||||
1 * calc(lastHour.yes, lastHour.yes + lastHour.no, 0);
|
||||
const lastMinutePercent =
|
||||
1 * calc(lastMinute.yes, lastMinute.yes + lastMinute.no, 0);
|
||||
|
||||
return (
|
||||
<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}>
|
||||
<Progress
|
||||
percentage={lastMinutePercent}
|
||||
@ -83,13 +91,17 @@ export default class MetricComponent extends React.Component {
|
||||
elseShow={
|
||||
<p>
|
||||
<strong>Last minute</strong>
|
||||
<br /> Yes {lastMinute.yes}, No: {lastMinute.no}
|
||||
<br /> Yes {lastMinute.yes}, No:{' '}
|
||||
{lastMinute.no}
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<Progress percentage={lastHourPercent} isFallback={lastHour.isFallback} />
|
||||
<Progress
|
||||
percentage={lastHourPercent}
|
||||
isFallback={lastHour.isFallback}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={lastHour.isFallback}
|
||||
show={<p>No metrics available</p>}
|
||||
@ -111,13 +123,20 @@ export default class MetricComponent extends React.Component {
|
||||
}
|
||||
elseShow={
|
||||
<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
|
||||
</Icon>
|
||||
<div>
|
||||
<small>
|
||||
<strong>Not used in an app in the last hour. </strong>
|
||||
This might be due to your client implementation not reporting usage.
|
||||
<strong>
|
||||
Not used in an app in the last
|
||||
hour.{' '}
|
||||
</strong>
|
||||
This might be due to your client
|
||||
implementation not reporting usage.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
@ -131,14 +150,20 @@ export default class MetricComponent extends React.Component {
|
||||
show={
|
||||
<>
|
||||
<strong>Created: </strong>
|
||||
<span>{this.formatFullDateTime(featureToggle.createdAt)}</span>
|
||||
<span>
|
||||
{this.formatFullDateTime(
|
||||
featureToggle.createdAt
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<br />
|
||||
<strong>Last seen: </strong>
|
||||
<span>{this.renderLastSeen(featureToggle.lastSeenAt)}</span>
|
||||
<span>
|
||||
{this.renderLastSeen(featureToggle.lastSeenAt)}
|
||||
</span>
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
@ -2,11 +2,31 @@ import ConditionallyRender from '../../common/ConditionallyRender';
|
||||
import MainLayout from '../MainLayout/MainLayout';
|
||||
|
||||
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 (
|
||||
<ConditionallyRender
|
||||
condition={isLoginPage}
|
||||
condition={standalonePages()}
|
||||
show={children}
|
||||
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';
|
||||
|
||||
test('returns all defined routes', () => {
|
||||
expect(routes.length).toEqual(35);
|
||||
expect(routes).toMatchSnapshot();
|
||||
});
|
||||
import { baseRoutes, getRoute } from '../routes';
|
||||
|
||||
test('returns all baseRoutes', () => {
|
||||
expect(baseRoutes.length).toEqual(12);
|
||||
|
@ -34,6 +34,9 @@ import AdminAuth from '../../page/admin/auth';
|
||||
import Reporting from '../../page/reporting';
|
||||
import Login from '../user/Login';
|
||||
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 = [
|
||||
// Features
|
||||
@ -307,40 +310,64 @@ export const routes = [
|
||||
hidden: true,
|
||||
layout: 'standalone',
|
||||
},
|
||||
// Admin
|
||||
{
|
||||
path: '/admin/api',
|
||||
parent: '/admin',
|
||||
title: 'API access',
|
||||
component: AdminApi,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
},
|
||||
{
|
||||
path: '/admin/users',
|
||||
parent: '/admin',
|
||||
title: 'Users',
|
||||
component: AdminUsers,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
},
|
||||
{
|
||||
path: '/admin/auth',
|
||||
parent: '/admin',
|
||||
title: 'Authentication',
|
||||
component: AdminAuth,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
title: 'Admin',
|
||||
icon: 'album',
|
||||
component: Admin,
|
||||
hidden: false,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
},
|
||||
// Admin
|
||||
{
|
||||
path: '/admin/api',
|
||||
parent: '/admin',
|
||||
title: 'API access',
|
||||
component: AdminApi,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
},
|
||||
{
|
||||
path: '/admin/users',
|
||||
parent: '/admin',
|
||||
title: 'Users',
|
||||
component: AdminUsers,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
},
|
||||
{
|
||||
path: '/admin/auth',
|
||||
parent: '/admin',
|
||||
title: 'Authentication',
|
||||
component: AdminAuth,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
title: 'Admin',
|
||||
icon: 'album',
|
||||
component: Admin,
|
||||
hidden: false,
|
||||
type: 'protected',
|
||||
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);
|
||||
|
@ -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 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 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 useQueryParams from '../../../hooks/useQueryParams';
|
||||
import ResetPasswordSuccess from '../common/ResetPasswordSuccess/ResetPasswordSuccess';
|
||||
import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout';
|
||||
|
||||
const Login = ({ history, loadInitialData, isUnauthorized, authDetails }) => {
|
||||
const theme = useTheme();
|
||||
const styles = useStyles();
|
||||
const smallScreen = useMediaQuery(theme.breakpoints.up('md'));
|
||||
const query = useQueryParams();
|
||||
|
||||
useEffect(() => {
|
||||
if (isUnauthorized()) {
|
||||
loadInitialData();
|
||||
} else {
|
||||
}
|
||||
/* eslint-disable-next-line */
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isUnauthorized()) {
|
||||
history.push('features');
|
||||
}
|
||||
/* eslint-disable-next-line */
|
||||
}, [authDetails]);
|
||||
|
||||
const resetPassword = query.get('reset') === 'true';
|
||||
|
||||
return (
|
||||
<div className={styles.loginContainer}>
|
||||
<div className={classnames(styles.container)}>
|
||||
<div
|
||||
className={classnames(
|
||||
styles.contentContainer,
|
||||
styles.gradient
|
||||
)}
|
||||
>
|
||||
<h1 className={styles.title}>
|
||||
<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>
|
||||
<StandaloneLayout>
|
||||
<div>
|
||||
<h2 className={styles.title}>Login</h2>
|
||||
<ConditionallyRender
|
||||
condition={resetPassword}
|
||||
show={<ResetPasswordSuccess />}
|
||||
/>
|
||||
<div className={styles.loginFormContainer}>
|
||||
<AuthenticationContainer history={history} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StandaloneLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -24,7 +24,7 @@ export const useStyles = makeStyles(theme => ({
|
||||
color: theme.palette.login.main,
|
||||
},
|
||||
title: {
|
||||
fontSize: '1.5rem',
|
||||
fontSize: theme.fontSizes.mainHeader,
|
||||
marginBottom: '0.5rem',
|
||||
display: 'flex',
|
||||
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() {
|
||||
this.props.fetchUser();
|
||||
|
||||
// find default locale and add it in choices if not present
|
||||
const locale = navigator.language || navigator.userLanguage;
|
||||
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="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>
|
||||
|
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);
|
||||
return state;
|
||||
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;
|
||||
case USER_LOGOUT:
|
||||
console.log("Resetting state due to logout");
|
||||
return new $Map();
|
||||
default:
|
||||
return state;
|
||||
|
@ -85,7 +85,8 @@ const theme = createMuiTheme({
|
||||
radius: { main: '3px' },
|
||||
},
|
||||
fontSizes: {
|
||||
mainHeader: '1.1rem',
|
||||
mainHeader: '1.2rem',
|
||||
subHeader: '1.1rem',
|
||||
},
|
||||
boxShadows: {
|
||||
chip: {
|
||||
|
@ -1805,6 +1805,11 @@
|
||||
dependencies:
|
||||
"@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":
|
||||
version "1.0.6"
|
||||
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"
|
||||
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:
|
||||
version "1.0.1"
|
||||
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"
|
||||
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:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
||||
|
Loading…
Reference in New Issue
Block a user