mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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/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