Ui/Download request replay (#1188)

* added icon

* download & upload

* button changes

* clean up

* changes

* pkj json

* img

* removed codeEditor options

* changes

Co-authored-by: Leon <>
This commit is contained in:
leon-up9 2022-07-10 16:48:18 +03:00 committed by GitHub
parent a2463b739a
commit 9aaf3f1423
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 210 additions and 85 deletions

View File

@ -65,6 +65,7 @@
"recharts": "^2.1.10", "recharts": "^2.1.10",
"redoc": "^2.0.0-rc.71", "redoc": "^2.0.0-rc.71",
"styled-components": "^5.3.5", "styled-components": "^5.3.5",
"use-file-picker": "^1.4.2",
"web-vitals": "^2.1.4", "web-vitals": "^2.1.4",
"xml-formatter": "^2.6.1" "xml-formatter": "^2.6.1"
}, },

View File

@ -17,6 +17,6 @@
width: 100%; width: 100%;
width: -moz-available; width: -moz-available;
width: -webkit-fill-available; width: -webkit-fill-available;
width: strech; width: stretch;
} }
} }

View File

@ -1,20 +1,20 @@
import React, {useCallback, useEffect, useMemo, useState} from "react"; import React, { useCallback, useEffect, useMemo, useState } from "react";
import styles from './EntriesList.module.sass'; import styles from './EntriesList.module.sass';
import ScrollableFeedVirtualized from "react-scrollable-feed-virtualized"; import ScrollableFeedVirtualized from "react-scrollable-feed-virtualized";
import Moment from 'moment'; import { EntryItem } from "../EntryListItem/EntryListItem";
import {EntryItem} from "../EntryListItem/EntryListItem";
import down from "assets/downImg.svg"; import down from "assets/downImg.svg";
import spinner from 'assets/spinner.svg'; import spinner from 'assets/spinner.svg';
import {RecoilState, useRecoilState, useRecoilValue, useSetRecoilState} from "recoil"; import { RecoilState, useRecoilState, useRecoilValue, useSetRecoilState } from "recoil";
import entriesAtom from "../../recoil/entries"; import entriesAtom from "../../recoil/entries";
import queryAtom from "../../recoil/query"; import queryAtom from "../../recoil/query";
import TrafficViewerApiAtom from "../../recoil/TrafficViewerApi"; import TrafficViewerApiAtom from "../../recoil/TrafficViewerApi";
import TrafficViewerApi from "../TrafficViewer/TrafficViewerApi"; import TrafficViewerApi from "../TrafficViewer/TrafficViewerApi";
import focusedEntryIdAtom from "../../recoil/focusedEntryId"; import focusedEntryIdAtom from "../../recoil/focusedEntryId";
import {toast} from "react-toastify"; import { toast } from "react-toastify";
import {MAX_ENTRIES, TOAST_CONTAINER_ID} from "../../configs/Consts"; import { MAX_ENTRIES, TOAST_CONTAINER_ID } from "../../configs/Consts";
import tappingStatusAtom from "../../recoil/tappingStatus"; import tappingStatusAtom from "../../recoil/tappingStatus";
import leftOffTopAtom from "../../recoil/leftOffTop"; import leftOffTopAtom from "../../recoil/leftOffTop";
import Moment from "moment";
interface EntriesListProps { interface EntriesListProps {
listEntryREF: any; listEntryREF: any;

View File

@ -37,11 +37,6 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
theme="github" theme="github"
onChange={onChange} onChange={onChange}
editorProps={{ $blockScrolling: true }} editorProps={{ $blockScrolling: true }}
setOptions={{
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
enableSnippets: true
}}
showPrintMargin={false} showPrintMargin={false}
value={code} value={code}
width="100%" width="100%"

View File

@ -0,0 +1,33 @@
import React from 'react';
import { useEffect } from 'react';
import { useFilePicker } from 'use-file-picker';
import { FileContent } from 'use-file-picker/dist/interfaces';
interface IFilePickerProps {
onLoadingComplete: (file: FileContent) => void;
elem: any
}
const FilePicker = ({ elem, onLoadingComplete }: IFilePickerProps) => {
const [openFileSelector, { filesContent }] = useFilePicker({
accept: ['.json'],
limitFilesConfig: { max: 1 },
maxFileSize: 1
});
const onFileSelectorClick = (e) => {
e.preventDefault();
e.stopPropagation();
openFileSelector();
}
useEffect(() => {
filesContent.length && onLoadingComplete(filesContent[0])
}, [filesContent, onLoadingComplete]);
return (<React.Fragment>
{React.cloneElement(elem, { onClick: onFileSelectorClick })}
</React.Fragment>)
}
export default FilePicker;

View File

@ -75,5 +75,6 @@ const KeyValueTable: React.FC<KeyValueTableProps> = ({ data, onDataChange, keyPl
})} })}
</div> </div>
} }
export const convertParamsToArr = (paramsObj) => Object.entries(paramsObj).map(([key, value]) => { return { key, value } })
export const convertArrToKeyValueObject = (arr) => arr.reduce((acc, curr) => { acc[curr.key] = curr.value; return acc }, {})
export default KeyValueTable export default KeyValueTable

