diff --git a/ui-common/package.json b/ui-common/package.json index 0cb0c99db..07a26c25a 100644 --- a/ui-common/package.json +++ b/ui-common/package.json @@ -65,6 +65,7 @@ "recharts": "^2.1.10", "redoc": "^2.0.0-rc.71", "styled-components": "^5.3.5", + "use-file-picker": "^1.4.2", "web-vitals": "^2.1.4", "xml-formatter": "^2.6.1" }, diff --git a/ui-common/src/components.scss b/ui-common/src/components.scss index 901c1505b..d9ccdfb94 100644 --- a/ui-common/src/components.scss +++ b/ui-common/src/components.scss @@ -17,6 +17,6 @@ width: 100%; width: -moz-available; width: -webkit-fill-available; - width: strech; + width: stretch; } -} \ No newline at end of file +} diff --git a/ui-common/src/components/EntriesList/EntriesList.tsx b/ui-common/src/components/EntriesList/EntriesList.tsx index 28a1a8edb..b04b23d16 100644 --- a/ui-common/src/components/EntriesList/EntriesList.tsx +++ b/ui-common/src/components/EntriesList/EntriesList.tsx @@ -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 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 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 queryAtom from "../../recoil/query"; import TrafficViewerApiAtom from "../../recoil/TrafficViewerApi"; import TrafficViewerApi from "../TrafficViewer/TrafficViewerApi"; import focusedEntryIdAtom from "../../recoil/focusedEntryId"; -import {toast} from "react-toastify"; -import {MAX_ENTRIES, TOAST_CONTAINER_ID} from "../../configs/Consts"; +import { toast } from "react-toastify"; +import { MAX_ENTRIES, TOAST_CONTAINER_ID } from "../../configs/Consts"; import tappingStatusAtom from "../../recoil/tappingStatus"; import leftOffTopAtom from "../../recoil/leftOffTop"; +import Moment from "moment"; interface EntriesListProps { listEntryREF: any; diff --git a/ui-common/src/components/UI/CodeEditor/CodeEditor.tsx b/ui-common/src/components/UI/CodeEditor/CodeEditor.tsx index bdbd41a7d..5cb171e5f 100644 --- a/ui-common/src/components/UI/CodeEditor/CodeEditor.tsx +++ b/ui-common/src/components/UI/CodeEditor/CodeEditor.tsx @@ -37,11 +37,6 @@ const CodeEditor: React.FC = ({ theme="github" onChange={onChange} editorProps={{ $blockScrolling: true }} - setOptions={{ - enableBasicAutocompletion: true, - enableLiveAutocompletion: true, - enableSnippets: true - }} showPrintMargin={false} value={code} width="100%" diff --git a/ui-common/src/components/UI/FilePicker/FilePicker.tsx b/ui-common/src/components/UI/FilePicker/FilePicker.tsx new file mode 100644 index 000000000..9fba41259 --- /dev/null +++ b/ui-common/src/components/UI/FilePicker/FilePicker.tsx @@ -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.cloneElement(elem, { onClick: onFileSelectorClick })} + ) +} + +export default FilePicker; diff --git a/ui-common/src/components/UI/KeyValueTable/KeyValueTable.tsx b/ui-common/src/components/UI/KeyValueTable/KeyValueTable.tsx index e592c2006..4d29daab3 100644 --- a/ui-common/src/components/UI/KeyValueTable/KeyValueTable.tsx +++ b/ui-common/src/components/UI/KeyValueTable/KeyValueTable.tsx @@ -75,5 +75,6 @@ const KeyValueTable: React.FC = ({ data, onDataChange, keyPl })} } - +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 diff --git a/ui-common/src/components/modals/ReplayRequestModal/ReplayRequestModal.module.sass b/ui-common/src/components/modals/ReplayRequestModal/ReplayRequestModal.module.sass index 3ce2064a2..ff74643ea 100644 --- a/ui-common/src/components/modals/ReplayRequestModal/ReplayRequestModal.module.sass +++ b/ui-common/src/components/modals/ReplayRequestModal/ReplayRequestModal.module.sass @@ -79,5 +79,12 @@ overflow: hidden b::after - content: '\b' + content: '\b' display: inline + +.icon + width: 24px + height: 26px + stroke-width: 0px + fill: $blue-color + stroke: $blue-color diff --git a/ui-common/src/components/modals/ReplayRequestModal/ReplayRequestModal.tsx b/ui-common/src/components/modals/ReplayRequestModal/ReplayRequestModal.tsx index 8d4c6328d..f4916893c 100644 --- a/ui-common/src/components/modals/ReplayRequestModal/ReplayRequestModal.tsx +++ b/ui-common/src/components/modals/ReplayRequestModal/ReplayRequestModal.tsx @@ -1,25 +1,30 @@ -import { Accordion, AccordionDetails, AccordionSummary, Backdrop, Box, Button, Fade, Modal } from "@mui/material"; 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 { 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 { RecoilState, useRecoilState, useRecoilValue } from "recoil"; +import { FileContent } from "use-file-picker/dist/interfaces"; import { TOAST_CONTAINER_ID } from "../../../configs/Consts"; -import styles from './ReplayRequestModal.module.sass' -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 { useCommonStyles } from "../../../helpers/commonStyle"; 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 { IReplayRequestData, KeyValuePair } from './interfaces'; +import styles from './ReplayRequestModal.module.sass'; const modalStyle = { position: 'absolute', @@ -37,11 +42,6 @@ const modalStyle = { paddingBottom: "15px" }; -interface ReplayRequestModalProps { - isOpen: boolean; - onClose: () => void; -} - enum RequestTabs { Params = "params", Headers = "headers", @@ -51,8 +51,6 @@ enum RequestTabs { const HTTP_METHODS = ["get", "post", "put", "head", "options", "delete"] 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) => { if (link) { @@ -69,43 +67,61 @@ const decodeQueryParam = (p) => { return decodeURIComponent(p.replace(/\+/g, ' ')); } +interface ReplayRequestModalProps { + isOpen: boolean; + onClose: () => void; +} + const ReplayRequestModal: React.FC = ({ isOpen, onClose }) => { const entryData = useRecoilValue(entryDataAtom) const request = entryData.data.request - const [method, setMethod] = useState(request?.method?.toLowerCase() as string) const getHostUrl = useCallback(() => { return entryData.data.dst.name ? entryData.data?.dst?.name : entryData.data.dst.ip }, [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 commonClasses = useCommonStyles(); const [currentTab, setCurrentTab] = useState(TABS[0].tab); 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) const [isLoading, setIsLoading] = useState(false) const [requestExpanded, setRequestExpanded] = useState(true) 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(getInitialRequestData()) + 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) => { - setParams(newParams); let newUrl = `${debouncedPath ? debouncedPath.split('?')[0] : ""}` - newParams.forEach(({ key, value }, index) => { - newUrl += index > 0 ? '&' : '?' - newUrl += `${key}` + (value ? `=${value}` : "") - }) - + newUrl = addParamsToUrl(newUrl, newParams) setPathInput(newUrl) - - }, [debouncedPath]) + }, [addParamsToUrl, debouncedPath]) useEffect(() => { - const newParams = getQueryStringParams(debouncedPath); - setParams(convertParamsToArr(newParams)) + const params = convertParamsToArr(getQueryStringParams(debouncedPath)); + setRequestData({ ...requestDataModel, params }) + // eslint-disable-next-line react-hooks/exhaustive-deps }, [debouncedPath]) const onModalClose = () => { @@ -114,33 +130,28 @@ const ReplayRequestModal: React.FC = ({ isOpen, onClose onClose() } - const resetModel = useCallback(() => { - setMethod(request?.method?.toLowerCase() as string) - setHostPortInput(`${entryData.base.proto.name}://${getHostUrl()}:${entryData.data.dst.port}`) - setPathInput(request.path); + const resetModal = useCallback((requestDataModel: IReplayRequestData, hostPortInputVal, pathVal) => { + setRequestData(requestDataModel) + setHostPortInput(hostPortInputVal) + setPathInput(addParamsToUrl(pathVal, requestDataModel.params)); setResponse(null); - setPostData(request?.postData?.text || JSON.stringify(request?.postData?.params)); - setParams(convertParamsToArr(request?.queryString || {})) - 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]) + setRequestExpanded(true); + }, [addParamsToUrl]) const onRefreshRequest = useCallback((event) => { - event.stopPropagation() - resetModel() - }, [resetModel]) + event.stopPropagation(); + const hostPortInputVal = getHostPortVal(); + resetModal(getInitialRequestData(), hostPortInputVal, request.path); + }, [getHostPortVal, getInitialRequestData, request.path, resetModal]) const sendRequest = useCallback(async () => { setResponse(null) - const headersData = headers.reduce((prev, corrent) => { - prev[corrent.key] = corrent.value - return prev - }, {}) - const buildUrl = `${hostPortInput}${pathInput}` - const requestData = { url: buildUrl, headers: headersData, data: postData, method } + const headersData = convertArrToKeyValueObject(requestDataModel.headers) + try { setIsLoading(true) + const requestData = { url: `${hostPortInput}${pathInput}`, headers: headersData, data: requestDataModel.postData, method: requestDataModel.method } const response = await trafficViewerApi.replayRequest(requestData) setResponse(response?.data?.representation) if (response.errorMessage) { @@ -150,7 +161,6 @@ const ReplayRequestModal: React.FC = ({ isOpen, onClose setRequestExpanded(false) setResponseExpanded(true) } - } catch (error) { setRequestExpanded(true) toast.error("Error occurred while fetching response", { containerId: TOAST_CONTAINER_ID }); @@ -159,27 +169,37 @@ const ReplayRequestModal: React.FC = ({ isOpen, onClose finally { 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 switch (currentTab) { case RequestTabs.Params: - innerComponent =
+ innerComponent =
break; case RequestTabs.Headers: innerComponent = -
setHeaders(heaedrs)} key={"Header"} valuePlaceholder="New Headers Value" keyPlaceholder="New Headers Key" /> +
setRequestData({ ...requestDataModel, headers: headers })} key={"Header"} valuePlaceholder="New Headers Value" keyPlaceholder="New Headers Key" />
- * X-Mizu Header added to reuqests + * X-Mizu Header added to requests break; case RequestTabs.Body: - const formatedCode = formatRequestWithOutError(postData || "", request?.postData?.mimeType) + const formattedCode = formatRequestWithOutError(requestDataModel.postData || "", request?.postData?.mimeType) innerComponent =
+ code={Utils.isJson(formattedCode) ? JSON.stringify(JSON.parse(formattedCode || "{}"), null, 2) : formattedCode} + onChange={(postData) => setRequestData({ ...requestDataModel, postData })} />
break; default: @@ -204,17 +224,43 @@ const ReplayRequestModal: React.FC = ({ isOpen, onClose
Replay Request + + + } + size="medium" + variant="contained" + className={commonClasses.outlinedButton + " " + commonClasses.imagedButton}> + Upload + } + />
setRequestExpanded(!requestExpanded)}> } aria-controls="response-content"> REQUEST - Refresh Reuqest
- setRequestData({ ...requestDataModel, method: e.target.value })}> {HTTP_METHODS.map(method => )} setHostPortInput(event.target.value)} className={`${commonClasses.textField} ${styles.hostPort}`} /> @@ -246,7 +292,7 @@ const ReplayRequestModal: React.FC = ({ isOpen, onClose
- + ); } diff --git a/ui-common/src/components/modals/ReplayRequestModal/interfaces.ts b/ui-common/src/components/modals/ReplayRequestModal/interfaces.ts new file mode 100644 index 000000000..f90189473 --- /dev/null +++ b/ui-common/src/components/modals/ReplayRequestModal/interfaces.ts @@ -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[] +} diff --git a/ui-common/src/components/modals/ServiceMapModal/ServiceMapModal.module.sass b/ui-common/src/components/modals/ServiceMapModal/ServiceMapModal.module.sass index 729075b5c..af3e56785 100644 --- a/ui-common/src/components/modals/ServiceMapModal/ServiceMapModal.module.sass +++ b/ui-common/src/components/modals/ServiceMapModal/ServiceMapModal.module.sass @@ -35,6 +35,7 @@ $modalMargin-from-edge : 35px color: $blue-gray font-weight: 600 margin-right: 35px + white-space: nowrap .graphSection flex: 85% diff --git a/ui-common/src/components/modals/TrafficStatsModal/TimelineBarChart/TimelineBarChart.tsx b/ui-common/src/components/modals/TrafficStatsModal/TimelineBarChart/TimelineBarChart.tsx index 0f3384c6b..2e3035611 100644 --- a/ui-common/src/components/modals/TrafficStatsModal/TimelineBarChart/TimelineBarChart.tsx +++ b/ui-common/src/components/modals/TrafficStatsModal/TimelineBarChart/TimelineBarChart.tsx @@ -35,7 +35,7 @@ export const TimelineBarChart: React.FC = ({ timeLineBarC }) protocolsBarsData.push(newProtocolObj); }) - const uniqueObjArray = Utils.creatUniqueObjArrayByProp(prtcNames, "name") + const uniqueObjArray = Utils.createUniqueObjArrayByProp(prtcNames, "name") setProtocolStats(protocolsBarsData); setProtocolsNamesAndColors(uniqueObjArray); }, [data, timeLineBarChartMode]) @@ -57,7 +57,7 @@ export const TimelineBarChart: React.FC = ({ timeLineBarC }) protocolsMethods.push(newMethodObj); }) - const uniqueObjArray = Utils.creatUniqueObjArrayByProp(protocolsMethodsNamesAndColors, "name") + const uniqueObjArray = Utils.createUniqueObjArrayByProp(protocolsMethodsNamesAndColors, "name") setMethodsNamesAndColors(uniqueObjArray); setMethodsStats(protocolsMethods); }, [data, timeLineBarChartMode, selectedProtocol]) diff --git a/ui-common/src/helpers/Utils.ts b/ui-common/src/helpers/Utils.ts index 01fab2493..284f7181f 100644 --- a/ui-common/src/helpers/Utils.ts +++ b/ui-common/src/helpers/Utils.ts @@ -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}))?/ +type JSONValue = + | string + | number + | boolean + | Object + export class Utils { static isIpAddress = (address: string): boolean => IP_ADDRESS_REGEX.test(address) @@ -51,7 +59,7 @@ export class Utils { return [hoursAndMinutes, newDate].join(' '); } - static creatUniqueObjArrayByProp = (objArray, prop) => { + static createUniqueObjArrayByProp = (objArray, prop) => { const map = new Map(objArray.map((item) => [item[prop], item])).values() return Array.from(map); } @@ -65,4 +73,24 @@ export class Utils { 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) + } }