/tests/__mocks__/setupTests.js"
68 | ],
69 | "transform": {
70 | "^.+\\.(js|jsx)$": "babel-jest"
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/routes/configuration/optionHelper.js:
--------------------------------------------------------------------------------
1 | const timeWindowOption = [
2 | { key: "1H", value: "1 Hour",},
3 | { key: "2H", value: "2 Hours",},
4 | { key: "3H", value: "3 Hours",},
5 | { key: "6H", value: "6 Hours",},
6 | { key: "12H", value: "12 Hours",},
7 | { key: "24H", value: "24 Hours",},
8 | ]
9 |
10 | const timezoneOption = [
11 | { key: -12, value: "Coordinated Universal Time-12 (UTC-12)",},
12 | { key: -11, value: "Niue, Samoa Standard Time (UTC-11)",},
13 | { key: -10, value: "Hawaii-Aleutian Standard Time (UTC-10)",},
14 | { key: -9, value: "Alaska Standard Time (UTC-9)",},
15 | { key: -8, value:"Pacific Standard Time (UTC-8)",},
16 | { key: -7, value: "Mountain Standard Time (UTC-7)",},
17 | { key: -6, value: "Central Standard Time (UTC-6)",},
18 | { key: -5, value: "Eastern Standard Time (UTC-5)",},
19 | { key: -4, value: "Atlantic Standard Time (UTC-4)",},
20 | { key: -3, value: "Brasilia, Argentine Time (UTC-3)",},
21 | { key: -2, value: "Fernando de Noronha, South Georgia Standard Time (UTC-2)",},
22 | { key: -1, value: "Azores, Cape Verde, Eastern Greenland Time (UTC-1)",},
23 | { key: 0, value: "Western European, Greenwich Mean Time (UTC-0)",},
24 | { key: 1, value: "Central European, Western African Time (UTC+1)",},
25 | { key: 2, value: "Central African, Eastern European Time (UTC+2)",},
26 | { key: 3, value: "Eastern African, Arabia Standard Time (UTC+3)",},
27 | { key: 4, value: "Azerbaijan, Georgia, Moscow Standard Time (UTC+4)",},
28 | { key: 5, value: "Turkmenistan, Pakistan, Uzbekistan Time (UTC+5)",},
29 | { key: 6, value: "Bangladesh, Vostok, Bhutan Time (UTC+6)",},
30 | { key: 7, value: "Indochina, West Indonesia, Christmas Island Time (UTC+7)",},
31 | { key: 8, value: "Hong Kong, Central Indonesia, Malaysia Time (UTC+8)",},
32 | { key: 9, value: "Korea-Japan Standard, East Indonesia Time (UTC+9)",},
33 | { key: 10, value: "Queensland, Victoria, Papua New Guinea Time (UTC+10)",},
34 | { key: 11, value: "Vladivostok, Pohnpei, Solomon Is. Time (UTC+11)",},
35 | { key: 12, value: "Nauru, New Zealand Standard Time (UTC+12)",},
36 | { key: 13, value: "West Samoa, Tonga, Phoenix Is. Time (UTC+12)",},
37 | { key: 14, value: "Line Is. Time (UTC+12)",},
38 | ]
39 |
40 | export { timeWindowOption, timezoneOption }
--------------------------------------------------------------------------------
/src/style/index.css:
--------------------------------------------------------------------------------
1 | /* Import Open Sans Font */
2 | @font-face {
3 | font-family: 'Open Sans';
4 | font-style: normal;
5 | font-weight: 300;
6 | src: url(../assets/fonts/Open_Sans/static/OpenSans/OpenSans-Light.ttf) format('truetype');
7 | }
8 |
9 | @font-face {
10 | font-family: 'Open Sans';
11 | font-style: normal;
12 | font-weight: 400;
13 | src: url(../assets/fonts/Open_Sans/static/OpenSans/OpenSans-Regular.ttf) format('truetype');
14 | }
15 |
16 | @font-face {
17 | font-family: 'Open Sans';
18 | font-style: normal;
19 | font-weight: 500;
20 | src: url(../assets/fonts/Open_Sans/static/OpenSans/OpenSans-Medium.ttf) format('truetype');
21 | }
22 |
23 | @font-face {
24 | font-family: 'Open Sans';
25 | font-style: normal;
26 | font-weight: 600;
27 | src: url(../assets/fonts/Open_Sans/static/OpenSans/OpenSans-SemiBold.ttf) format('truetype');
28 | }
29 |
30 | @font-face {
31 | font-family: 'Open Sans';
32 | font-style: normal;
33 | font-weight: 700;
34 | src: url(../assets/fonts/Open_Sans/static/OpenSans/OpenSans-Bold.ttf) format('truetype');
35 | }
36 |
37 | html, body {
38 | min-height: 100%;
39 | height: max-content;
40 | width: 100%;
41 | padding: 0;
42 | margin: 0;
43 | background: #FAFAFA;
44 | color: #444;
45 | -webkit-font-smoothing: antialiased;
46 | -moz-osx-font-smoothing: grayscale;
47 | }
48 |
49 | * {
50 | box-sizing: border-box;
51 | }
52 |
53 | #app {
54 | min-height: 100%;
55 | height: max-content;
56 | }
57 |
58 | #app .pro-sidebar-inner {
59 | background: #ffff;
60 | }
61 |
62 | #app .pro-sidebar {
63 | color: black;
64 | }
65 |
66 | #app #navArrow-header {
67 | color: black;
68 | position: absolute;
69 | right: 0;
70 | z-index: 9999;
71 | line-height: 20px;
72 | border-radius: 50%;
73 | font-weight: bold;
74 | font-size: 22px;
75 | top:auto;
76 | cursor: pointer;
77 | }
78 |
79 | #app .pro-sidebar-header {
80 | display: flex;
81 | justify-content: center;
82 | align-items: center;
83 | }
84 |
85 | #app .pro-sidebar .pro-menu a:hover {
86 | color: #226666;
87 | }
88 |
89 | #app .pro-sidebar .pro-menu .pro-menu-item.active {
90 | color: #226666;
91 | }
92 |
93 | #app .pro-sidebar .pro-menu .pro-menu-item > .pro-inner-item:hover {
94 | color: #226666;
95 | font-weight: bold;
96 | }
97 |
98 | #app .pro-sidebar .pro-menu .pro-menu-item > .pro-inner-item:focus {
99 | color: #226666;
100 | }
--------------------------------------------------------------------------------
/src/routes/verifyUser/index.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { useState } from 'preact/hooks';
3 | import { useEffect } from "react";
4 | import BASE_URL from '../../config/api/constant.js';
5 | import axios from "axios";
6 | import style from './style.css';
7 |
8 | import SuccessPage from '../../components/successPage/index.js';
9 | import InvalidPage from '../../components/invalidPage/index.js';
10 | import LoadingPage from '../../components/loadingPage/index.js';
11 | import { Button } from '@chakra-ui/react';
12 | import { Link } from 'preact-router';
13 |
14 | const VerifyUser = () => {
15 | const paramVerifyToken = new URLSearchParams(window.location.search).get('key')
16 | const [verifyToken, setVerifyToken] = useState(null)
17 | const [response, setResponse] = useState({})
18 | const [isLoading, setIsLoading] = useState(true)
19 |
20 | useEffect(() => {
21 | if (verifyToken != null) {
22 | const data = {
23 | key: verifyToken,
24 | }
25 | axios.post(`${BASE_URL}/register/verify`, data)
26 | .then(() => {
27 | setResponse({
28 | success: true
29 | })
30 | setIsLoading(false)
31 | })
32 | .catch(() => {
33 | setResponse({
34 | success: false
35 | })
36 | setIsLoading(false)
37 | })
38 | }
39 |
40 | }, [verifyToken])
41 |
42 | if (paramVerifyToken){
43 | setVerifyToken(paramVerifyToken);
44 | }
45 |
46 | return (
47 | {!paramVerifyToken ?
48 |
50 | : isLoading ?
51 |
52 | : response.success ?
53 |
56 | :
57 |
59 | }
60 |
61 | {!isLoading &&
62 |
63 |
64 | Back to login page
65 |
66 |
67 | }
68 |
)
69 | }
70 |
71 | export default VerifyUser;
--------------------------------------------------------------------------------
/src/components/teamMemberComponent/index.js:
--------------------------------------------------------------------------------
1 | import { h, Fragment } from 'preact';
2 | import { Flex, Spacer, Box, CloseButton, Text, Heading, Spinner } from '@chakra-ui/react';
3 | import { getUserToken } from '../../config/api/auth.js';
4 | import axios from "axios";
5 | import BASE_URL from '../../config/api/constant.js';
6 | import { useState } from 'preact/hooks';
7 |
8 | function TeamMemberComponent({email=null, verified=null, cancelUserId=null, header=false, refreshTeamData=()=>{}}){
9 | /*
10 | email= user name
11 | verified = Split into 2 case:
12 | true --> give green color and "member"
13 | false --> give grey color and "pending"
14 | cancelUserId = user_id that is not yet verified, only used when verified=false
15 | */
16 |
17 | const [isLoadingDelete, setIsLoadingDelete] = useState(false);
18 | if (header){
19 | return(
20 |
21 |
22 | Members
23 |
24 |
25 |
26 | Status
27 |
28 |
29 |
30 | )
31 | }
32 |
33 |
34 | const removeUser = () => {
35 | setIsLoadingDelete(true);
36 | const data = {
37 | user_id: cancelUserId
38 | }
39 |
40 | axios.post(`${BASE_URL}/invite-member/cancel/`, data, {
41 | headers: {
42 | Authorization: `Token ${getUserToken()}`
43 | }
44 | }).then(() => {
45 | refreshTeamData();
46 | })
47 | }
48 |
49 | return(
50 |
51 |
52 | {email}
53 |
54 |
55 | {verified ?
56 | <>
57 |
58 | member
59 |
60 |
61 | >
62 | :
63 | <>
64 |
65 | pending
66 |
67 |
68 | {isLoadingDelete ?
69 |
70 | :
71 |
72 | }
73 |
74 | >
75 | }
76 |
77 | )
78 | }
79 |
80 | export default TeamMemberComponent;
--------------------------------------------------------------------------------
/src/routes/acceptInvite/index.js:
--------------------------------------------------------------------------------
1 | import { h } from "preact";
2 | import { useState } from 'preact/hooks';
3 | import { useEffect } from "react";
4 | import BASE_URL from '../../config/api/constant.js';
5 | import axios from "axios";
6 | import style from './style.css';
7 |
8 | import SuccessPage from '../../components/successPage/index.js';
9 | import InvalidPage from '../../components/invalidPage/index.js';
10 | import LoadingPage from '../../components/loadingPage/index.js';
11 | import { Link } from "preact-router";
12 | import { Button } from "@chakra-ui/react";
13 |
14 | const AcceptInvite = () => {
15 | const paramInviteToken = new URLSearchParams(window.location.search).get('key')
16 | const [inviteToken, setInviteToken] = useState(null)
17 | const [response, setResponse] = useState({})
18 | const [isLoading, setIsLoading] = useState(true)
19 |
20 | if (paramInviteToken){
21 | setInviteToken(paramInviteToken);
22 | }
23 |
24 | useEffect(() => {
25 | if (inviteToken != null) {
26 | const data = {
27 | key: inviteToken,
28 | }
29 | axios.post(`${BASE_URL}/invite-member/accept/`, data)
30 | .then(() => {
31 | setResponse({
32 | success: true
33 | })
34 | setIsLoading(false)
35 | })
36 | .catch(() => {
37 | setResponse({
38 | success: false
39 | })
40 | setIsLoading(false)
41 | })
42 | }
43 |
44 | }, [inviteToken])
45 |
46 | return (
47 | {!paramInviteToken ?
48 |
50 | : isLoading ?
51 |
52 | : response.success ?
53 |
56 | :
57 |
60 | }
61 |
62 | {!isLoading &&
63 |
64 |
65 | Back to login page
66 |
67 |
68 | }
69 |
)
70 | }
71 |
72 | export default AcceptInvite;
--------------------------------------------------------------------------------
/tests/routes/verifyUser/verifyUser.test.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { screen, waitFor, render } from '@testing-library/preact';
3 | import * as axios from 'axios';
4 | import { getCurrentUrl, route } from 'preact-router';
5 | import App from '../../../src/components/app.js';
6 |
7 | jest.mock("axios");
8 |
9 | describe("Test Verify User", () => {
10 |
11 | test('when processing data then show loader', async() => {
12 | axios.post.mockImplementation(() =>
13 | new Promise((resolve) => {
14 | setTimeout(()=> resolve({
15 | status: 200,
16 | response: {
17 | data: {
18 | success: true
19 | }
20 | }
21 | }), 3000)
22 | })
23 | )
24 |
25 | render( )
26 | route('/verify?key=abcdefg')
27 | await waitFor(async () => {
28 | expect(getCurrentUrl()).toBe('/verify?key=abcdefg')
29 | expect(screen.getByText('Loading')).toBeDefined()
30 | }, 5000)
31 | })
32 |
33 | test('if token is not passed show block page', async() => {
34 |
35 | render( )
36 | route('/verify')
37 |
38 | await waitFor(async () => {
39 | expect(screen.getByText('Token Not Passed')).toBeDefined()
40 | }, 50000)
41 | })
42 |
43 | test('if token is valid show success page', async() => {
44 |
45 | axios.post.mockImplementation(() => Promise.resolve({
46 | status: 200,
47 | response: {
48 | data: {
49 | success: true
50 | }
51 | }
52 | }))
53 |
54 | render( )
55 | route('/verify?key=abcdefg')
56 |
57 | await waitFor(async () => {
58 | expect(getCurrentUrl()).toBe('/verify?key=abcdefg')
59 | expect(screen.getByText('Loading')).toBeDefined()
60 | }, 5000)
61 |
62 | await waitFor(async () => {
63 | expect(screen.getByText('User Email Verified')).toBeDefined()
64 | }, 50000)
65 |
66 | })
67 |
68 | test('if token is invalid show fail page', async() => {
69 |
70 | axios.post.mockImplementation(() => Promise.reject({
71 | status: 400,
72 | response: {
73 | data: {
74 | error: "Invalid Token"
75 | }
76 | }
77 | }))
78 |
79 | render( )
80 | route('/verify?key=abcdefg')
81 |
82 | await waitFor(async () => {
83 | expect(getCurrentUrl()).toBe('/verify?key=abcdefg')
84 | expect(screen.getByText('Loading')).toBeDefined()
85 | }, 5000)
86 |
87 | await waitFor(async () => {
88 | expect(screen.getByText('Token Invalid')).toBeDefined()
89 | }, 50000)
90 |
91 | })
92 |
93 | })
--------------------------------------------------------------------------------
/tests/routes/acceptInvite/acceptInvite.test.js:
--------------------------------------------------------------------------------
1 | import { h } from "preact";
2 | import { screen, waitFor, render } from '@testing-library/preact';
3 | import * as axios from 'axios';
4 | import { getCurrentUrl, route } from 'preact-router';
5 | import { deleteUserToken } from '../../../src/config/api/auth.js';
6 | import App from '../../../src/components/app.js';
7 |
8 | jest.mock("axios");
9 | describe("Test Accept Invite", () => {
10 |
11 | test('when processing data then show loader', async() => {
12 | axios.post.mockImplementation(() =>
13 | new Promise((resolve) => {
14 | setTimeout(()=> resolve({
15 | status: 200,
16 | response: {
17 | data: {
18 | success: true
19 | }
20 | }
21 | }), 3000)
22 | })
23 | )
24 |
25 | render( )
26 | route('/invite-member?key=abcdefg')
27 | await waitFor(async () => {
28 | expect(getCurrentUrl()).toBe('/invite-member?key=abcdefg')
29 | expect(screen.getByText('Loading')).toBeDefined()
30 | }, 5000)
31 | })
32 |
33 | test('if token is not passed show block page', async() => {
34 | deleteUserToken()
35 |
36 | render( )
37 | route('/invite-member')
38 |
39 | await waitFor(async () => {
40 | expect(screen.getByText('Token Not Passed')).toBeDefined()
41 | }, 50000)
42 | })
43 |
44 | test('if token is valid show success page', async() => {
45 | deleteUserToken()
46 | axios.post.mockImplementation(() => Promise.resolve({
47 | status: 200,
48 | response: {
49 | data: {
50 | success: true
51 | }
52 | }
53 | }))
54 |
55 | render( )
56 | route('/invite-member?key=abcdefg')
57 |
58 | await waitFor(async () => {
59 | expect(getCurrentUrl()).toBe('/invite-member?key=abcdefg')
60 | expect(screen.getByText('Loading')).toBeDefined()
61 | }, 5000)
62 |
63 | await waitFor(async () => {
64 | expect(screen.getByText('Team Invite Accepted')).toBeDefined()
65 | }, 50000)
66 |
67 | })
68 |
69 | test('if token is invalid show fail page', async() => {
70 | deleteUserToken()
71 | axios.post.mockImplementation(() => Promise.reject({
72 | status: 400,
73 | response: {
74 | data: {
75 | error: "Invalid Token"
76 | }
77 | }
78 | }))
79 |
80 | render( )
81 | route('/invite-member?key=abcdefg')
82 |
83 | await waitFor(async () => {
84 | expect(getCurrentUrl()).toBe('/invite-member?key=abcdefg')
85 | expect(screen.getByText('Loading')).toBeDefined()
86 | }, 5000)
87 |
88 | await waitFor(async () => {
89 | expect(screen.getByText('Token Invalid')).toBeDefined()
90 | }, 50000)
91 |
92 | })
93 |
94 | })
--------------------------------------------------------------------------------
/src/components/forms/fileInput/index.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { FormControl, FormLabel, FormErrorMessage, Box, InputGroup, InputLeftElement, Icon, Flex } from '@chakra-ui/react';
3 | import { useController } from "react-hook-form";
4 | import { FiFile } from 'react-icons/fi'
5 | import { Input } from '@chakra-ui/input';
6 | import { useRef, useState } from 'preact/hooks';
7 | import Asterisk from '../asterisk';
8 | import logo from '../../../assets/icons/logo-monapi.svg';
9 |
10 | const FileInput = (props) => {
11 | const { id, errors, rules, accept, w, multiple, title, control,
12 | placeholder, textChange, hasLabel = true, description, defaultImage } = props;
13 |
14 | const [img, setImg] = useState(defaultImage != undefined ? defaultImage : logo)
15 |
16 | const handleChangeImage = (e) => {
17 | if (e.target.files[0]) {
18 | onChange(e.target.files[0])
19 | setImg(URL.createObjectURL(e.target.files[0]))
20 | }
21 | }
22 |
23 | const inputRef = useRef()
24 | const {
25 | field: { ref, onChange, value, ...inputProps },
26 | } = useController({
27 | name: id,
28 | control,
29 | rules,
30 | });
31 | return (
32 |
33 |
34 | {hasLabel &&
35 | {title}
36 | }
37 |
38 |
39 |
40 | Selected: {value && value.name || '-'}
41 |
42 |
43 |
44 |
45 |
46 |
47 | handleChangeImage(e)}
50 | type='file'
51 | accept={accept}
52 | multiple={multiple || false}
53 | name={id}
54 | ref={inputRef}
55 | {...inputProps}
56 | inputRef={ref}
57 | style={{ display: 'none' }}
58 | />
59 |
60 | inputRef.current.click()}
62 | readOnly={true}
63 | bgColor='#F1F1F1'
64 | cursor="pointer"
65 | value={value ? textChange : placeholder}
66 | id={id}
67 | >
68 |
69 | {description}
70 |
71 |
72 |
73 | {errors[id] && errors[id].message}
74 |
75 |
76 | );
77 |
78 | }
79 |
80 | export default FileInput;
--------------------------------------------------------------------------------
/src/components/chart/response_time_chart/index.js:
--------------------------------------------------------------------------------
1 | import { createRef, h } from 'preact';
2 | import { useEffect, useState } from 'preact/hooks';
3 | import Chart from 'chart.js/auto';
4 | import "chartjs-adapter-date-fns";
5 | import ChartConfig from '../ChartConfig';
6 |
7 | const ResponseTimeChart = ({response_time, stepSizeInSecond}) => {
8 | const [chart, setChart] = useState(null);
9 |
10 | if (response_time == undefined) {
11 | return (
)
12 | }
13 |
14 | const parseISODateToChart = (isoDate) => {
15 | const date = new Date(isoDate);
16 | const year = date.getFullYear();
17 | const month = date.getMonth() + 1;
18 | const dt = date.getDate();
19 | const hour = date.getHours();
20 | const minute =date.getMinutes();
21 |
22 | return `${year}-${month.toString().padStart(2,'0')}-${dt.toString().padStart(2, '0')} ${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`
23 | }
24 |
25 |
26 | function responseTimeList(response_time) {
27 | const listOfNumber = []
28 | response_time.forEach(dict => {
29 | if (dict.avg === 0) {
30 | listOfNumber.push({
31 | x: parseISODateToChart(dict.start_time),
32 | y: -1,
33 | })
34 | } else {
35 | listOfNumber.push({
36 | x: parseISODateToChart(dict.start_time),
37 | y: dict.avg,
38 | })
39 | }
40 | });
41 | return listOfNumber
42 | }
43 |
44 | const chartRef = createRef();
45 |
46 | useEffect(() => {
47 | const ctx = chartRef.current.getContext("2d");
48 |
49 | if (chart != null) { chart.destroy() }
50 |
51 | const data = responseTimeList(response_time)
52 | const labels = data.slice()
53 |
54 | const colors=[];
55 | for (let i=0; i < data.length; i++) {
56 | labels[i] = '';
57 | if (data[i].y === -1) {
58 | colors[i] = "grey";
59 | data[i].y += 1;
60 | labels[i] = "No Data";
61 | } else {
62 | colors[i] = "#36CB72";
63 | }
64 | }
65 |
66 | const y_scale_config = {
67 | beginAtZero: true,
68 | ticks: {
69 | callback(value) {
70 | return `${value} ms`;
71 | }
72 | }
73 | }
74 |
75 | const xScaleConfig = {
76 | min: parseISODateToChart(response_time[0].start_time),
77 | max: parseISODateToChart(response_time[response_time.length - 1].start_time),
78 | type: 'time',
79 | time: {
80 | unit: 'second',
81 | stepSize: stepSizeInSecond,
82 | displayFormats: {
83 | minute: 'HH:mm aaa',
84 | second: 'HH:mm aaa',
85 | hour: 'HH:mm aaa'
86 | }
87 | }
88 | }
89 |
90 | const tooltipConfig = {
91 | callbacks: {
92 | title: (context) => {
93 | return labels[context[0].dataIndex];
94 | }
95 | }
96 | }
97 |
98 | const responseTimeChart = new Chart(ctx, ChartConfig("Response Time", labels, data, colors, xScaleConfig, y_scale_config, tooltipConfig))
99 |
100 | setChart(responseTimeChart);
101 | }, [response_time])
102 |
103 | return (
104 |
109 | )
110 | }
111 |
112 | export default ResponseTimeChart;
--------------------------------------------------------------------------------
/src/assets/icons/logo-monapi.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/components/chart/success_rate_percentage_chart/index.js:
--------------------------------------------------------------------------------
1 | import { createRef, h } from 'preact';
2 | import { useEffect, useState } from 'preact/hooks';
3 | import Chart from 'chart.js/auto';
4 | import "chartjs-adapter-date-fns";
5 | import ChartConfig from '../ChartConfig'
6 |
7 | const SuccessRatePercentageChart = ({success_rate, stepSizeInSecond}) => {
8 | const [chart, setChart] = useState(null);
9 |
10 | if (success_rate == undefined) {
11 | return (
)
12 | }
13 |
14 | function successRateDictToPercentageNumber(dict) {
15 | if (dict.success+dict.failed === 0) {
16 | return -1;
17 | }
18 | return dict.success/(dict.success+dict.failed)
19 | }
20 |
21 | const parseISODateToChart = (isoDate) => {
22 | const date = new Date(isoDate);
23 | const year = date.getFullYear();
24 | const month = date.getMonth() + 1;
25 | const dt = date.getDate();
26 | const hour = date.getHours();
27 | const minute = date.getMinutes();
28 |
29 | return `${year}-${month.toString().padStart(2,'0')}-${dt.toString().padStart(2, '0')} ${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`
30 | }
31 |
32 | function successRateList(success_rate) {
33 | const listOfNumber = []
34 | success_rate.forEach(dict => {
35 | listOfNumber.push({
36 | x: parseISODateToChart(dict.start_time),
37 | y: successRateDictToPercentageNumber(dict)
38 | });
39 | });
40 | return listOfNumber
41 | }
42 |
43 | const chartRef = createRef()
44 |
45 | useEffect(() => {
46 | const ctx = chartRef.current.getContext("2d");
47 |
48 | if (chart != null) { chart.destroy() }
49 |
50 | const data = successRateList(success_rate)
51 | const labels = data.slice()
52 |
53 | const colors=[];
54 | for (let i=0; i < data.length; i++) {
55 | labels[i] = '';
56 |
57 | if (data[i].y == 1) {
58 | colors[i] = "#36CB72";
59 | } else if (data[i].y === -1) {
60 | colors[i] = "grey";
61 | data[i].y += 1;
62 | labels[i] = "No Data"
63 | } else if (data[i].y == 0) {
64 | colors[i] = "#CB5136";
65 | } else {
66 | colors[i] = "yellow";
67 | }
68 | }
69 |
70 | const y_scale_config = {
71 | beginAtZero: true,
72 | max: 1,
73 | ticks: {
74 | format: {
75 | style: 'percent'
76 | }
77 | }
78 | }
79 |
80 | const tooltipConfig = {
81 | callbacks: {
82 | title: (context) => {
83 | return labels[context[0].dataIndex];
84 | }
85 | }
86 | }
87 |
88 | const xScaleConfig = {
89 | min: parseISODateToChart(success_rate[0].start_time),
90 | max: parseISODateToChart(success_rate[success_rate.length - 1].start_time),
91 | type: 'time',
92 | time: {
93 | unit: 'second',
94 | stepSize: stepSizeInSecond,
95 | displayFormats: {
96 | minute: 'HH:mm aaa',
97 | hour: 'HH:mm aaa',
98 | second: 'HH:mm aaa',
99 | }
100 | }
101 | }
102 | const successRateChart = new Chart(ctx, ChartConfig( "Success Rate", labels, data, colors, xScaleConfig, y_scale_config, tooltipConfig))
103 |
104 | setChart(successRateChart);
105 | }, [success_rate])
106 |
107 | return (
108 |
113 | )
114 | }
115 |
116 | export default SuccessRatePercentageChart;
--------------------------------------------------------------------------------
/src/components/app.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { Router } from 'preact-router';
3 | import { ChakraProvider } from '@chakra-ui/react';
4 | import * as Sentry from "@sentry/react";
5 | import { BrowserTracing } from "@sentry/tracing";
6 | import theme from '../config/theme';
7 | import ROUTE from '../config/api/route';
8 | import { AuthenticationChecker } from '../config/middleware/middleware';
9 | import DashboardWrapper from './dashboardWrapper';
10 | import ViewListMonitor from '../routes/view_list_monitor';
11 | import Login from '../routes/login';
12 | import Register from '../routes/register';
13 | import '../config/middleware/axios';
14 | import ViewAPIMonitorDetail from '../routes/view_api_monitor_detail/index.js';
15 | import ErrorLogs from '../routes/error_logs/index.js';
16 | import CreateAPIMonitor from '../routes/createAPIMonitor';
17 | import EditAPIMonitor from '../routes/editAPIMonitor';
18 | import Configuration from '../routes/configuration/index.js';
19 | import ForgetPassword from '../routes/forgetPassword/index.js';
20 | import ForgetPasswordToken from '../routes/forgetPasswordToken/index.js';
21 | import Redirect from './redirect/index.js';
22 | import TestAPI from '../routes/a_test_api/index.js';
23 | import CreateNewTeam from '../routes/createTeam/index.js';
24 | import ViewCurrentTeam from '../routes/view_current_team/index.js';
25 | import { UserContext } from '../config/context';
26 | import { useState } from 'preact/hooks';
27 | import AcceptInvite from '../routes/acceptInvite/index.js';
28 | import StatusPageDashboard from '../routes/statusPageDashboard';
29 | import EditTeam from '../routes/editTeam/index.js';
30 | import StatusPage from '../routes/statusPage/index.js';
31 | import VerifyUser from '../routes/verifyUser/index.js';
32 |
33 | const App = () => {
34 | const sentryDSN = process.env.PREACT_APP_SENTRY_DSN;
35 | const googleAnalyticsTag = process.env.PREACT_APP_GOOGLE_ANALYTICS_TAG;
36 | const [currentTeamId, setCurrentTeamId] = useState()
37 | if (sentryDSN !== "") {
38 | Sentry.init({
39 | dsn: sentryDSN,
40 | integrations: [new BrowserTracing()],
41 |
42 | // Set tracesSampleRate to 1.0 to capture 100%
43 | // of transactions for performance monitoring.
44 | // We recommend adjusting this value in production
45 | tracesSampleRate: 1.0,
46 | });
47 | }
48 |
49 | return (
50 | {googleAnalyticsTag !== "" &&
51 |
52 |
59 |
}
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
);
96 | }
97 |
98 | export default App;
99 |
--------------------------------------------------------------------------------
/src/routes/forgetPasswordToken/index.js:
--------------------------------------------------------------------------------
1 | import { h } from "preact";
2 | import { useState } from "preact/hooks";
3 | import PasswordInput from '../../components/forms/passwordinput/index.js'
4 | import { Button, Text, Spinner,Box, Flex, Grid, GridItem } from '@chakra-ui/react';
5 | import { useForm } from 'react-hook-form';
6 | import { route } from 'preact-router';
7 | import ROUTE from '../../config/api/route.js';
8 | import BASE_URL from '../../config/api/constant.js';
9 | import * as axios from "axios";
10 |
11 | const ForgetPasswordToken = ()=>{
12 | const [errl, setErrl] = useState([])
13 | const [isLoading, setIsLoading] = useState(false);
14 | const [key, setKey] = useState("")
15 |
16 | const param = new URLSearchParams(window.location.search).get('key')
17 | setKey(param)
18 |
19 | const {
20 | handleSubmit,
21 | register,
22 | formState: { errors },
23 | } = useForm();
24 |
25 | const onSubmit = (res) => {
26 | setIsLoading(true)
27 | const data = {
28 | key,
29 | password: res.password
30 | }
31 |
32 | axios.post(`${BASE_URL}/forget-password/change/`, data)
33 | .then(() => {
34 | route(`${ROUTE.LOGIN}?isChangePassword=true`)
35 | setIsLoading(false)
36 | })
37 | .catch((error) => {
38 | if (error.response.data['password']) {
39 | setErrl(error.response.data.password)
40 | } else if (error.response.data['error']) {
41 | setErrl([error.response.data.error])
42 | }
43 | setIsLoading(false)
44 | });
45 |
46 | return data
47 | }
48 |
49 | return (
50 |
56 |
58 |
61 |
62 |
63 | Forget Password
64 |
65 |
66 |
76 |
77 | {errl.length != 0 && (
78 |
79 |
Error:
80 |
81 | {errl.map((e,i) => (
82 | {e}
83 | ))}
84 |
85 |
86 | )}
87 |
88 |
89 | {isLoading ? : "Change Password"}
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | Get Better Insight About Your API Performance in Seconds
98 |
99 |
100 |
101 |
102 | );
103 | }
104 |
105 |
106 | export default ForgetPasswordToken;
--------------------------------------------------------------------------------
/src/routes/editTeam/index.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { Button, Spinner, Text, Box, Flex, Grid, GridItem, FormLabel } from '@chakra-ui/react';
3 | import { useForm } from 'react-hook-form';
4 | import { useState, useEffect } from 'preact/hooks';
5 | import axios from 'axios';
6 | import { route } from 'preact-router';
7 | import BASE_URL from '../../config/api/constant.js';
8 | import ROUTE from '../../config/api/route.js';
9 | import { getUserToken } from '../../config/api/auth.js';
10 | import TeamEditor from '../../components/teamEditor/index.js';
11 |
12 | const EditTeam = ({id}) => {
13 | const [isLoadingEdit, setLoadingEdit] = useState(false);
14 | const {
15 | handleSubmit,
16 | register,
17 | control,
18 | formState: { errors },
19 | } = useForm({
20 | defaultValues: {
21 | logo: null,
22 | }
23 | });
24 |
25 | const [currentTeam, setCurrentTeam] = useState()
26 |
27 | useEffect(()=>{
28 | axios.get(`${BASE_URL}/team-management/current/`, {
29 | headers: {
30 | Authorization:`Token ${getUserToken()}`
31 | }
32 | }).then((response)=>{
33 | setCurrentTeam(response.data)
34 | })
35 | },[])
36 |
37 |
38 | const onSubmit = (data) => {
39 | setLoadingEdit(true);
40 | let formData = new FormData()
41 | formData.append('description', data.description)
42 |
43 | if (data.logo) {
44 | formData.append('logo', data.logo)
45 | }
46 |
47 | axios.put(`${BASE_URL}/team-management/${id}/`, formData, {
48 | headers: {
49 | 'Content-Type': 'multipart/form-data',
50 | 'Authorization':`Token ${getUserToken()}`
51 | }
52 | }).then(()=>{
53 | route(ROUTE.TEAM_MANAGEMENT + '/current');
54 | setLoadingEdit(false)
55 | })
56 |
57 | };
58 |
59 | return (
60 |
61 |
67 | {currentTeam != undefined &&
68 |
69 |
70 |
71 | Edit New Team
72 |
73 |
74 |
75 |
76 | Team Name
77 |
78 | {currentTeam?.name}
79 |
80 |
81 |
82 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | {isLoadingEdit ? : 'Save' }
99 |
100 |
101 |
102 |
103 | }
104 |
105 |
106 | );
107 |
108 | }
109 |
110 |
111 | export default EditTeam;
--------------------------------------------------------------------------------
/src/routes/createTeam/index.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { Button, Spinner, Text, Box, Flex, Grid, GridItem } from '@chakra-ui/react';
3 | import { useForm } from 'react-hook-form';
4 | import { useState } from 'preact/hooks';
5 | import axios from 'axios';
6 | import { route } from 'preact-router';
7 |
8 | import TextInput from '../../components/forms/textinput/index.js';
9 | import TeamEditor from '../../components/teamEditor/index.js';
10 | import BASE_URL from '../../config/api/constant.js';
11 | import ROUTE from '../../config/api/route.js';
12 | import { getUserToken } from '../../config/api/auth.js';
13 |
14 | const CreateTeam = () => {
15 | const [isLoadingCreate, setLoadingCreate] = useState(false);
16 |
17 | const {
18 | handleSubmit,
19 | register,
20 | control,
21 | formState: { errors },
22 | } = useForm({
23 | defaultValues: {
24 | name: "",
25 | description: "",
26 | logo: null,
27 | }
28 | });
29 |
30 | const onSubmit = (data) => {
31 | setLoadingCreate(true);
32 | let formData = new FormData()
33 | formData.append('name', data.name)
34 | formData.append('description', data.description)
35 | if (data.logo) {
36 | formData.append('logo', data.logo)
37 | }
38 |
39 | axios.post(`${BASE_URL}/team-management/`, formData, {
40 | headers: {
41 | 'Content-Type': 'multipart/form-data',
42 | 'Authorization':`Token ${getUserToken()}`
43 | }
44 | }).then(()=>{
45 | route(`${ROUTE.TEAM_MANAGEMENT}/current`);
46 | setLoadingCreate(false);
47 | window.location.reload();
48 | })
49 | };
50 |
51 | return (
52 |
53 |
59 |
60 |
61 |
62 |
63 | Create New Team
64 |
65 |
66 |
67 |
78 |
79 |
80 |
81 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | {isLoadingCreate ? : 'Create' }
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | );
102 |
103 | }
104 |
105 |
106 | export default CreateTeam;
--------------------------------------------------------------------------------
/tests/routes/createTeam/createTeam.test.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { render, waitFor, screen } from '@testing-library/preact';
3 | import CreateTeam from '../../../src/routes/createTeam/index.js';
4 | import userEvent from '@testing-library/user-event'
5 | import * as axios from 'axios';
6 |
7 | jest.mock('axios')
8 |
9 | describe('Test input', () => {
10 | test('When user doesnt fill the team name, then error raises', async () => {
11 | render( );
12 |
13 | const Create = screen.getByText('Create');
14 | userEvent.click(Create);
15 | await waitFor(() => {
16 | //required for the team name, whereas non-required for others, so just one error
17 | expect(screen.getAllByText("Required")).toHaveLength(1);
18 | });
19 | });
20 |
21 | test('When user fill the team name, then success', async () => {
22 | const response = {
23 | name: "myteam",
24 | description: "myDesc",
25 | logo:null
26 | }
27 | const mockPost = jest.fn()
28 | axios.post.mockImplementation(() => {
29 | mockPost()
30 | return Promise.resolve({data: response})
31 | })
32 |
33 | render( );
34 |
35 | const nameField = await screen.findByPlaceholderText('Insert Team Name');
36 | userEvent.type(nameField, 'myteam')
37 |
38 | const Create = screen.getByText('Create');
39 | userEvent.click(Create);
40 | await waitFor(() => {
41 | expect(mockPost).toHaveBeenCalledTimes(0)
42 | });
43 | });
44 |
45 | test('When user fill inputs completely, then success', async () => {
46 | render( );
47 |
48 | const nameField = await screen.findByPlaceholderText('Insert Team Name');
49 | userEvent.type(nameField, 'myteam')
50 |
51 | const descField = await screen.findByPlaceholderText('Insert Team Description');
52 | userEvent.type(descField, 'myDesc')
53 |
54 | global.URL.createObjectURL = jest.fn();
55 | const imageFile = new File([new ArrayBuffer(1)], 'hello.png', { type: "image/png" });
56 |
57 | const fileField = await screen.findByPlaceholderText('Select Image');
58 |
59 | // test file input is optional
60 | userEvent.upload(fileField, null)
61 |
62 | // test file input can upload image file
63 | userEvent.upload(fileField, imageFile)
64 |
65 | const response = {
66 | name: "myteam",
67 | description: "myDesc",
68 | logo:imageFile
69 | }
70 | const mockPost = jest.fn()
71 | axios.post.mockImplementation(() => {
72 | mockPost()
73 | return Promise.resolve({data: response})
74 | })
75 |
76 | const Create = screen.getByText('Create');
77 | userEvent.click(Create);
78 |
79 | await waitFor(() => {
80 | expect(mockPost).toHaveBeenCalledTimes(1);
81 | });
82 | });
83 |
84 | test('When user upload image file size more than 10MB, then failed', async () => {
85 | render( );
86 |
87 | const nameField = await screen.findByPlaceholderText('Insert Team Name');
88 | userEvent.type(nameField, 'myteam')
89 |
90 | global.URL.createObjectURL = jest.fn();
91 |
92 | // create new file that have size more than 10MB, specificly 10MB + 1b
93 | const imageFile = new File([new ArrayBuffer(1024 * 1024 * 10 + 1)], 'hello.png', { type: "image/png" });
94 |
95 | const fileField = await screen.findByPlaceholderText('Select Image');
96 |
97 | // test file input can upload image file
98 | userEvent.upload(fileField, imageFile)
99 |
100 | const Create = screen.getByText('Create');
101 | userEvent.click(Create);
102 |
103 | await waitFor(() => {
104 | expect(screen.getByText("Please choose image that the file size less than or equal to 10MB")).toBeDefined()
105 | });
106 | });
107 |
108 | test('When user click Create button, then show spinner to loading', async () => {
109 | let response = {
110 | name: "myteam"
111 | }
112 | axios.post.mockImplementation(() => {
113 | return new Promise((resolve)=> {
114 | setTimeout(() => resolve({
115 | data: response
116 | }), 3000)
117 | })
118 | });
119 |
120 | render( );
121 |
122 | const nameField = await screen.findByPlaceholderText('Insert Team Name');
123 | userEvent.type(nameField, 'myteam')
124 |
125 | const Create = screen.getByText('Create');
126 | userEvent.click(Create);
127 |
128 | await waitFor(() => {
129 | expect(screen.getByText('Loading...')).toBeDefined()
130 | })
131 | })
132 |
133 | })
--------------------------------------------------------------------------------
/src/assets/fonts/Open_Sans/README.txt:
--------------------------------------------------------------------------------
1 | Open Sans Variable Font
2 | =======================
3 |
4 | This download contains Open Sans as both variable fonts and static fonts.
5 |
6 | Open Sans is a variable font with these axes:
7 | wdth
8 | wght
9 |
10 | This means all the styles are contained in these files:
11 | OpenSans-VariableFont_wdth,wght.ttf
12 | OpenSans-Italic-VariableFont_wdth,wght.ttf
13 |
14 | If your app fully supports variable fonts, you can now pick intermediate styles
15 | that aren’t available as static fonts. Not all apps support variable fonts, and
16 | in those cases you can use the static font files for Open Sans:
17 | static/OpenSans_Condensed/OpenSans_Condensed-Light.ttf
18 | static/OpenSans_Condensed/OpenSans_Condensed-Regular.ttf
19 | static/OpenSans_Condensed/OpenSans_Condensed-Medium.ttf
20 | static/OpenSans_Condensed/OpenSans_Condensed-SemiBold.ttf
21 | static/OpenSans_Condensed/OpenSans_Condensed-Bold.ttf
22 | static/OpenSans_Condensed/OpenSans_Condensed-ExtraBold.ttf
23 | static/OpenSans_SemiCondensed/OpenSans_SemiCondensed-Light.ttf
24 | static/OpenSans_SemiCondensed/OpenSans_SemiCondensed-Regular.ttf
25 | static/OpenSans_SemiCondensed/OpenSans_SemiCondensed-Medium.ttf
26 | static/OpenSans_SemiCondensed/OpenSans_SemiCondensed-SemiBold.ttf
27 | static/OpenSans_SemiCondensed/OpenSans_SemiCondensed-Bold.ttf
28 | static/OpenSans_SemiCondensed/OpenSans_SemiCondensed-ExtraBold.ttf
29 | static/OpenSans/OpenSans-Light.ttf
30 | static/OpenSans/OpenSans-Regular.ttf
31 | static/OpenSans/OpenSans-Medium.ttf
32 | static/OpenSans/OpenSans-SemiBold.ttf
33 | static/OpenSans/OpenSans-Bold.ttf
34 | static/OpenSans/OpenSans-ExtraBold.ttf
35 | static/OpenSans_Condensed/OpenSans_Condensed-LightItalic.ttf
36 | static/OpenSans_Condensed/OpenSans_Condensed-Italic.ttf
37 | static/OpenSans_Condensed/OpenSans_Condensed-MediumItalic.ttf
38 | static/OpenSans_Condensed/OpenSans_Condensed-SemiBoldItalic.ttf
39 | static/OpenSans_Condensed/OpenSans_Condensed-BoldItalic.ttf
40 | static/OpenSans_Condensed/OpenSans_Condensed-ExtraBoldItalic.ttf
41 | static/OpenSans_SemiCondensed/OpenSans_SemiCondensed-LightItalic.ttf
42 | static/OpenSans_SemiCondensed/OpenSans_SemiCondensed-Italic.ttf
43 | static/OpenSans_SemiCondensed/OpenSans_SemiCondensed-MediumItalic.ttf
44 | static/OpenSans_SemiCondensed/OpenSans_SemiCondensed-SemiBoldItalic.ttf
45 | static/OpenSans_SemiCondensed/OpenSans_SemiCondensed-BoldItalic.ttf
46 | static/OpenSans_SemiCondensed/OpenSans_SemiCondensed-ExtraBoldItalic.ttf
47 | static/OpenSans/OpenSans-LightItalic.ttf
48 | static/OpenSans/OpenSans-Italic.ttf
49 | static/OpenSans/OpenSans-MediumItalic.ttf
50 | static/OpenSans/OpenSans-SemiBoldItalic.ttf
51 | static/OpenSans/OpenSans-BoldItalic.ttf
52 | static/OpenSans/OpenSans-ExtraBoldItalic.ttf
53 |
54 | Get started
55 | -----------
56 |
57 | 1. Install the font files you want to use
58 |
59 | 2. Use your app's font picker to view the font family and all the
60 | available styles
61 |
62 | Learn more about variable fonts
63 | -------------------------------
64 |
65 | https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
66 | https://variablefonts.typenetwork.com
67 | https://medium.com/variable-fonts
68 |
69 | In desktop apps
70 |
71 | https://theblog.adobe.com/can-variable-fonts-illustrator-cc
72 | https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
73 |
74 | Online
75 |
76 | https://developers.google.com/fonts/docs/getting_started
77 | https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
78 | https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
79 |
80 | Installing fonts
81 |
82 | MacOS: https://support.apple.com/en-us/HT201749
83 | Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
84 | Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
85 |
86 | Android Apps
87 |
88 | https://developers.google.com/fonts/docs/android
89 | https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
90 |
91 | License
92 | -------
93 | Please read the full license text (OFL.txt) to understand the permissions,
94 | restrictions and requirements for usage, redistribution, and modification.
95 |
96 | You can use them in your products & projects – print or digital,
97 | commercial or otherwise.
98 |
99 | This isn't legal advice, please consider consulting a lawyer and see the full
100 | license for all details.
101 |
--------------------------------------------------------------------------------
/src/routes/statusPageDashboard/index.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { Box, Text, Image, Spinner } from '@chakra-ui/react'
3 | import { useEffect, useState } from 'preact/hooks'
4 | import { Link } from 'preact-router/match'
5 | import BASE_URL from '../../config/api/constant';
6 | import SuccessRate from '../../components/success_rate';
7 | import logo from '../../assets/icons/logo-monapi.svg';
8 | import style_bar from '../../components/success_rate/style.css';
9 | import axios from 'axios';
10 | import style from './style.css';
11 |
12 | const StatusPageDashboard = ({path}) => {
13 | const [monitor, setMonitor]=useState([])
14 | const [errorMessage, setErrorMessage]=useState("")
15 | const [isLoading, setIsLoading] = useState(true);
16 |
17 | useEffect(()=>{
18 | setIsLoading(true);
19 | axios.get(`${BASE_URL}/status-page/dashboard/`, {
20 | params: {
21 | path
22 | }
23 | }).then((response) => {
24 | setMonitor(response.data)
25 | setIsLoading(false)
26 | }).catch((error) => {
27 | setErrorMessage(error.response.data.error)
28 | setIsLoading(false)
29 | })
30 | },[path])
31 |
32 | function checkAllResultNotFailed(monitor) {
33 | return monitor.every((category) =>
34 | category.success_rate_category.every((hour) => hour.failed === 0)
35 | )
36 | }
37 |
38 | return(
39 |
40 |
41 | Status Page
42 |
43 | {isLoading ?
44 |
45 | :
46 | (monitor.length != 0) ?
47 |
48 | Bar chart is calculated based on the last 24 hours where each bar block represents 1 hour
49 |
50 |
51 |
52 | : 100% Success rate
53 |
54 |
55 |
56 | : 1-99% Success rate
57 |
58 |
59 |
60 | : 0% Success rate
61 |
62 |
63 |
64 | : No data
65 |
66 |
67 |
68 | { checkAllResultNotFailed(monitor) ?
69 |
70 | All API endpoint(s) are healthy!
71 |
72 | :
73 |
74 | Some API endpoint(s) are not healthy
75 | Your experience may be affected
76 |
77 | }
78 |
79 | {monitor.map((val, idx) => (
80 |
81 |
{val.name}
82 |
83 | {val.success_rate_category.map((hour, idx)=>(
84 |
85 | ))}
86 | { val.success_rate_category.length !== 0 ?
: No data }
87 |
88 |
89 |
24 hours ago
90 |
91 | ))
92 | }
93 |
94 |
95 | :
96 | No data
97 | }
98 |
99 | { errorMessage === "" ?
: {errorMessage} }
100 |
101 |
102 | Powered by
103 |
104 |
105 |
106 |
107 |
108 | )
109 | }
110 |
111 | export default StatusPageDashboard;
--------------------------------------------------------------------------------
/src/assets/fonts/Open_Sans/OFL.txt:
--------------------------------------------------------------------------------
1 | Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans)
2 |
3 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
4 | This license is copied below, and is also available with a FAQ at:
5 | http://scripts.sil.org/OFL
6 |
7 |
8 | -----------------------------------------------------------
9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10 | -----------------------------------------------------------
11 |
12 | PREAMBLE
13 | The goals of the Open Font License (OFL) are to stimulate worldwide
14 | development of collaborative font projects, to support the font creation
15 | efforts of academic and linguistic communities, and to provide a free and
16 | open framework in which fonts may be shared and improved in partnership
17 | with others.
18 |
19 | The OFL allows the licensed fonts to be used, studied, modified and
20 | redistributed freely as long as they are not sold by themselves. The
21 | fonts, including any derivative works, can be bundled, embedded,
22 | redistributed and/or sold with any software provided that any reserved
23 | names are not used by derivative works. The fonts and derivatives,
24 | however, cannot be released under any other type of license. The
25 | requirement for fonts to remain under this license does not apply
26 | to any document created using the fonts or their derivatives.
27 |
28 | DEFINITIONS
29 | "Font Software" refers to the set of files released by the Copyright
30 | Holder(s) under this license and clearly marked as such. This may
31 | include source files, build scripts and documentation.
32 |
33 | "Reserved Font Name" refers to any names specified as such after the
34 | copyright statement(s).
35 |
36 | "Original Version" refers to the collection of Font Software components as
37 | distributed by the Copyright Holder(s).
38 |
39 | "Modified Version" refers to any derivative made by adding to, deleting,
40 | or substituting -- in part or in whole -- any of the components of the
41 | Original Version, by changing formats or by porting the Font Software to a
42 | new environment.
43 |
44 | "Author" refers to any designer, engineer, programmer, technical
45 | writer or other person who contributed to the Font Software.
46 |
47 | PERMISSION & CONDITIONS
48 | Permission is hereby granted, free of charge, to any person obtaining
49 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
50 | redistribute, and sell modified and unmodified copies of the Font
51 | Software, subject to the following conditions:
52 |
53 | 1) Neither the Font Software nor any of its individual components,
54 | in Original or Modified Versions, may be sold by itself.
55 |
56 | 2) Original or Modified Versions of the Font Software may be bundled,
57 | redistributed and/or sold with any software, provided that each copy
58 | contains the above copyright notice and this license. These can be
59 | included either as stand-alone text files, human-readable headers or
60 | in the appropriate machine-readable metadata fields within text or
61 | binary files as long as those fields can be easily viewed by the user.
62 |
63 | 3) No Modified Version of the Font Software may use the Reserved Font
64 | Name(s) unless explicit written permission is granted by the corresponding
65 | Copyright Holder. This restriction only applies to the primary font name as
66 | presented to the users.
67 |
68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69 | Software shall not be used to promote, endorse or advertise any
70 | Modified Version, except to acknowledge the contribution(s) of the
71 | Copyright Holder(s) and the Author(s) or with their explicit written
72 | permission.
73 |
74 | 5) The Font Software, modified or unmodified, in part or in whole,
75 | must be distributed entirely under this license, and must not be
76 | distributed under any other license. The requirement for fonts to
77 | remain under this license does not apply to any document created
78 | using the Font Software.
79 |
80 | TERMINATION
81 | This license becomes null and void if any of the above conditions are
82 | not met.
83 |
84 | DISCLAIMER
85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93 | OTHER DEALINGS IN THE FONT SOFTWARE.
94 |
--------------------------------------------------------------------------------
/src/routes/forgetPassword/index.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import TextInput from '../../components/forms/textinput/index.js';
3 | import { Button, Spinner, Text,Box, Flex, Grid, GridItem } from '@chakra-ui/react';
4 | import { useForm } from 'react-hook-form';
5 | import axios from 'axios';
6 | import BASE_URL from '../../config/api/constant.js';
7 | import { useState } from 'preact/hooks';
8 | import { route } from 'preact-router';
9 | import ROUTE from '../../config/api/route.js';
10 |
11 | const ForgetPassword = ()=>{
12 | const [isLoading, setIsLoading] = useState(false)
13 |
14 | const {
15 | handleSubmit,
16 | register,
17 | formState: { errors },
18 | } = useForm();
19 |
20 | const [responseMessage, setResponseMessage] = useState('')
21 | const [errorMessage, setErrorMessage] = useState('')
22 |
23 | const onSubmit = async (data) => {
24 | setIsLoading(true);
25 | setErrorMessage('');
26 | setResponseMessage('');
27 | try {
28 | await axios.post(`${BASE_URL}/forget-password/token/`, data)
29 | .then(() => {
30 | setResponseMessage('Please check your email and follow instruction on the email')
31 | setIsLoading(false)
32 | })
33 | } catch(error) {
34 | if (error.response.data.error) {
35 | setErrorMessage(error['response']['data']['error'])
36 | } else if (error.response.data.email) {
37 | setErrorMessage(error['response']['data']['email'][0])
38 | } else {
39 | setErrorMessage('We have encountered an error. Please contact our team and try again')
40 | }
41 | setIsLoading(false)
42 | }
43 | };
44 | return (
45 |
51 |
53 |
56 |
57 |
58 | Forget Password
59 |
60 |
61 |
71 |
72 |
73 | {responseMessage != '' && {responseMessage} }
74 |
75 | {errorMessage.length != 0 && (
76 |
77 | Error: {errorMessage}
78 |
79 | )}
80 |
81 |
82 |
83 | {isLoading ? : "Submit"}
84 |
85 |
86 |
87 | route(ROUTE.LOGIN)} color='#4B8F8C' style={{cursor:'pointer', textDecoration: 'underline'}}>Login with your account
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | Get Better Insight About Your API Performance in Seconds
97 |
98 |
99 |
100 |
101 | );
102 |
103 |
104 |
105 | }
106 |
107 |
108 | export default ForgetPassword;
--------------------------------------------------------------------------------
/tests/routes/forgetPasswordToken/forgetPasswordToken.test.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { screen, waitFor, render } from '@testing-library/preact';
3 | import * as axios from 'axios';
4 | import { getCurrentUrl, route } from 'preact-router';
5 | import App from '../../../src/components/app.js';
6 | import userEvent from '@testing-library/user-event'
7 | import { deleteUserToken } from '../../../src/config/api/auth.js';
8 |
9 | jest.mock("axios");
10 |
11 | describe('Test Change Password', () => {
12 | test('try to change password', async () => {
13 | deleteUserToken()
14 | let response = {
15 | success: true
16 | }
17 | axios.post.mockImplementation(() => Promise.resolve({ data: response}));
18 |
19 | render( );
20 | route('/forget_password?key=abcdef')
21 |
22 | await waitFor(() => {
23 | expect(screen.getByText('Forget Password')).toBeDefined()
24 | })
25 |
26 | const emailField = await screen.findByPlaceholderText('New Password');
27 | const submit = screen.getByText('Change Password');
28 |
29 |
30 | userEvent.type(emailField, 'tes4@gmail.com')
31 | userEvent.click(submit)
32 |
33 | await waitFor(() => {
34 | expect(getCurrentUrl()).toBe("/login?isChangePassword=true")
35 | })
36 | })
37 |
38 | test('try to change password failed', async () => {
39 | deleteUserToken()
40 | axios.post.mockImplementation(() => Promise.reject({
41 | response: {
42 | data: {
43 | error : "Please choose password that different from your current password"
44 | }
45 | }
46 | }))
47 | render( );
48 | route('/forget_password?key=abcdef')
49 |
50 | await waitFor(() => {
51 | expect(screen.getByText('Forget Password')).toBeDefined()
52 | })
53 |
54 | const emailField = await screen.findByPlaceholderText('New Password');
55 | const submit = screen.getByText('Change Password');
56 |
57 |
58 | userEvent.type(emailField, 'tes4@gmail.com')
59 | userEvent.click(submit)
60 |
61 | await waitFor(() => {
62 | expect(screen.getByText('Please choose password that different from your current password')).toBeDefined()
63 | })
64 | })
65 |
66 | test('try to change password invalid password failed', async () => {
67 | deleteUserToken()
68 | axios.post.mockImplementation(() => Promise.reject({
69 | response: {
70 | data: {
71 | password : ["The password must contain at least 1 uppercase letter, A-Z."]
72 | }
73 | }
74 | }))
75 | render( );
76 | route('/forget_password?key=abcdef')
77 |
78 | await waitFor(() => {
79 | expect(screen.getByText('Forget Password')).toBeDefined()
80 | })
81 |
82 | const emailField = await screen.findByPlaceholderText('New Password');
83 | const submit = screen.getByText('Change Password');
84 |
85 |
86 | userEvent.type(emailField, 'tes4@gmail.com')
87 | userEvent.click(submit)
88 |
89 | await waitFor(() => {
90 | expect(screen.getByText('The password must contain at least 1 uppercase letter, A-Z.')).toBeDefined()
91 | })
92 | })
93 |
94 | test('try to change password loading when processing', async () => {
95 | deleteUserToken()
96 |
97 | axios.post.mockImplementation(() => {
98 | return new Promise((_, reject)=> {
99 | setTimeout(() => reject({
100 | response: {
101 | data: {
102 | error : "Please choose password that different from your current password"
103 | }
104 | }
105 | }), 3000)
106 | })
107 | });
108 |
109 | render( );
110 | route('/forget_password?key=abcdef')
111 |
112 | await waitFor(() => {
113 | expect(screen.getByText('Forget Password')).toBeDefined()
114 | })
115 |
116 | const newPasswordField = await screen.findByPlaceholderText('New Password');
117 | const submit = screen.getByText('Change Password');
118 |
119 |
120 | userEvent.type(newPasswordField, 'newpass')
121 | userEvent.click(submit)
122 |
123 | await waitFor(() => {
124 | expect(screen.getByText('Loading...')).toBeDefined()
125 | })
126 | })
127 |
128 | test('key changes on token user', async () => {
129 | deleteUserToken()
130 | let response = {
131 | success: true
132 | }
133 | axios.post.mockImplementation(() => Promise.resolve({ data: response}));
134 |
135 | render( );
136 | route('/forget_password?key=abcdef')
137 |
138 | await waitFor(() => {
139 | expect(screen.getByText('Forget Password')).toBeDefined()
140 | })
141 |
142 | const emailField = await screen.findByPlaceholderText('New Password');
143 | const submit = screen.getByText('Change Password');
144 |
145 |
146 | userEvent.type(emailField, 'tes4@gmail.com')
147 | userEvent.click(submit)
148 |
149 | await waitFor(() => {
150 | expect(getCurrentUrl()).toBe("/login?isChangePassword=true")
151 | })
152 | route('/forget_password?key=abcd')
153 | })
154 | });
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MonAPI
2 | 
3 |
4 | MonAPI: Democratizing API Monitoring Tools through Open Sourcing
5 |
6 | This repository is providing frontend service for MonAPI, written using Preact.
7 |
8 | | Pipeline | Status |
9 | | ----------- | ----------- |
10 | | Sonarcloud Scanner | [](https://github.com/MonAPI-xyz/MonAPI-Frontend) |
11 | | Quality Gate Production | [](https://sonarcloud.io/summary/new_code?id=MonAPI-xyz_MonAPI-Frontend) |
12 | | Security Rating Production | [](https://sonarcloud.io/summary/new_code?id=MonAPI-xyz_MonAPI-Frontend) |
13 | | Coverage Production | [](https://sonarcloud.io/summary/new_code?id=MonAPI-xyz_MonAPI-Frontend) |
14 | | Quality Gate Staging | [](https://sonarcloud.io/summary/new_code?id=MonAPI-xyz_MonAPI-Frontend) |
15 | | Security Rating Staging | [](https://sonarcloud.io/summary/new_code?id=MonAPI-xyz_MonAPI-Frontend) |
16 | | Coverage Staging | [](https://sonarcloud.io/summary/new_code?id=MonAPI-xyz_MonAPI-Frontend) |
17 | | Production Deploy | [](https://github.com/MonAPI-xyz/MonAPI-Frontend) |
18 | | Staging Deploy | [](https://github.com/MonAPI-xyz/MonAPI-Frontend) |
19 |
20 |
21 | ## Table of contents
22 | - [Features](#features)
23 | - [CLI Commands](#cli-commands)
24 | - [Related Documentation](#related-documentation)
25 | - [Latest Release Notes](#latest-release-notes)
26 | - [Our Websites](#our-websites)
27 | - [Our Teams](#our-teams)
28 | - [License](#license)
29 | - [Acknowledgements](#acknowledgements)
30 |
31 | ## Features
32 |
33 | - [x] Authentication
34 | - [x] API Monitor Dashboard
35 | - [x] API Monitor Alert
36 | - [x] API Monitor Error Log
37 | - [x] API Test
38 | - [x] Multi-Step API Monitor
39 | - [x] Team Management
40 | - [x] Integrated Status Page
41 |
42 |
43 | ## CLI Commands
44 |
45 | ``` bash
46 | # install dependencies
47 | npm install
48 |
49 | # serve with hot reload at localhost:8080
50 | npm run dev
51 |
52 | # build for production with minification
53 | npm run build
54 |
55 | # test the production build locally
56 | npm run serve
57 |
58 | # run tests with jest and enzyme
59 | npm run test
60 | ```
61 |
62 | For detailed explanation on how things work, checkout the [CLI Readme](https://github.com/developit/preact-cli/blob/master/README.md).
63 |
64 | ## Related Documentation
65 |
66 | - [Run Development Server](https://github.com/MonAPI-xyz/MonAPI-Frontend/blob/staging/docs/development.md)
67 | - [Docker Container](https://github.com/MonAPI-xyz/MonAPI-Frontend/blob/staging/docs/docker.md)
68 |
69 | ## Latest Release Notes
70 | Version: v1.0.1
71 | Date: 28th December 2022
72 | 1. Fix bug password validation on forget password
73 | 2. Update pagerduty email configuration title
74 |
75 | Full release notes can be found in [Release Notes](https://github.com/MonAPI-xyz/MonAPI-Frontend/blob/staging/docs/release_notes.md)
76 |
77 | ## Our websites
78 | 🌐 [Main Site - https://monapi.xyz](https://monapi.xyz)
79 |
80 | 📝 [Blog Site - https://blog.monapi.xyz](https://blog.monapi.xyz)
81 |
82 | 📝 [User Manual - https://docs.monapi.xyz](https://docs.monapi.xyz)
83 |
84 | 📝 [Technical Documentation - https://docs.monapi.xyz/monapi-tech-documentation/](https://docs.monapi.xyz/monapi-tech-documentation/)
85 |
86 | 📺 [Youtube - https://www.youtube.com/@monapi](https://www.youtube.com/@monapi)
87 |
88 | ## Our Teams
89 | - Lucky Susanto
90 | - Ferdi Fadillah
91 | - Hugo Irwanto
92 | - Muhammad Luthfi Fahlevi
93 | - Andrew
94 |
95 | ## License
96 | The scripts and documentation in this project are released under the [GNU General Public License v3.0](https://github.com/MonAPI-xyz/MonAPI-Frontend/blob/main/LICENSE).
97 |
98 |
99 | ## Acknowledgements
100 | * Computer Science Universitas Indonesia - Software Engineering Project 2022
101 |
102 |
103 | [](https://sonarcloud.io/summary/new_code?id=MonAPI-xyz_MonAPI-Frontend)
104 |
--------------------------------------------------------------------------------
/src/routes/view_api_monitor_detail/index.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import {
3 | Box,
4 | Button,
5 | ButtonGroup,
6 | Flex,
7 | Heading,
8 | Select,
9 | Spacer,
10 | Spinner } from '@chakra-ui/react'
11 | import { useEffect, useState } from 'preact/hooks'
12 | import { route } from 'preact-router';
13 | import style from './style.css';
14 | import AlertComponent from '../../components/alertComponent';
15 | import axios from 'axios';
16 | import BASE_URL from '../../config/api/constant.js';
17 | import ROUTE from '../../config/api/route.js';
18 | import SuccessRatePercentageChart from '../../components/chart/success_rate_percentage_chart';
19 | import ResponseTimeChart from '../../components/chart/response_time_chart';
20 | import { getUserToken } from '../../config/api/auth';
21 |
22 | const ViewAPIMonitorDetail = ({id}) => {
23 | const options = [
24 | { value: "30MIN", label: "30 Minutes Ago" },
25 | { value: "60MIN", label: "1 Hour Ago" },
26 | { value: "180MIN", label: "3 Hours Ago" },
27 | { value: "360MIN", label: "6 Hours Ago" },
28 | { value: "720MIN", label: "12 Hours Ago" },
29 | { value: "1440MIN", label: "24 Hours Ago" },
30 | ];
31 |
32 | const stepSizeInSeconds = {
33 | "30MIN": 120,
34 | "60MIN": 240,
35 | "180MIN": 480,
36 | "360MIN": 1200,
37 | "720MIN": 2400,
38 | "1440MIN": 3600,
39 | }
40 |
41 | const [detail, setDetail]=useState([])
42 | const [deletePopup, setDeletePopup] = useState(false)
43 | const [isLoadingDelete, setIsLoadingDelete] = useState(false)
44 | const [selectValue, setSelectValue] = useState("30MIN")
45 | const [isLoading, setIsLoading] = useState(false)
46 |
47 | const onDelete = () => {
48 | setIsLoadingDelete(true)
49 | axios.delete(`${BASE_URL}/monitor/${id}/`, {
50 | headers: {
51 | Authorization: `Token ${getUserToken()}`
52 | }
53 | }).then(() => {
54 | route(ROUTE.DASHBOARD)
55 | setIsLoadingDelete(false)
56 | })
57 | }
58 |
59 | useEffect(() => {
60 | setIsLoading(true);
61 | axios.get(`${BASE_URL}/monitor/${id}/`, {
62 | params:{
63 | range: `${selectValue}`
64 | },
65 | headers: {
66 | Authorization: `Token ${getUserToken()}`
67 | }
68 | }).then((response) => {
69 | setDetail(response.data)
70 | setIsLoading(false);
71 | }).catch((err)=>{
72 | if (err.response.status === 404) {
73 | route('/')
74 | }
75 | })
76 | }, [id, selectValue]);
77 |
78 | const onChange = (e) => {
79 | setSelectValue(e.target.value)
80 | }
81 |
82 | return (
83 |
84 |
85 |
86 | {detail.name}
87 |
88 |
89 |
90 | route(`/${id}/edit/`)}>Edit
91 | {setDeletePopup(true)}}>
92 |
: 'Yes'}
99 | popupOpen={deletePopup}
100 | setPopupOpen={setDeletePopup}
101 | onSubmit={onDelete}
102 | buttonRightColor='red' />
103 |
104 |
105 |
106 |
107 |
108 |
109 |
Request URL
110 |
{detail.url}
111 |
112 |
113 |
Schedule
114 |
{detail.schedule}
115 |
116 |
117 |
118 |
119 |
120 | {options.map(({ label, value }) => (
121 |
122 | {label}
123 |
124 | ))}
125 |
126 |
127 | {isLoading ?
128 |
129 | :
130 |
131 |
132 |
135 |
136 |
137 |
140 |
141 |
142 | }
143 |
144 |
145 | )
146 | }
147 |
148 | export default ViewAPIMonitorDetail;
--------------------------------------------------------------------------------
/tests/routes/forget_password/forget_password.test.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { screen, waitFor, render } from '@testing-library/preact';
3 | import * as axios from 'axios';
4 | import { getCurrentUrl, route } from 'preact-router';
5 | import App from '../../../src/components/app.js';
6 | import userEvent from '@testing-library/user-event'
7 | import { deleteUserToken } from '../../../src/config/api/auth.js';
8 |
9 | jest.mock("axios");
10 |
11 | describe('Test Success Sent Email', () => {
12 | test('send email success', async () => {
13 | deleteUserToken()
14 | let response = {
15 | success: true
16 | }
17 | axios.post.mockImplementation(() => Promise.resolve({ data: response}));
18 |
19 | render( );
20 | route('/forget')
21 |
22 | await waitFor(() => {
23 | expect(screen.getByText('Forget Password')).toBeDefined()
24 | })
25 |
26 | const emailField = await screen.findByPlaceholderText('john@example.com');
27 | const submit = screen.getByText('Submit');
28 |
29 |
30 | userEvent.type(emailField, 'tes4@gmail.com')
31 | userEvent.click(submit)
32 |
33 | await waitFor(() => {
34 | expect(screen.getByText('Please check your email and follow instruction on the email')).toBeDefined()
35 | })
36 | })
37 |
38 | test('send forget password email when loading show spinner', async () => {
39 | deleteUserToken()
40 | let response = {
41 | success: true
42 | }
43 | axios.post.mockImplementation(() => {
44 | return new Promise((resolve)=> {
45 | setTimeout(() => resolve({
46 | data: response
47 | }), 3000)
48 | })
49 | });
50 |
51 | render( );
52 | route('/forget')
53 |
54 | await waitFor(() => {
55 | expect(screen.getByText('Forget Password')).toBeDefined()
56 | })
57 |
58 | const emailField = await screen.findByPlaceholderText('john@example.com');
59 | const submit = screen.getByText('Submit');
60 |
61 |
62 | userEvent.type(emailField, 'tes4@gmail.com')
63 | userEvent.click(submit)
64 |
65 | await waitFor(() => {
66 | expect(screen.getByText('Loading...')).toBeDefined()
67 | })
68 | })
69 |
70 | test('send email failed error system', async () => {
71 | deleteUserToken()
72 | axios.post.mockImplementation(() => Promise.reject({
73 | response: {
74 | data: {
75 | error : "User not exists with given email"
76 | }
77 | }
78 | }))
79 |
80 | render( );
81 | route('/forget')
82 |
83 | await waitFor(() => {
84 | expect(screen.getByText('Forget Password')).toBeDefined()
85 | })
86 |
87 | const emailField = await screen.findByPlaceholderText('john@example.com');
88 | const submit = screen.getByText('Submit');
89 |
90 |
91 | userEvent.type(emailField, 'tes8@gmail.com')
92 | userEvent.click(submit)
93 |
94 | await waitFor(() => {
95 | expect(screen.getByText('Error: User not exists with given email')).toBeDefined()
96 | })
97 | })
98 |
99 | test('send email with invalid email address', async () => {
100 | deleteUserToken()
101 | axios.post.mockImplementation(() => Promise.reject({
102 | response: {
103 | data: {
104 | email : ["Please enter valid email address"]
105 | }
106 | }
107 | }))
108 |
109 | render( );
110 | route('/forget')
111 |
112 | await waitFor(() => {
113 | expect(screen.getByText('Forget Password')).toBeDefined()
114 | })
115 |
116 | const emailField = await screen.findByPlaceholderText('john@example.com');
117 | const submit = screen.getByText('Submit');
118 |
119 |
120 | userEvent.type(emailField, 'tes8@gmail.com')
121 | userEvent.click(submit)
122 |
123 | await waitFor(() => {
124 | expect(screen.getByText('Error: Please enter valid email address')).toBeDefined()
125 | })
126 | })
127 |
128 | test('send email with invalid failed unknown error' , async () => {
129 | deleteUserToken()
130 | axios.post.mockImplementation(() => Promise.reject({
131 | response: {
132 | data: {
133 | unknownError: '',
134 | }
135 | }
136 | }))
137 |
138 | render( );
139 | route('/forget')
140 |
141 | await waitFor(() => {
142 | expect(screen.getByText('Forget Password')).toBeDefined()
143 | })
144 |
145 | const emailField = await screen.findByPlaceholderText('john@example.com');
146 | const submit = screen.getByText('Submit');
147 |
148 |
149 | userEvent.type(emailField, 'tes8@gmail.com')
150 | userEvent.click(submit)
151 |
152 | await waitFor(() => {
153 | expect(screen.getByText('Error: We have encountered an error. Please contact our team and try again')).toBeDefined()
154 | })
155 | })
156 |
157 | test('click already have account redirect to login', async () => {
158 | deleteUserToken()
159 | render( );
160 | route('/forget')
161 |
162 | await waitFor(() => {
163 | expect(screen.getByText('Forget Password')).toBeDefined()
164 | })
165 |
166 | const loginButton = screen.getByText('Login with your account');
167 |
168 | userEvent.click(loginButton)
169 |
170 | await waitFor(() => {
171 | expect(getCurrentUrl()).toBe('/login')
172 | })
173 | })
174 | });
--------------------------------------------------------------------------------
/src/routes/view_list_monitor/index.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { useEffect, useState, useContext } from 'preact/hooks'
3 | import { Link } from 'preact-router/match'
4 | import BASE_URL from '../../config/api/constant';
5 | import style from './style.css';
6 | import axios from 'axios';
7 | import SuccessRate from '../../components/success_rate';
8 | import { getUserToken } from '../../config/api/auth';
9 | import { FaAngleRight } from 'react-icons/fa';
10 | import SuccessRatePercentageChart from '../../components/chart/success_rate_percentage_chart';
11 | import ResponseTimeChart from '../../components/chart/response_time_chart';
12 | import style_detail from '../view_api_monitor_detail/style.css' ;
13 | import style_bar from '../../components/success_rate/style.css' ;
14 | import moment from "moment";
15 | import { UserContext } from '../../config/context';
16 | import { Button, Spinner } from '@chakra-ui/react';
17 |
18 | const ViewListMonitor = () => {
19 | const [monitor,setMonitor]=useState([])
20 | const [detail, setDetail]=useState({})
21 | const {currentTeam} = useContext(UserContext);
22 | const [currentTeamId] = currentTeam;
23 | const [isLoadingStats, setIsLoadingStats] = useState(false);
24 | const [isLoadingMonitor, setIsLoadingMonitor] = useState(false);
25 |
26 | useEffect(()=>{
27 | setIsLoadingMonitor(true);
28 | axios.get(`${BASE_URL}/monitor/`, {
29 | headers: {
30 | Authorization:`Token ${getUserToken()}`
31 | }
32 | }).then((response)=>{
33 | setMonitor(response.data)
34 | setIsLoadingMonitor(false);
35 | })
36 | },[currentTeamId])
37 |
38 | useEffect(() => {
39 | setIsLoadingStats(true);
40 | axios.get(`${BASE_URL}/monitor/stats/`, {
41 | headers: {
42 | Authorization: `Token ${getUserToken()}`
43 | }
44 | }).then((response) => {
45 | setDetail(response.data);
46 | setIsLoadingStats(false);
47 | })
48 | }, [currentTeamId]);
49 |
50 | return(
51 |
52 |
53 | {isLoadingStats ?
54 |
55 | :
56 |
57 |
58 |
61 |
62 |
63 |
66 |
67 |
68 | }
69 |
70 |
71 |
72 |
API Monitors
73 |
74 |
75 | Create New
76 |
77 |
78 |
79 |
80 | {(monitor.length != 0)?
81 |
82 |
83 |
84 |
100% Success rate
85 |
86 |
87 |
88 |
1-99% Success rate
89 |
90 |
91 |
92 |
0% Success rate
93 |
94 |
98 |
99 | :
100 |
101 | }
102 |
103 | {isLoadingMonitor ?
104 |
105 | :
106 |
107 | {(monitor.length != 0 && !isLoadingMonitor) ?
108 |
109 | API Name
110 | Path URL
111 | Success Rate
112 | Response Time (Avg)
113 | Success Rate History (24h)
114 |
115 |
116 | :
117 | There is no monitor. You can click green button "Create New" in the middle right side
118 | }
119 |
120 | {monitor.map((val)=>(
121 |
122 | {val.name}
123 | {val.url}
124 | {val.success_rate}%
125 | {val.avg_response_time} ms
126 |
127 |
128 | {val.success_rate_history.map((history, idx)=>(
129 |
130 | ))}
131 |
132 | Last checked: {val.last_result ? moment(val.last_result.execution_time).fromNow() : "-"}
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 | ))}
141 |
142 | }
143 |
144 | )
145 |
146 | };
147 |
148 | export default ViewListMonitor;
--------------------------------------------------------------------------------
/tests/components/alertComponent/alertComponent.test.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { render, fireEvent, waitFor, screen } from '@testing-library/preact';
3 | import AlertComponent from '../../../src/components/alertComponent';
4 |
5 | describe('Test alert component', () => {
6 | test('When render with isButton false, then success', async () => {
7 | const mockCallback = jest.fn();
8 | const onSubmit = () => {}
9 |
10 | render( );
22 |
23 | await waitFor(async () => {
24 | expect(screen.queryByRole('button')).toBeNull();
25 | expect(screen.queryByRole('clickable-text').textContent).toBe('Test Alert');
26 | })
27 | });
28 |
29 | test('When render with isButton true, then success', async () => {
30 | const mockCallback = jest.fn();
31 | const onSubmit = () => {}
32 |
33 | render( );
45 |
46 | await waitFor(() => {
47 | expect(screen.getByRole('button').textContent).toBe('Test Alert')
48 | })
49 | });
50 |
51 | test('When click alert clickable-text, then popup appears', async () => {
52 | const mockCallback = jest.fn();
53 | const onSubmit = () => {}
54 |
55 | render( );
67 |
68 | fireEvent.click(screen.getByRole('clickable-text'));
69 | await waitFor(() => {
70 | expect(mockCallback.mock.calls.length).toBe(1);
71 | expect(mockCallback.mock.calls[0][0]).toBe(true);
72 | expect(screen.queryByText('Alert Header')).toBeNull();
73 | })
74 | });
75 |
76 | test('When click alert button, then popup appears', async () => {
77 | const mockCallback = jest.fn();
78 | const onSubmit = () => {}
79 |
80 | render( );
92 |
93 | fireEvent.click(screen.getByRole('button'));
94 | await waitFor(() => {
95 | expect(mockCallback.mock.calls.length).toBe(1);
96 | expect(mockCallback.mock.calls[0][0]).toBe(true);
97 | expect(screen.queryByText('Alert Header')).toBeNull();
98 | })
99 | });
100 |
101 | test('When click Cancel button in popup, then popupOpen status to be false', async () => {
102 | const mockCallback = jest.fn();
103 | const onSubmit = () => {}
104 |
105 | render( );
117 |
118 | fireEvent.click(screen.getByRole('button', {name:'Cancel'}));
119 | await waitFor(() => {
120 | expect(mockCallback.mock.calls.length).toBe(1);
121 | expect(mockCallback.mock.calls[0][0]).toBe(false);
122 | })
123 | })
124 |
125 | test('When click Yes button in popup, then popupOpen status still true and run submit', async () => {
126 | const mockCallback = jest.fn();
127 | const onSubmit = () => {}
128 |
129 | render( );
141 |
142 | fireEvent.click(screen.getByRole('button', {name:'Yes'}));
143 | await waitFor(() => {
144 | expect(mockCallback.mock.calls.length).toBe(0);
145 |
146 | //onSubmit do nothing, so it is still on the current condition
147 | expect(screen.queryByText('Alert Header')).toBeDefined();
148 |
149 | })
150 | })
151 |
152 | test('When click Close button (x) in popup, then popupOpen status to be false', async () => {
153 | const mockCallback = jest.fn();
154 | const onSubmit = () => {}
155 |
156 | render( );
168 |
169 | fireEvent.click(screen.getByRole('button', {name:'Close'}));
170 | await waitFor(() => {
171 | expect(mockCallback.mock.calls.length).toBe(1);
172 | expect(mockCallback.mock.calls[0][0]).toBe(false);
173 | expect(screen.queryByText('Alert Header')).toBeDefined();
174 | })
175 | })
176 | })
--------------------------------------------------------------------------------
/tests/routes/editTeam/editTeam.test.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { render, waitFor, screen } from '@testing-library/preact';
3 | import EditTeam from '../../../src/routes/editTeam';
4 | import userEvent from '@testing-library/user-event'
5 | import * as axios from 'axios';
6 | import { getCurrentUrl } from 'preact-router';
7 |
8 | jest.mock('axios')
9 |
10 | describe('Test input', () => {
11 |
12 | test('When user fill the description, then success', async () => {
13 | const responseCurrentTeam = {
14 | id: 47,
15 | name: "team3",
16 | logo: "/uploads/teamlogo/d35b869d-e411-4648-9fe8-e1a0700c4202.jpeg",
17 | description: "des",
18 | teammember: [
19 | {
20 | team: 47,
21 | user: {
22 | id: 16,
23 | username: "hugoirdev@gmail.com",
24 | email: "hugoirdev@gmail.com",
25 | first_name: "",
26 | last_name: ""
27 | },
28 | verified: true
29 | }
30 | ]
31 | }
32 |
33 | axios.get.mockImplementation(() => {
34 | return Promise.resolve({data: responseCurrentTeam})
35 | })
36 |
37 | render( );
38 |
39 | const response = {
40 | id: 43,
41 | name: "Hugoirdev"
42 | }
43 |
44 | const mockPut = jest.fn()
45 | axios.put.mockImplementation(() => {
46 | mockPut()
47 | return Promise.resolve({data: response})
48 | })
49 | const descField = await screen.findByPlaceholderText('Insert Team Description');
50 | userEvent.type(descField, 'teamdesc')
51 |
52 | const edit = screen.getByText('Save');
53 | userEvent.click(edit);
54 | await waitFor(() => {
55 | expect(getCurrentUrl()).toBe('/')
56 | expect(mockPut).toHaveBeenCalledTimes(0)
57 | });
58 | });
59 |
60 | test('When empty image and update then success', async () => {
61 | const responseCurrentTeam = {
62 | id: 47,
63 | name: "team3",
64 | logo: null,
65 | description: "des",
66 | teammember: [
67 | {
68 | team: 47,
69 | user: {
70 | id: 16,
71 | username: "hugoirdev@gmail.com",
72 | email: "hugoirdev@gmail.com",
73 | first_name: "",
74 | last_name: ""
75 | },
76 | verified: true
77 | }
78 | ]
79 | }
80 |
81 | axios.get.mockImplementation(() => {
82 | return Promise.resolve({data: responseCurrentTeam})
83 | })
84 |
85 | render( );
86 |
87 | const response = {
88 | id: 43,
89 | name: "Hugoirdev"
90 | }
91 |
92 | const mockPut = jest.fn()
93 | axios.put.mockImplementation(() => {
94 | mockPut()
95 | return Promise.resolve({data: response})
96 | })
97 | const descField = await screen.findByPlaceholderText('Insert Team Description');
98 | userEvent.type(descField, 'teamdesc')
99 |
100 | const edit = screen.getByText('Save');
101 | userEvent.click(edit);
102 | await waitFor(() => {
103 | expect(getCurrentUrl()).toBe('/')
104 | expect(mockPut).toHaveBeenCalledTimes(0)
105 | });
106 | });
107 |
108 | test('When user fill inputs completely, then success', async () => {
109 | render( );
110 |
111 |
112 | const descField = await screen.findByPlaceholderText('Insert Team Description');
113 | userEvent.type(descField, 'myDesc')
114 |
115 | global.URL.createObjectURL = jest.fn();
116 | const imageFile = new File([new ArrayBuffer(1)], 'hello.png', { type: "image/png" });
117 |
118 | const fileField = await screen.findByPlaceholderText('Select Image');
119 |
120 | // test file input is optional
121 | userEvent.upload(fileField, null)
122 |
123 | // test file input can upload image file
124 | userEvent.upload(fileField, imageFile)
125 |
126 | const response = {
127 | id: 43,
128 | name: "Hugoirdev"
129 | }
130 |
131 | const mockPut = jest.fn()
132 | axios.put.mockImplementation(() => {
133 | mockPut()
134 | return Promise.resolve({data: response})
135 | })
136 |
137 | const edit = screen.getByText('Save');
138 | userEvent.click(edit);
139 |
140 | await waitFor(() => {
141 | expect(mockPut).toHaveBeenCalledTimes(1);
142 | });
143 | });
144 |
145 | test('When user upload image file size more than 10MB, then failed', async () => {
146 | render( );
147 |
148 | global.URL.createObjectURL = jest.fn();
149 |
150 | // create new file that have size more than 10MB, specificly 10MB + 1b
151 | const imageFile = new File([new ArrayBuffer(1024 * 1024 * 10 + 1)], 'hello.png', { type: "image/png" });
152 |
153 | const fileField = await screen.findByPlaceholderText('Select Image');
154 |
155 | // test file input can upload image file
156 | userEvent.upload(fileField, imageFile)
157 |
158 | const edit = screen.getByText('Save');
159 | userEvent.click(edit);
160 |
161 | await waitFor(() => {
162 | expect(screen.getByText("Please choose image that the file size less than or equal to 10MB")).toBeDefined()
163 | });
164 | });
165 |
166 | test('When user click edit button, then show spinner to loading', async () => {
167 |
168 | let response = {
169 | id: 43,
170 | name: "Hugoirdev"
171 | }
172 | axios.put.mockImplementation(() => {
173 | return new Promise((resolve)=> {
174 | setTimeout(() => resolve({
175 | data: response
176 | }), 3000)
177 | })
178 | });
179 |
180 | render( );
181 |
182 | const descField = await screen.findByPlaceholderText('Insert Team Description');
183 | userEvent.type(descField, 'teamdesc')
184 |
185 | const edit = screen.getByText('Save');
186 | userEvent.click(edit);
187 |
188 | await waitFor(() => {
189 | expect(screen.getByText('Loading...')).toBeDefined()
190 | })
191 | })
192 |
193 | })
--------------------------------------------------------------------------------
/tests/routes/configuration/configuration.test.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { screen, waitFor, render } from '@testing-library/preact';
3 | import * as axios from 'axios';
4 | import { getCurrentUrl, route } from 'preact-router';
5 | import App from '../../../src/components/app.js';
6 | import Configuration from '../../../src/routes/configuration/index.js'
7 | import userEvent from '@testing-library/user-event'
8 | import { setUserToken } from '../../../src/config/api/auth.js';
9 |
10 | jest.mock("axios");
11 |
12 | describe('Test configuration sites', () => {
13 | test('try to access configuration sites', async () => {
14 | const response = []
15 | axios.get.mockImplementation(() => Promise.resolve({data: response}))
16 | setUserToken("token")
17 | render( );
18 | route('/configuration/')
19 |
20 | await waitFor(() => {
21 | expect(getCurrentUrl()).toBe('/configuration/');
22 | })
23 | })
24 |
25 | test('save config', async () => {
26 | const response = {
27 | utc:0,
28 | is_slack_active: false,
29 | slack_token: "",
30 | slack_channel_id: "",
31 | is_discord_active: false,
32 | discord_webhook_url: "",
33 | is_pagerduty_active: false,
34 | pagerduty_api_key: "",
35 | pagerduty_default_from_email: "",
36 | pagerduty_service_id: "",
37 | is_email_active: false,
38 | email_name: '',
39 | email_address:'',
40 | email_host: "",
41 | email_port: null,
42 | email_username: "",
43 | email_password: "",
44 | email_use_tls: true,
45 | email_use_ssl: true,
46 | threshold_pct: 100,
47 | time_window:'1H',
48 | }
49 | axios.get.mockImplementation(() => Promise.resolve({data: response}))
50 | setUserToken("token")
51 | render( );
52 | route('/configuration/')
53 |
54 | const saveButton = screen.getByText("Save")
55 | userEvent.click(saveButton);
56 |
57 | axios.post.mockImplementation(()=>Promise.resolve({data: response}))
58 |
59 | await waitFor(() => {
60 | expect(screen.getByText("Successful save configuration")).toBeDefined()
61 | expect(getCurrentUrl()).toBe('/configuration/');
62 | })
63 | })
64 |
65 | test('save config loading', async () => {
66 | const response = {
67 | utc:0,
68 | is_slack_active: false,
69 | slack_token: "",
70 | slack_channel_id: "",
71 | is_discord_active: false,
72 | discord_webhook_url: "",
73 | is_pagerduty_active: false,
74 | pagerduty_api_key: "",
75 | pagerduty_default_from_email: "",
76 | pagerduty_service_id: "",
77 | is_email_active: false,
78 | email_name: '',
79 | email_address:'',
80 | email_host: "",
81 | email_port: null,
82 | email_username: "",
83 | email_password: "",
84 | email_use_tls: true,
85 | email_use_ssl: true,
86 | threshold_pct:100,
87 | time_window:'1H',
88 | }
89 | axios.get.mockImplementation(() => Promise.resolve({data: response}))
90 | setUserToken("token")
91 | render( );
92 | route('/configuration/')
93 |
94 | const saveButton = screen.getByText("Save")
95 | userEvent.click(saveButton);
96 |
97 | axios.post.mockImplementation(() => {
98 | return new Promise((resolve)=> {
99 | setTimeout(() => resolve({
100 | data: response
101 | }), 3000)
102 | })
103 | })
104 |
105 | await waitFor(() => {
106 | expect(screen.getByText("Loading...")).toBeDefined()
107 | })
108 |
109 | userEvent.click(saveButton);
110 | })
111 |
112 | test('save config failed', async () => {
113 | const response = {
114 | utc:0,
115 | is_slack_active: false,
116 | slack_token: "",
117 | slack_channel_id: "",
118 | is_discord_active: false,
119 | discord_webhook_url: "",
120 | is_pagerduty_active: false,
121 | pagerduty_api_key: "",
122 | pagerduty_default_from_email: "",
123 | pagerduty_service_id: "",
124 | is_email_active: false,
125 | email_name: '',
126 | email_address:'',
127 | email_host: "",
128 | email_port: null,
129 | email_username: "",
130 | email_password: "",
131 | email_use_tls: true,
132 | email_use_ssl: true,
133 | threshold_pct:100,
134 | time_window:'1H',
135 | }
136 | axios.get.mockImplementation(() => Promise.resolve({data: response}))
137 | setUserToken("token")
138 | render( );
139 | route('/configuration/')
140 |
141 | const saveButton = screen.getByText("Save")
142 | userEvent.click(saveButton);
143 |
144 | axios.post.mockImplementation(()=>Promise.reject({response: {data:{error:"error test case"}}}))
145 |
146 | await waitFor(() => {
147 | expect(screen.getByText("error test case")).toBeDefined()
148 | expect(getCurrentUrl()).toBe('/configuration/');
149 | })
150 | })
151 |
152 | test('when do wrong input on Threshold, then appears error message', async () => {
153 | const response = {
154 | utc:0,
155 | is_slack_active: false,
156 | slack_token: "",
157 | slack_channel_id: "",
158 | is_discord_active: false,
159 | discord_webhook_url: "",
160 | is_pagerduty_active: false,
161 | pagerduty_api_key: "",
162 | pagerduty_default_from_email: "",
163 | pagerduty_service_id: "",
164 | is_email_active: false,
165 | email_name: '',
166 | email_address:'',
167 | email_host: "",
168 | email_port: null,
169 | email_username: "",
170 | email_password: "",
171 | email_use_tls: true,
172 | email_use_ssl: true,
173 | threshold_pct:100,
174 | time_window:'1H',
175 | }
176 | axios.get.mockImplementation(() => Promise.resolve({data: response}))
177 | setUserToken("token")
178 | render( );
179 |
180 | const saveButton = screen.getByText("Save")
181 |
182 | const thresholdAccordion = screen.getByText("Threshold")
183 | userEvent.click(thresholdAccordion);
184 |
185 | const thresholdValueInput = screen.getByPlaceholderText("Insert the integer value from 1 to 100")
186 |
187 | userEvent.clear(thresholdValueInput)
188 | userEvent.click(saveButton);
189 | await waitFor(() => {
190 | expect(screen.getByText("Required")).toBeDefined()
191 | })
192 |
193 | userEvent.clear(thresholdValueInput)
194 | userEvent.type(thresholdValueInput, '90.5')
195 | userEvent.click(saveButton);
196 | await waitFor(() => {
197 | expect(screen.getByText("Value must be integer")).toBeDefined()
198 | })
199 | })
200 | });
--------------------------------------------------------------------------------
/tests/routes/login/login.test.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import Login from '../../../src/routes/login/index.js';
3 | import { screen, waitFor, render } from '@testing-library/preact';
4 | import * as axios from 'axios';
5 | import { getCurrentUrl, route } from 'preact-router';
6 | import App from '../../../src/components/app.js';
7 | import userEvent from '@testing-library/user-event'
8 | import { deleteUserToken, setUserToken } from '../../../src/config/api/auth.js';
9 |
10 | jest.mock("axios");
11 |
12 | describe('Test Success Login', () => {
13 | test('try to login and success login and redirected to dashboard', async () => {
14 | deleteUserToken()
15 | let response = {
16 | response: "Sign-in successful.",
17 | email: "user@gmail.com",
18 | token: "d16c4059484867e8d12ff535072509e3f29719e7"
19 | }
20 | axios.post.mockImplementation(() => Promise.resolve({ data: response}));
21 |
22 | render( );
23 | route('/login')
24 | const emailField = await screen.findByPlaceholderText('john@example.com');
25 | const passwordField = await screen.findByPlaceholderText('************');
26 | const signIn = screen.getByText('Sign In');
27 |
28 | userEvent.type(emailField, 'tes4@gmail.com')
29 | userEvent.type(passwordField, 'Tes12345')
30 |
31 | userEvent.click(signIn);
32 |
33 | await waitFor(() => {
34 | expect(getCurrentUrl()).toBe('/');
35 | })
36 | })
37 |
38 | test('when process login then show loader', async () => {
39 | deleteUserToken()
40 | let response = {
41 | response: "Sign-in successful.",
42 | email: "user@gmail.com",
43 | token: "d16c4059484867e8d12ff535072509e3f29719e7"
44 | }
45 |
46 | axios.post.mockImplementation(() =>
47 | new Promise((resolve)=> {
48 | setTimeout(() => resolve({
49 | data: response
50 | }), 3000)
51 | })
52 | )
53 |
54 | render( );
55 | route('/login')
56 | const emailField = await screen.findByPlaceholderText('john@example.com');
57 | const passwordField = await screen.findByPlaceholderText('************');
58 | const signIn = screen.getByText('Sign In');
59 |
60 | userEvent.type(emailField, 'tes4@gmail.com')
61 | userEvent.type(passwordField, 'Tes12345')
62 |
63 | userEvent.click(signIn);
64 |
65 | await waitFor(() => {
66 | expect(screen.getByText('Loading...')).toBeDefined()
67 | })
68 | })
69 | });
70 |
71 | describe('Test Accessing Routes', () => {
72 | test('authenticated and try accessing login route', async () => {
73 | const response = []
74 | axios.get.mockImplementation(() => Promise.resolve({data: response}))
75 | setUserToken('d16c4059484867e8d12ff535072509e3f29719e7')
76 |
77 | render( );
78 | route('/login')
79 |
80 | await waitFor(() => {
81 | expect(getCurrentUrl()).toBe('/');
82 | })
83 | })
84 | })
85 |
86 | describe('Test Failed Login', () => {
87 | test('try to login and failed login and keep on login page', async () => {
88 | deleteUserToken()
89 | let response = {
90 | data: {response: "Invalid email or password."}
91 | }
92 | axios.post.mockImplementation(() => Promise.reject({
93 | status:401,
94 | response
95 | })
96 | );
97 | render( );
98 | route('/login')
99 | const emailField = await screen.findByPlaceholderText('john@example.com');
100 | const passwordField = await screen.findByPlaceholderText('************');
101 | const signIn = screen.getByText('Sign In');
102 |
103 | userEvent.type(emailField, 'tes4@gmail.com')
104 | userEvent.type(passwordField, 'Tes123425')
105 |
106 | userEvent.click(signIn);
107 |
108 | await waitFor(() => {
109 | expect(screen.getByText('Invalid email or password.')).toBeDefined()
110 | expect(getCurrentUrl()).toBe('/login');
111 | })
112 | })
113 | });
114 |
115 | describe('Test Form Login', () => {
116 | test('require to fill the both email and password', async () => {
117 | render( );
118 |
119 | const signIn = screen.getByText('Sign In');
120 | userEvent.click(signIn);
121 | await waitFor(() => {
122 | expect(screen.getAllByText("Required")).toHaveLength(2);
123 | });
124 |
125 | });
126 |
127 | test('email minimum length should be 3', async () => {
128 | render( );
129 | const emailField = await screen.findByPlaceholderText('john@example.com');
130 | const signIn = screen.getByText('Sign In');
131 |
132 | userEvent.type(emailField, 'us')
133 | userEvent.click(signIn);
134 | await waitFor(() => {
135 | expect(screen.getByText('Minimum length should be 3')).toBeDefined()
136 | });
137 | });
138 |
139 | test('password minimum length should be 8', async () => {
140 | render( );
141 | const passwordField = await screen.findByPlaceholderText('************');
142 | const signIn = screen.getByText('Sign In');
143 |
144 | userEvent.type(passwordField, 'pass')
145 | userEvent.click(signIn);
146 | await waitFor(() => {
147 | expect(screen.getByText('Minimum length should be 8')).toBeDefined()
148 | });
149 | });
150 |
151 | test('click sign up text span', async () => {
152 | deleteUserToken();
153 | render( );
154 | route('/login')
155 |
156 | userEvent.click(screen.getByText('sign up'));
157 |
158 | await waitFor(() => {
159 | expect(getCurrentUrl()).toBe('/register');
160 | })
161 | })
162 |
163 | test('click forget password text span', async () => {
164 | deleteUserToken();
165 | render( );
166 | route('/login')
167 |
168 | await waitFor(() => {
169 | expect(screen.getByText('Forget your password?')).toBeDefined()
170 | });
171 |
172 | userEvent.click(screen.getByText('Forget your password?'));
173 |
174 | await waitFor(() => {
175 | expect(getCurrentUrl()).toBe('/forget');
176 | })
177 | })
178 |
179 | test('successfully registered user is redirected to /login', async() => {
180 | deleteUserToken()
181 | render( );
182 | route('/login?isRegistered=true')
183 |
184 | await waitFor(async () => {
185 | expect(screen.getByText('Check your email to verify your account.')).toBeDefined()
186 | })
187 | })
188 |
189 | test('successfully change password user is redirected to /login', async() => {
190 | deleteUserToken()
191 | render( );
192 | route('/login?isChangePassword=true')
193 |
194 | await waitFor(async () => {
195 | expect(screen.getByText('Password changed successfully. Please login with your new password.')).toBeDefined()
196 | })
197 | })
198 | });
--------------------------------------------------------------------------------
/src/routes/register/index.js:
--------------------------------------------------------------------------------
1 | import TextInput from '../../components/forms/textinput/index.js'
2 | import { h } from "preact";
3 | import { useState } from "preact/hooks";
4 | import PasswordInput from '../../components/forms/passwordinput/index.js'
5 | import { Button, Text, Spinner,Box, Flex, Grid, GridItem } from '@chakra-ui/react';
6 | import { useForm } from 'react-hook-form';
7 | import { route } from 'preact-router';
8 | import ROUTE from '../../config/api/route.js';
9 | import { isAuthenticate } from '../../config/middleware/middleware.js';
10 | import BASE_URL from '../../config/api/constant.js';
11 | import * as axios from "axios";
12 |
13 | function Register() {
14 | const [errl, setErrl] = useState([])
15 | const [isLoading, setIsLoading] = useState(false);
16 |
17 | const {
18 | handleSubmit,
19 | register,
20 | formState: { errors },
21 | } = useForm();
22 |
23 | if (isAuthenticate()) {
24 | route(ROUTE.DASHBOARD)
25 | return;
26 | }
27 |
28 | const onSubmit = (res) => {
29 | setIsLoading(true)
30 | const data = {
31 | email: res.email,
32 | password: res.password,
33 | password2: res.password2
34 | }
35 |
36 | axios.post(`${BASE_URL}/register/api`, data)
37 | .then(() => {
38 | route(ROUTE.LOGIN + "?isRegistered=true")
39 | setIsLoading(false)
40 | })
41 | .catch((error) => {
42 | let error_logs = []
43 | if (error.response.data.response) {
44 | error_logs = error_logs.concat(error.response.data.response)
45 | }
46 | if (error.response.data.email) {
47 | const email_error = error.response.data.email
48 | error_logs = error_logs.concat(email_error)
49 | }
50 | if (error.response.data.password) {
51 | let password_error = error.response.data.password
52 | if (password_error[0].startsWith('[')){
53 | password_error = error.response.data.password[0].split("', '")
54 | password_error[0] = password_error[0].slice(2);
55 | password_error[password_error.length - 1] = password_error[password_error.length - 1].slice(0, -2);
56 | }
57 | error_logs = error_logs.concat(password_error)
58 | }
59 | setErrl(error_logs)
60 | setIsLoading(false)
61 | });
62 |
63 | return data
64 | }
65 |
66 | return (
67 |
73 |
75 |
78 |
79 |
80 | Register
81 |
82 |
83 |
93 |
94 |
95 |
105 |
106 |
107 |
117 |
118 |
119 | {errl.length != 0 && (
120 |
121 |
Error:
122 |
123 | {errl.map((e,i) => (
124 | {e}
125 | ))}
126 |
127 |
128 | )}
129 |
130 |
131 | {isLoading ? : "Sign Up"}
132 |
133 |
134 |
135 |
136 | Already have an account?
137 |
138 | route(ROUTE.LOGIN)} color='#4B8F8C'>Sign in
139 |
140 |
141 |
142 |
143 |
144 |
145 | Get Better Insight About Your API Performance in Seconds
146 |
147 |
148 |
149 |
150 | );
151 | }
152 |
153 | export default Register;
--------------------------------------------------------------------------------