View File

@ -81,3 +81,10 @@
b::after b::after
content: '\b' content: '\b'
display: inline display: inline
.icon
width: 24px
height: 26px
stroke-width: 0px
fill: $blue-color
stroke: $blue-color

View File

@ -1,25 +1,30 @@
import { Accordion, AccordionDetails, AccordionSummary, Backdrop, Box, Button, Fade, Modal } from "@mui/material";
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import DownloadIcon from '@mui/icons-material/FileDownloadOutlined';
import UploadIcon from '@mui/icons-material/UploadFile';
import closeIcon from "assets/close.svg";
import refreshImg from "assets/refresh.svg";
import { Accordion, AccordionDetails, AccordionSummary, Backdrop, Box, Button, Fade, Modal } from "@mui/material";
import React, { Fragment, useCallback, useEffect, useState } from "react"; import React, { Fragment, useCallback, useEffect, useState } from "react";
import { useCommonStyles } from "../../../helpers/commonStyle";
import { Tabs } from "../../UI";
import KeyValueTable from "../../UI/KeyValueTable/KeyValueTable";
import CodeEditor from "../../UI/CodeEditor/CodeEditor";
import { useRecoilValue, RecoilState, useRecoilState } from "recoil";
import TrafficViewerApiAtom from "../../../recoil/TrafficViewerApi/atom";
import TrafficViewerApi from "../../TrafficViewer/TrafficViewerApi";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { RecoilState, useRecoilState, useRecoilValue } from "recoil";
import { FileContent } from "use-file-picker/dist/interfaces";
import { TOAST_CONTAINER_ID } from "../../../configs/Consts"; import { TOAST_CONTAINER_ID } from "../../../configs/Consts";
import styles from './ReplayRequestModal.module.sass' import { useCommonStyles } from "../../../helpers/commonStyle";
import closeIcon from "assets/close.svg"
import refreshImg from "assets/refresh.svg"
import { formatRequestWithOutError } from "../../EntryDetailed/EntrySections/EntrySections";
import entryDataAtom from "../../../recoil/entryData";
import { AutoRepresentation, TabsEnum } from "../../EntryDetailed/EntryViewer/AutoRepresentation";
import useDebounce from "../../../hooks/useDebounce"
import replayRequestModalOpenAtom from "../../../recoil/replayRequestModalOpen";
import { Utils } from "../../../helpers/Utils"; import { Utils } from "../../../helpers/Utils";
import useDebounce from "../../../hooks/useDebounce";
import entryDataAtom from "../../../recoil/entryData";
import replayRequestModalOpenAtom from "../../../recoil/replayRequestModalOpen";
import TrafficViewerApiAtom from "../../../recoil/TrafficViewerApi/atom";
import { formatRequestWithOutError } from "../../EntryDetailed/EntrySections/EntrySections";
import { AutoRepresentation, TabsEnum } from "../../EntryDetailed/EntryViewer/AutoRepresentation";
import TrafficViewerApi from "../../TrafficViewer/TrafficViewerApi";
import { Tabs } from "../../UI";
import CodeEditor from "../../UI/CodeEditor/CodeEditor";
import FilePicker from '../../UI/FilePicker/FilePicker';
import KeyValueTable, { convertArrToKeyValueObject, convertParamsToArr } from "../../UI/KeyValueTable/KeyValueTable";
import { LoadingWrapper } from "../../UI/withLoading/withLoading"; import { LoadingWrapper } from "../../UI/withLoading/withLoading";
import { IReplayRequestData, KeyValuePair } from './interfaces';
import styles from './ReplayRequestModal.module.sass';
const modalStyle = { const modalStyle = {
position: 'absolute', position: 'absolute',
@ -37,11 +42,6 @@ const modalStyle = {
paddingBottom: "15px" paddingBottom: "15px"
}; };
interface ReplayRequestModalProps {
isOpen: boolean;
onClose: () => void;
}
enum RequestTabs { enum RequestTabs {
Params = "params", Params = "params",
Headers = "headers", Headers = "headers",
@ -51,8 +51,6 @@ enum RequestTabs {
const HTTP_METHODS = ["get", "post", "put", "head", "options", "delete"] const HTTP_METHODS = ["get", "post", "put", "head", "options", "delete"]
const TABS = [{ tab: RequestTabs.Headers }, { tab: RequestTabs.Params }, { tab: RequestTabs.Body }]; const TABS = [{ tab: RequestTabs.Headers }, { tab: RequestTabs.Params }, { tab: RequestTabs.Body }];
const convertParamsToArr = (paramsObj) => Object.entries(paramsObj).map(([key, value]) => { return { key, value } })
const getQueryStringParams = (link: String) => { const getQueryStringParams = (link: String) => {
if (link) { if (link) {
@ -69,43 +67,61 @@ const decodeQueryParam = (p) => {
return decodeURIComponent(p.replace(/\+/g, ' ')); return decodeURIComponent(p.replace(/\+/g, ' '));
} }
interface ReplayRequestModalProps {
isOpen: boolean;
onClose: () => void;
}
const ReplayRequestModal: React.FC<ReplayRequestModalProps> = ({ isOpen, onClose }) => { const ReplayRequestModal: React.FC<ReplayRequestModalProps> = ({ isOpen, onClose }) => {
const entryData = useRecoilValue(entryDataAtom) const entryData = useRecoilValue(entryDataAtom)
const request = entryData.data.request const request = entryData.data.request
const [method, setMethod] = useState(request?.method?.toLowerCase() as string)
const getHostUrl = useCallback(() => { const getHostUrl = useCallback(() => {
return entryData.data.dst.name ? entryData.data?.dst?.name : entryData.data.dst.ip return entryData.data.dst.name ? entryData.data?.dst?.name : entryData.data.dst.ip
}, [entryData.data.dst.ip, entryData.data.dst.name]) }, [entryData.data.dst.ip, entryData.data.dst.name])
const [hostPortInput, setHostPortInput] = useState(`${entryData.base.proto.name}://${getHostUrl()}:${entryData.data.dst.port}`) const getHostPortVal = useCallback(() => {
return `${entryData.base.proto.name}://${getHostUrl()}:${entryData.data.dst.port}`
}, [entryData.base.proto.name, entryData.data.dst.port, getHostUrl])
const [hostPortInput, setHostPortInput] = useState(getHostPortVal())
const [pathInput, setPathInput] = useState(request.path); const [pathInput, setPathInput] = useState(request.path);
const commonClasses = useCommonStyles(); const commonClasses = useCommonStyles();
const [currentTab, setCurrentTab] = useState(TABS[0].tab); const [currentTab, setCurrentTab] = useState(TABS[0].tab);
const [response, setResponse] = useState(null); const [response, setResponse] = useState(null);
const [postData, setPostData] = useState(request?.postData?.text || JSON.stringify(request?.postData?.params));
const [params, setParams] = useState(convertParamsToArr(request?.queryString || {}))
const [headers, setHeaders] = useState(convertParamsToArr(request?.headers || {}))
const trafficViewerApi = useRecoilValue(TrafficViewerApiAtom as RecoilState<TrafficViewerApi>) const trafficViewerApi = useRecoilValue(TrafficViewerApiAtom as RecoilState<TrafficViewerApi>)
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [requestExpanded, setRequestExpanded] = useState(true) const [requestExpanded, setRequestExpanded] = useState(true)
const [responseExpanded, setResponseExpanded] = useState(false) const [responseExpanded, setResponseExpanded] = useState(false)
const getInitialRequestData = useCallback((): IReplayRequestData => {
return {
method: request?.method?.toLowerCase() as string,
hostPort: `${entryData.base.proto.name}://${getHostUrl()}:${entryData.data.dst.port}`,
path: request.path,
postData: request.postData?.text || JSON.stringify(request.postData?.params),
headers: convertParamsToArr(request.headers || {}),
params: convertParamsToArr(request.queryString || {})
}
}, [entryData.base.proto.name, entryData.data.dst.port, getHostUrl, request.headers, request?.method, request.path, request.postData?.params, request.postData?.text, request.queryString])
const [requestDataModel, setRequestData] = useState<IReplayRequestData>(getInitialRequestData())
const debouncedPath = useDebounce(pathInput, 500); const debouncedPath = useDebounce(pathInput, 500);
const addParamsToUrl = useCallback((url: string, params: KeyValuePair[]) => {
const urlParams = new URLSearchParams("");
params.forEach(param => urlParams.append(param.key, param.value as string))
return `${url}?${urlParams.toString()}`
}, [])
const onParamsChange = useCallback((newParams) => { const onParamsChange = useCallback((newParams) => {
setParams(newParams);
let newUrl = `${debouncedPath ? debouncedPath.split('?')[0] : ""}` let newUrl = `${debouncedPath ? debouncedPath.split('?')[0] : ""}`
newParams.forEach(({ key, value }, index) => { newUrl = addParamsToUrl(newUrl, newParams)
newUrl += index > 0 ? '&' : '?'
newUrl += `${key}` + (value ? `=${value}` : "")
})
setPathInput(newUrl) setPathInput(newUrl)
}, [addParamsToUrl, debouncedPath])
}, [debouncedPath])
useEffect(() => { useEffect(() => {
const newParams = getQueryStringParams(debouncedPath); const params = convertParamsToArr(getQueryStringParams(debouncedPath));
setParams(convertParamsToArr(newParams)) setRequestData({ ...requestDataModel, params })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedPath]) }, [debouncedPath])
const onModalClose = () => { const onModalClose = () => {
@ -114,33 +130,28 @@ const ReplayRequestModal: React.FC<ReplayRequestModalProps> = ({ isOpen, onClose
onClose() onClose()
} }
const resetModel = useCallback(() => { const resetModal = useCallback((requestDataModel: IReplayRequestData, hostPortInputVal, pathVal) => {
setMethod(request?.method?.toLowerCase() as string) setRequestData(requestDataModel)
setHostPortInput(`${entryData.base.proto.name}://${getHostUrl()}:${entryData.data.dst.port}`) setHostPortInput(hostPortInputVal)
setPathInput(request.path); setPathInput(addParamsToUrl(pathVal, requestDataModel.params));
setResponse(null); setResponse(null);
setPostData(request?.postData?.text || JSON.stringify(request?.postData?.params)); setRequestExpanded(true);
setParams(convertParamsToArr(request?.queryString || {})) }, [addParamsToUrl])
setHeaders(convertParamsToArr(request?.headers || {}))
setRequestExpanded(true)
}, [entryData.base.proto.name, entryData.data.dst.port, getHostUrl, request?.headers, request?.method, request.path, request?.postData?.params, request?.postData?.text, request?.queryString])
const onRefreshRequest = useCallback((event) => { const onRefreshRequest = useCallback((event) => {
event.stopPropagation() event.stopPropagation();
resetModel() const hostPortInputVal = getHostPortVal();
}, [resetModel]) resetModal(getInitialRequestData(), hostPortInputVal, request.path);
}, [getHostPortVal, getInitialRequestData, request.path, resetModal])
const sendRequest = useCallback(async () => { const sendRequest = useCallback(async () => {
setResponse(null) setResponse(null)
const headersData = headers.reduce((prev, corrent) => { const headersData = convertArrToKeyValueObject(requestDataModel.headers)
prev[corrent.key] = corrent.value
return prev
}, {})
const buildUrl = `${hostPortInput}${pathInput}`
const requestData = { url: buildUrl, headers: headersData, data: postData, method }
try { try {
setIsLoading(true) setIsLoading(true)
const requestData = { url: `${hostPortInput}${pathInput}`, headers: headersData, data: requestDataModel.postData, method: requestDataModel.method }
const response = await trafficViewerApi.replayRequest(requestData) const response = await trafficViewerApi.replayRequest(requestData)
setResponse(response?.data?.representation) setResponse(response?.data?.representation)
if (response.errorMessage) { if (response.errorMessage) {
@ -150,7 +161,6 @@ const ReplayRequestModal: React.FC<ReplayRequestModalProps> = ({ isOpen, onClose
setRequestExpanded(false) setRequestExpanded(false)
setResponseExpanded(true) setResponseExpanded(true)
} }
} catch (error) { } catch (error) {
setRequestExpanded(true) setRequestExpanded(true)
toast.error("Error occurred while fetching response", { containerId: TOAST_CONTAINER_ID }); toast.error("Error occurred while fetching response", { containerId: TOAST_CONTAINER_ID });
@ -159,27 +169,37 @@ const ReplayRequestModal: React.FC<ReplayRequestModalProps> = ({ isOpen, onClose
finally { finally {
setIsLoading(false) setIsLoading(false)
} }
}, [hostPortInput, pathInput, requestDataModel.headers, requestDataModel.method, requestDataModel.postData, trafficViewerApi])
}, [headers, hostPortInput, method, pathInput, postData, trafficViewerApi]) const onDownloadRequest = useCallback((e) => {
e.stopPropagation()
const date = Utils.getNow()
Utils.exportToJson(requestDataModel, `${getHostUrl()} - ${date}`)
}, [getHostUrl, requestDataModel])
const onLoadingComplete = useCallback((fileContent: FileContent) => {
const requestData = JSON.parse(fileContent.content) as IReplayRequestData
resetModal(requestData, requestData.hostPort, requestData.path)
}, [resetModal])
let innerComponent let innerComponent
switch (currentTab) { switch (currentTab) {
case RequestTabs.Params: case RequestTabs.Params:
innerComponent = <div className={styles.keyValueContainer}><KeyValueTable data={params} onDataChange={onParamsChange} key={"params"} valuePlaceholder="New Param Value" keyPlaceholder="New param Key" /></div> innerComponent = <div className={styles.keyValueContainer}><KeyValueTable data={requestDataModel.params} onDataChange={onParamsChange} key={"params"} valuePlaceholder="New Param Value" keyPlaceholder="New param Key" /></div>
break; break;
case RequestTabs.Headers: case RequestTabs.Headers:
innerComponent = <Fragment> innerComponent = <Fragment>
<div className={styles.keyValueContainer}><KeyValueTable data={headers} onDataChange={(heaedrs) => setHeaders(heaedrs)} key={"Header"} valuePlaceholder="New Headers Value" keyPlaceholder="New Headers Key" /> <div className={styles.keyValueContainer}><KeyValueTable data={requestDataModel.headers} onDataChange={(headers) => setRequestData({ ...requestDataModel, headers: headers })} key={"Header"} valuePlaceholder="New Headers Value" keyPlaceholder="New Headers Key" />
</div> </div>
<span className={styles.note}><b>* </b> X-Mizu Header added to reuqests</span> <span className={styles.note}><b>* </b> X-Mizu Header added to requests</span>
</Fragment> </Fragment>
break; break;
case RequestTabs.Body: case RequestTabs.Body:
const formatedCode = formatRequestWithOutError(postData || "", request?.postData?.mimeType) const formattedCode = formatRequestWithOutError(requestDataModel.postData || "", request?.postData?.mimeType)
innerComponent = <div className={styles.codeEditor}> innerComponent = <div className={styles.codeEditor}>
<CodeEditor language={request?.postData?.mimeType.split("/")[1]} <CodeEditor language={request?.postData?.mimeType.split("/")[1]}
code={Utils.isJson(formatedCode) ? JSON.stringify(JSON.parse(formatedCode || "{}"), null, 2) : formatedCode} code={Utils.isJson(formattedCode) ? JSON.stringify(JSON.parse(formattedCode || "{}"), null, 2) : formattedCode}
onChange={setPostData} /> onChange={(postData) => setRequestData({ ...requestDataModel, postData })} />
</div> </div>
break; break;
default: default:
@ -204,17 +224,43 @@ const ReplayRequestModal: React.FC<ReplayRequestModalProps> = ({ isOpen, onClose
<div className={styles.headerContainer}> <div className={styles.headerContainer}>
<div className={styles.headerSection}> <div className={styles.headerSection}>
<span className={styles.title}>Replay Request</span> <span className={styles.title}>Replay Request</span>
<Button style={{ marginLeft: "2%", textTransform: 'unset' }}
startIcon={<img src={refreshImg} className="custom" alt="Refresh Request"></img>}
size="medium"
variant="contained"
className={commonClasses.outlinedButton + " " + commonClasses.imagedButton}
onClick={onRefreshRequest}
>
Refresh
</Button>
<Button style={{ marginLeft: "2%", textTransform: 'unset' }}
startIcon={<DownloadIcon className={`custom ${styles.icon}`} />}
size="medium"
variant="contained"
className={commonClasses.outlinedButton + " " + commonClasses.imagedButton}
onClick={onDownloadRequest}
>
Download
</Button>
<FilePicker onLoadingComplete={onLoadingComplete}
elem={<Button style={{ marginLeft: "2%", textTransform: 'unset' }}
startIcon={<UploadIcon className={`custom ${styles.icon}`} />}
size="medium"
variant="contained"
className={commonClasses.outlinedButton + " " + commonClasses.imagedButton}>
Upload
</Button>}
/>
</div> </div>
</div> </div>
<div className={styles.modalContainer}> <div className={styles.modalContainer}>
<Accordion TransitionProps={{ unmountOnExit: true }} expanded={requestExpanded} onChange={() => setRequestExpanded(!requestExpanded)}> <Accordion TransitionProps={{ unmountOnExit: true }} expanded={requestExpanded} onChange={() => setRequestExpanded(!requestExpanded)}>
<AccordionSummary expandIcon={<ExpandMoreIcon />} aria-controls="response-content"> <AccordionSummary expandIcon={<ExpandMoreIcon />} aria-controls="response-content">
<span className={styles.sectionHeader}>REQUEST</span> <span className={styles.sectionHeader}>REQUEST</span>
<img src={refreshImg} style={{ marginLeft: "10px" }} title="Refresh Reuqest" alt="Refresh Reuqest" onClick={onRefreshRequest} />
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
<div className={styles.path}> <div className={styles.path}>
<select className={styles.select} value={method} onChange={(e) => setMethod(e.target.value)}> <select className={styles.select} value={requestDataModel.method} onChange={(e) => setRequestData({ ...requestDataModel, method: e.target.value })}>
{HTTP_METHODS.map(method => <option value={method} key={method}>{method}</option>)} {HTTP_METHODS.map(method => <option value={method} key={method}>{method}</option>)}
</select> </select>
<input placeholder="Host:Port" value={hostPortInput} onChange={(event) => setHostPortInput(event.target.value)} className={`${commonClasses.textField} ${styles.hostPort}`} /> <input placeholder="Host:Port" value={hostPortInput} onChange={(event) => setHostPortInput(event.target.value)} className={`${commonClasses.textField} ${styles.hostPort}`} />
@ -246,7 +292,7 @@ const ReplayRequestModal: React.FC<ReplayRequestModalProps> = ({ isOpen, onClose
</div> </div>
</Box> </Box>
</Fade> </Fade>
</Modal> </Modal >
); );
} }

View File

@ -0,0 +1,13 @@
export interface KeyValuePair {
key: string;
value: unknown;
}
export interface IReplayRequestData {
method: string;
hostPort: string;
path: string;
postData: string;
headers: KeyValuePair[]
params: KeyValuePair[]
}

View File

@ -35,6 +35,7 @@ $modalMargin-from-edge : 35px
color: $blue-gray color: $blue-gray
font-weight: 600 font-weight: 600
margin-right: 35px margin-right: 35px
white-space: nowrap
.graphSection .graphSection
flex: 85% flex: 85%

View File

@ -35,7 +35,7 @@ export const TimelineBarChart: React.FC<TimelineBarChartProps> = ({ timeLineBarC
}) })
protocolsBarsData.push(newProtocolObj); protocolsBarsData.push(newProtocolObj);
}) })
const uniqueObjArray = Utils.creatUniqueObjArrayByProp(prtcNames, "name") const uniqueObjArray = Utils.createUniqueObjArrayByProp(prtcNames, "name")
setProtocolStats(protocolsBarsData); setProtocolStats(protocolsBarsData);
setProtocolsNamesAndColors(uniqueObjArray); setProtocolsNamesAndColors(uniqueObjArray);
}, [data, timeLineBarChartMode]) }, [data, timeLineBarChartMode])
@ -57,7 +57,7 @@ export const TimelineBarChart: React.FC<TimelineBarChartProps> = ({ timeLineBarC
}) })
protocolsMethods.push(newMethodObj); protocolsMethods.push(newMethodObj);
}) })
const uniqueObjArray = Utils.creatUniqueObjArrayByProp(protocolsMethodsNamesAndColors, "name") const uniqueObjArray = Utils.createUniqueObjArrayByProp(protocolsMethodsNamesAndColors, "name")
setMethodsNamesAndColors(uniqueObjArray); setMethodsNamesAndColors(uniqueObjArray);
setMethodsStats(protocolsMethods); setMethodsStats(protocolsMethods);
}, [data, timeLineBarChartMode, selectedProtocol]) }, [data, timeLineBarChartMode, selectedProtocol])

