diff --git a/src/app/api/url-checker.jsx b/src/app/api/url-checker.jsx new file mode 100644 index 0000000000..d36c7cfe6a --- /dev/null +++ b/src/app/api/url-checker.jsx @@ -0,0 +1,10 @@ +import { useMutation } from 'react-query'; + +import { useAxios } from './axios'; + +const usePostUrlChecker = () => { + const axios = useAxios(); + return useMutation(async (data) => axios.post('urls/validate', data)); +}; + +export default usePostUrlChecker; diff --git a/src/app/navigation/navigation-top.html b/src/app/navigation/navigation-top.html index 9480391a80..b470a67f8c 100644 --- a/src/app/navigation/navigation-top.html +++ b/src/app/navigation/navigation-top.html @@ -90,6 +90,7 @@
  • Change Requests
  • System Maintenance
  • Dashboard
  • +
  • URL Checker
  • FF4j
  • diff --git a/src/app/pages/administration/administration.module.js b/src/app/pages/administration/administration.module.js index f066c6d51b..95ef2ebf2e 100644 --- a/src/app/pages/administration/administration.module.js +++ b/src/app/pages/administration/administration.module.js @@ -5,6 +5,7 @@ import ChplLoginPage from './login/login-wrapper'; import ChplReportsWrapper from './reports/reports-wrapper'; import ChplSystemMaintenanceWrapper from './system-maintenance/system-maintenance-wrapper'; import ChplUploadPageWrapper from './upload/upload-page-wrapper'; +import ChplUrlCheckerWrapper from './url-checker/url-checker-wrapper'; import { reactToAngularComponent } from 'services/angular-react-helper'; @@ -39,4 +40,5 @@ angular .component('chplLoginPageBridge', reactToAngularComponent(ChplLoginPage)) .component('chplReportsWrapperBridge', reactToAngularComponent(ChplReportsWrapper)) .component('chplSystemMaintenanceWrapperBridge', reactToAngularComponent(ChplSystemMaintenanceWrapper)) - .component('chplUploadPageWrapperBridge', reactToAngularComponent(ChplUploadPageWrapper)); + .component('chplUploadPageWrapperBridge', reactToAngularComponent(ChplUploadPageWrapper)) + .component('chplUrlCheckerWrapperBridge', reactToAngularComponent(ChplUrlCheckerWrapper)); diff --git a/src/app/pages/administration/administration.state.js b/src/app/pages/administration/administration.state.js index 206eecf6fd..0d21c519ba 100644 --- a/src/app/pages/administration/administration.state.js +++ b/src/app/pages/administration/administration.state.js @@ -100,6 +100,14 @@ const states = [{ title: 'CHPL Administration - Upload', roles: ['chpl-admin', 'chpl-onc', 'chpl-onc-acb'], }, +}, { + name: 'administration.url-checker', + url: '/url-checker', + component: 'chplUrlCheckerWrapperBridge', + data: { + title: 'CHPL Administration - URL Checker', + roles: ['chpl-admin', 'chpl-onc', 'chpl-onc-acb'], + }, }, { name: 'login', url: '/login', diff --git a/src/app/pages/administration/url-checker/url-checker-wrapper.jsx b/src/app/pages/administration/url-checker/url-checker-wrapper.jsx new file mode 100644 index 0000000000..5f65283109 --- /dev/null +++ b/src/app/pages/administration/url-checker/url-checker-wrapper.jsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import ChplUrlChecker from './url-checker'; + +import AppWrapper from 'app-wrapper'; + +function ChplUrlCheckerWrapper() { + return ( + + + + ); +} + +export default ChplUrlCheckerWrapper; + +ChplUrlCheckerWrapper.propTypes = { +}; diff --git a/src/app/pages/administration/url-checker/url-checker.jsx b/src/app/pages/administration/url-checker/url-checker.jsx new file mode 100644 index 0000000000..935a1d6394 --- /dev/null +++ b/src/app/pages/administration/url-checker/url-checker.jsx @@ -0,0 +1,336 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { + Box, + Button, + Card, + CardContent, + CircularProgress, + Container, + Typography, + makeStyles, +} from '@material-ui/core'; +import { useSnackbar } from 'notistack'; +import { useFormik } from 'formik'; +import * as yup from 'yup'; +import CancelIcon from '@material-ui/icons/Cancel'; +import CheckCircleIcon from '@material-ui/icons/CheckCircle'; +import VerifiedUserIcon from '@material-ui/icons/VerifiedUser'; + +import { ChplTextField } from 'components/util'; +import { utilStyles, palette } from 'themes'; +import usePostUrlChecker from 'api/url-checker'; +import { UserContext } from 'shared/contexts'; + +const useStyles = makeStyles((theme) => ({ + ...utilStyles, + titlePadding: { + paddingTop: '16px', + paddingBottom: '16px', + }, + titleBackground: { + backgroundColor: palette.white, + paddingBottom: '16px', + marginTop: '-16px', + padding: '16px 32px', + boxShadow: 'rgb(149 157 165 / 10%) 0 4px 8px', + }, + pageBackground: { + backgroundColor: palette.lightGray, + minHeight: 'calc(100vh - 64px)', + }, + resultsCard: { + width: '32.3%', + overflowWrap: 'break-word', + [theme.breakpoints.down('sm')]: { + width: '100%', + }, + }, + resultsCardHalf: { + width: '49.2%', + overflowWrap: 'break-word', + [theme.breakpoints.down('sm')]: { + width: '100%', + }, + }, + statusText: { + display: 'flex', + alignItems: 'center', + }, + resultsContainer: { + display: 'flex', + flexDirection: 'row', + gap: '16px', + paddingBottom: '16px', + [theme.breakpoints.down('sm')]: { + flexDirection: 'column', + }, + }, +})); + +const validationSchema = yup.object({ + url: yup.string() + .required('Field is required') + .url('Improper format (http://www.example.com)'), +}); + +function ChplUrlChecker() { + const { hasAnyRole } = useContext(UserContext); + const { enqueueSnackbar } = useSnackbar(); + const { data, isLoading, isSuccess, mutate } = usePostUrlChecker(); + const [urlCheckResponse, setUrlCheckResponse] = useState(undefined); + const classes = useStyles(); + + useEffect(() => { + if (isLoading || !isSuccess) { return; } + setUrlCheckResponse(data.data); + }, [data, isLoading, isSuccess]); + + const validate = (urlToValidate) => { + setUrlCheckResponse(undefined); + mutate(urlToValidate, { + onError: () => { + enqueueSnackbar('There was an error attempting to check the URL.', { + variant: 'error', + }); + }, + }); + } + + const formik = useFormik({ + initialValues: { + url: '', + }, + onSubmit: () => { + const urlToValidate = { + url: formik.values.url, + }; + validate(urlToValidate); + }, + validationSchema, + }); + + return ( + <> + + + URL Checker + Validate a URL + + + + + + + + {(isLoading || isLoading) && ( + + + + )} + {urlCheckResponse + && ( + <> + Results + + + + + Status: + + + {urlCheckResponse.passed ? 'Passed' : 'Failure'} + {urlCheckResponse.passed + ? ( + + ) : ( + + )} + + {urlCheckResponse.errorMessage + && ( + <> + + Error Message: + + {urlCheckResponse.errorMessage} + + )} + + + + + + URL: + + + {urlCheckResponse.url} + + + + + { (hasAnyRole(['chpl-admin', 'chpl-onc']) || !urlCheckResponse.passed) + && ( + <> + Assertions + + + + {urlCheckResponse.httpResponseAssertion?.actualValue ? ( + <> + + HTTP Status Code: + + + + {urlCheckResponse.httpResponseAssertion.actualValue} + + {urlCheckResponse.httpResponseAssertion.passed + ? ( + + ) : ( + + )} + + + + Reference for HTTP Status Codes + + + + ) : ( + <> + + No HTTP Status Code Available: + + + + The HTTP response code could not be retrieved or is unavailable. + + {urlCheckResponse.httpResponseAssertion.passed + ? ( + + ) : ( + + )} + + + )} + + + + + + Response Time (in milliseconds): + + {urlCheckResponse.responseTimeAssertion?.actualValue ? + ( + <> + + + {urlCheckResponse.responseTimeAssertion.actualValue} + + {urlCheckResponse.responseTimeAssertion.passed + ? ( + + ) : ( + + )} + + + ) : ( + <> + + + The response time is empty or unavailable. + + {urlCheckResponse.responseTimeAssertion.passed + ? ( + + ) : ( + + )} + + + )} + + + + + {urlCheckResponse.bodyNotEmptyAssertion?.actualValue ? ( + <> + + Body Content: + + + + {urlCheckResponse.bodyNotEmptyAssertion.actualValue + ? urlCheckResponse.bodyNotEmptyAssertion.actualValue + : 'Empty body content'} + + {urlCheckResponse.bodyNotEmptyAssertion.passed + ? ( + + ) : ( + + )} + + + ) : ( + <> + + No Content Available: + + + + The body content is empty or unavailable. + + {urlCheckResponse.bodyNotEmptyAssertion.passed + ? ( + + ) : ( + + )} + + + )} + + + + + )} + + )} + + + ); +} + +export default ChplUrlChecker; + +ChplUrlChecker.propTypes = { +}; \ No newline at end of file