View File

@ -1,5 +1,13 @@
import Moment from 'moment';
const IP_ADDRESS_REGEX = /([0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3})(:([0-9]{1,5}))?/ const IP_ADDRESS_REGEX = /([0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3})(:([0-9]{1,5}))?/
type JSONValue =
| string
| number
| boolean
| Object
export class Utils { export class Utils {
static isIpAddress = (address: string): boolean => IP_ADDRESS_REGEX.test(address) static isIpAddress = (address: string): boolean => IP_ADDRESS_REGEX.test(address)
@ -51,7 +59,7 @@ export class Utils {
return [hoursAndMinutes, newDate].join(' '); return [hoursAndMinutes, newDate].join(' ');
} }
static creatUniqueObjArrayByProp = (objArray, prop) => { static createUniqueObjArrayByProp = (objArray, prop) => {
const map = new Map(objArray.map((item) => [item[prop], item])).values() const map = new Map(objArray.map((item) => [item[prop], item])).values()
return Array.from(map); return Array.from(map);
} }
@ -65,4 +73,24 @@ export class Utils {
return true; return true;
} }
static downloadFile = (data: string, filename: string, fileType: string) => {
const blob = new Blob([data], { type: fileType })
const a = document.createElement('a');
a.href = window.URL.createObjectURL(blob);
a.download = filename;
a.click();
a.remove();
}
static exportToJson = (data: JSONValue, name) => {
Utils.downloadFile(JSON.stringify(data), `${name}.json`, 'text/json')
}
static getTimeFormatted = (time: Moment.MomentInput) => {
return Moment(time).utc().format('MM/DD/YYYY, h:mm:ss.SSS A')
}
static getNow = (format: string = 'MM/DD/YYYY, HH:mm:ss.SSS') => {
return Moment().format(format)
}
} }