From a4d0e250c904732a30bc82d944f11443c9408439 Mon Sep 17 00:00:00 2001 From: gadotroee <55343099+gadotroee@users.noreply.github.com> Date: Mon, 21 Mar 2022 14:21:08 +0200 Subject: [PATCH] Fixing acceptance tests (#900) --- .../testHelpers/StatusBarHelper.js | 9 +- .../integration/testHelpers/TrafficHelper.js | 4 +- .../cypress/integration/tests/GuiPort.js | 2 +- .../integration/tests/MultipleNamespaces.js | 2 +- .../cypress/integration/tests/Regex.js | 4 +- .../cypress/integration/tests/UiTest.js | 3 +- ui-common/example/src/App.tsx | 8 +- ui-common/package.json | 3 +- .../TrafficViewer/TrafficViewer.tsx | 201 +++++++++--------- .../TrafficViewer/TrafficViewerApi.ts | 6 +- .../components/TrafficViewer/assets/ebpf.png | Bin 0 -> 21879 bytes ui-common/src/components/UI/Queryable.tsx | 2 +- ui-common/src/components/UI/StatusBar.tsx | 5 +- ui-common/src/hooks/useWS.tsx | 20 +- ui/package.json | 2 +- .../Pages/TrafficPage/TrafficPage.tsx | 19 +- 16 files changed, 142 insertions(+), 148 deletions(-) create mode 100644 ui-common/src/components/TrafficViewer/assets/ebpf.png diff --git a/acceptanceTests/cypress/integration/testHelpers/StatusBarHelper.js b/acceptanceTests/cypress/integration/testHelpers/StatusBarHelper.js index 7f3ab13db..d16581752 100644 --- a/acceptanceTests/cypress/integration/testHelpers/StatusBarHelper.js +++ b/acceptanceTests/cypress/integration/testHelpers/StatusBarHelper.js @@ -1,8 +1,7 @@ const columns = {podName : 1, namespace : 2, tapping : 3}; -const greenStatusImageSrc = '/static/media/success.662997eb.svg'; function getDomPathInStatusBar(line, column) { - return `.expandedStatusBar > :nth-child(2) > > :nth-child(2) > :nth-child(${line}) > :nth-child(${column})`; + return `[data-cy="expandedStatusBar"] > :nth-child(2) > > :nth-child(2) > :nth-child(${line}) > :nth-child(${column})`; } export function checkLine(line, expectedValues) { @@ -12,14 +11,14 @@ export function checkLine(line, expectedValues) { cy.get(getDomPathInStatusBar(line, columns.namespace)).invoke('text').then(namespaceValue => { expect(namespaceValue).to.equal(expectedValues.namespace); - cy.get(getDomPathInStatusBar(line, columns.tapping)).children().should('have.attr', 'src', greenStatusImageSrc); + cy.get(getDomPathInStatusBar(line, columns.tapping)).children().should('have.attr', 'src').and("match", /success.*\.svg/); }); }); } export function findLineAndCheck(expectedValues) { - cy.get('.expandedStatusBar > :nth-child(2) > > :nth-child(2) > > :nth-child(1)').then(pods => { - cy.get('.expandedStatusBar > :nth-child(2) > > :nth-child(2) > > :nth-child(2)').then(namespaces => { + cy.get('[data-cy="expandedStatusBar"] > :nth-child(2) > > :nth-child(2) > > :nth-child(1)').then(pods => { + cy.get('[data-cy="expandedStatusBar"] > :nth-child(2) > > :nth-child(2) > > :nth-child(2)').then(namespaces => { // organizing namespaces array const podObjectsArray = Object.values(pods ?? {}); const namespacesObjectsArray = Object.values(namespaces ?? {}); diff --git a/acceptanceTests/cypress/integration/testHelpers/TrafficHelper.js b/acceptanceTests/cypress/integration/testHelpers/TrafficHelper.js index b4b668b77..b125271ac 100644 --- a/acceptanceTests/cypress/integration/testHelpers/TrafficHelper.js +++ b/acceptanceTests/cypress/integration/testHelpers/TrafficHelper.js @@ -45,7 +45,7 @@ export function leftTextCheck(entryNum, path, expectedText) { export function leftOnHoverCheck(entryNum, path, filterName) { cy.get(`#list #entry-${entryNum} ${path}`).trigger('mouseover'); - cy.get(`#list #entry-${entryNum} .Queryable-Tooltip`).invoke('text').should('match', new RegExp(filterName)); + cy.get(`#list #entry-${entryNum} [data-cy='QueryableTooltip']`).invoke('text').should('match', new RegExp(filterName)); } export function rightTextCheck(path, expectedText) { @@ -54,7 +54,7 @@ export function rightTextCheck(path, expectedText) { export function rightOnHoverCheck(path, expectedText) { cy.get(`#rightSideContainer ${path}`).trigger('mouseover'); - cy.get(`#rightSideContainer .Queryable-Tooltip`).invoke('text').should('match', new RegExp(expectedText)); + cy.get(`#rightSideContainer [data-cy='QueryableTooltip']`).invoke('text').should('match', new RegExp(expectedText)); } export function checkThatAllEntriesShown() { diff --git a/acceptanceTests/cypress/integration/tests/GuiPort.js b/acceptanceTests/cypress/integration/tests/GuiPort.js index 16627db19..e47e53fbf 100644 --- a/acceptanceTests/cypress/integration/tests/GuiPort.js +++ b/acceptanceTests/cypress/integration/tests/GuiPort.js @@ -8,6 +8,6 @@ it('check', function () { cy.visit(`http://localhost:${port}`); cy.wait('@statusTap').its('response.statusCode').should('match', /^2\d{2}/); - cy.get('.podsCount').trigger('mouseover'); + cy.get(`[data-cy="expandedStatusBar"]`).trigger('mouseover',{force: true}); findLineAndCheck(getExpectedDetailsDict(podName, namespace)); }); diff --git a/acceptanceTests/cypress/integration/tests/MultipleNamespaces.js b/acceptanceTests/cypress/integration/tests/MultipleNamespaces.js index c27a3636e..c696747c1 100644 --- a/acceptanceTests/cypress/integration/tests/MultipleNamespaces.js +++ b/acceptanceTests/cypress/integration/tests/MultipleNamespaces.js @@ -2,7 +2,7 @@ import {findLineAndCheck, getExpectedDetailsDict} from '../testHelpers/StatusBar it('opening', function () { cy.visit(Cypress.env('testUrl')); - cy.get('.podsCount').trigger('mouseover'); + cy.get(`[data-cy="podsCountText"]`).trigger('mouseover'); }); [1, 2, 3].map(doItFunc); diff --git a/acceptanceTests/cypress/integration/tests/Regex.js b/acceptanceTests/cypress/integration/tests/Regex.js index de449a7a4..ac9ddf9ef 100644 --- a/acceptanceTests/cypress/integration/tests/Regex.js +++ b/acceptanceTests/cypress/integration/tests/Regex.js @@ -3,9 +3,9 @@ import {getExpectedDetailsDict, checkLine} from '../testHelpers/StatusBarHelper' it('opening', function () { cy.visit(Cypress.env('testUrl')); - cy.get('.podsCount').trigger('mouseover'); + cy.get(`[data-cy="podsCountText"]`).trigger('mouseover'); - cy.get('.expandedStatusBar > :nth-child(2) > > :nth-child(2) >').should('have.length', 1); // one line + cy.get('[data-cy="expandedStatusBar"] > :nth-child(2) > > :nth-child(2) >').should('have.length', 1); // one line checkLine(1, getExpectedDetailsDict(Cypress.env('name'), Cypress.env('namespace'))); }); diff --git a/acceptanceTests/cypress/integration/tests/UiTest.js b/acceptanceTests/cypress/integration/tests/UiTest.js index 01c131238..ec2395a17 100644 --- a/acceptanceTests/cypress/integration/tests/UiTest.js +++ b/acceptanceTests/cypress/integration/tests/UiTest.js @@ -26,7 +26,7 @@ it('opening mizu', function () { verifyMinimumEntries(); it('top bar check', function () { - cy.get('.podsCount').trigger('mouseover'); + cy.get(`[data-cy="podsCountText"]`).trigger('mouseover'); podsArray.map(findLineAndCheck); cy.reload(); }); @@ -205,6 +205,7 @@ function checkFilter(filterDetails){ // checks the hover on the last entry (the only one in DOM at the beginning) leftOnHoverCheck(totalEntries - 1, leftSidePath, name); + cy.get('.w-tc-editor-text').clear(); // applying the filter with alt+enter or with the button cy.get('.w-tc-editor-text').type(`${name}${applyByEnter ? '{alt+enter}' : ''}`); cy.get('.w-tc-editor').should('have.attr', 'style').and('include', Cypress.env('greenFilterColor')); diff --git a/ui-common/example/src/App.tsx b/ui-common/example/src/App.tsx index 5ec1e3c68..111318db5 100644 --- a/ui-common/example/src/App.tsx +++ b/ui-common/example/src/App.tsx @@ -5,10 +5,10 @@ import Api, {getWebsocketUrl} from "./api"; const api = Api.getInstance() -const App = () => { - const {message,error,isOpen, openSocket, closeSocket, sendQuery} = useWS(getWebsocketUrl()) - const trafficViewerApi = {...api, webSocket:{open : openSocket, close: closeSocket, sendQuery: sendQuery}} - sendQuery(DEFAULT_QUERY); +const App = () => { + const {message,error,isOpen, openSocket, closeSocket, sendQueryWhenWsOpen} = useWS(getWebsocketUrl()) + const trafficViewerApi = {...api, webSocket:{open : openSocket, close: closeSocket, sendQueryWhenWsOpen: sendQueryWhenWsOpen}} + sendQueryWhenWsOpen(DEFAULT_QUERY); useEffect(() => { return () =>{ diff --git a/ui-common/package.json b/ui-common/package.json index d08771ed9..dd174e752 100644 --- a/ui-common/package.json +++ b/ui-common/package.json @@ -1,6 +1,6 @@ { "name": "@up9/mizu-common", - "version": "1.0.125", + "version": "1.0.128", "description": "Made with create-react-library", "author": "", "license": "MIT", @@ -30,7 +30,6 @@ "react":"^17.0.2", "react-dom": "^17.0.2", "recoil": "^0.5.2", - "react-copy-to-clipboard": "^5.0.3", "@types/jest": "^26.0.22", "@types/node": "^12.20.10" diff --git a/ui-common/src/components/TrafficViewer/TrafficViewer.tsx b/ui-common/src/components/TrafficViewer/TrafficViewer.tsx index 86f4d8f5c..2d9326980 100644 --- a/ui-common/src/components/TrafficViewer/TrafficViewer.tsx +++ b/ui-common/src/components/TrafficViewer/TrafficViewer.tsx @@ -8,7 +8,8 @@ import { EntryDetailed } from "./EntryDetailed"; import playIcon from 'assets/run.svg'; import pauseIcon from 'assets/pause.svg'; import variables from '../../variables.module.scss'; -import { toast } from 'react-toastify'; +import { toast,ToastContainer } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; import debounce from 'lodash/debounce'; import { RecoilRoot, RecoilState, useRecoilState, useRecoilValue, useSetRecoilState } from "recoil"; import entriesAtom from "../../recoil/entries"; @@ -21,7 +22,6 @@ import TrafficViewerApi from "./TrafficViewerApi"; import { StatusBar } from "../UI/StatusBar"; import tappingStatusAtom from "../../recoil/tappingStatus/atom"; - const useLayoutStyles = makeStyles(() => ({ details: { flex: "0 0 50%", @@ -45,26 +45,22 @@ const useLayoutStyles = makeStyles(() => ({ interface TrafficViewerProps { setAnalyzeStatus?: (status: any) => void; api?: any - message?: {} - error?: {} - isWebSocketOpen: boolean trafficViewerApiProp: TrafficViewerApi, actionButtons?: JSX.Element, - isShowStatusBar?: boolean + isShowStatusBar?: boolean, + webSocketUrl : string } -const TrafficViewer: React.FC = ({ setAnalyzeStatus, message, error, isWebSocketOpen, trafficViewerApiProp, actionButtons, isShowStatusBar }) => { +export const TrafficViewer : React.FC = ({setAnalyzeStatus, trafficViewerApiProp, actionButtons,isShowStatusBar,webSocketUrl}) => { + const classes = useLayoutStyles(); const [entries, setEntries] = useRecoilState(entriesAtom); const [focusedEntryId, setFocusedEntryId] = useRecoilState(focusedEntryIdAtom); const [wsConnection, setWsConnection] = useRecoilState(websocketConnectionAtom); const query = useRecoilValue(queryAtom); - const [queryToSend, setQueryToSend] = useState("") const setTrafficViewerApiState = useSetRecoilState(trafficViewerApiAtom as RecoilState) const [tappingStatus, setTappingStatus] = useRecoilState(tappingStatusAtom); - - const [noMoreDataTop, setNoMoreDataTop] = useState(false); const [isSnappedToBottom, setIsSnappedToBottom] = useState(true); @@ -107,6 +103,7 @@ const TrafficViewer: React.FC = ({ setAnalyzeStatus, message handleQueryChange(query); }, [query, handleQueryChange]); + const ws = useRef(null); const listEntry = useRef(null); const openWebSocket = (query: string, resetEntries: boolean) => { @@ -117,98 +114,99 @@ const TrafficViewer: React.FC = ({ setAnalyzeStatus, message setLeftOffTop(null); setNoMoreDataTop(false); } - setQueryToSend(query) - trafficViewerApiProp.webSocket.open(); - } - - const onmessage = useCallback((e) => { - if (!e?.data) return; - const message = JSON.parse(e.data); - switch (message.messageType) { - case "entry": - const entry = message.data; - if (!focusedEntryId) setFocusedEntryId(entry.id.toString()); - const newEntries = [...entries, entry]; - if (newEntries.length === 10001) { - setLeftOffTop(newEntries[0].entry.id); - newEntries.shift(); - setNoMoreDataTop(false); - } - setEntries(newEntries); - break; - case "status": - setTappingStatus(message.tappingStatus); - break; - case "analyzeStatus": - setAnalyzeStatus(message.analyzeStatus); - break; - case "outboundLink": - onTLSDetected(message.Data.DstIP); - break; - case "toast": - toast[message.data.type](message.data.text, { - position: "bottom-right", - theme: "colored", - autoClose: message.data.autoClose, - hideProgressBar: false, - closeOnClick: true, - pauseOnHover: true, - draggable: true, - progress: undefined, - }); - break; - case "queryMetadata": - setQueriedCurrent(queriedCurrent + message.data.current); - setQueriedTotal(message.data.total); - setLeftOffBottom(message.data.leftOff); - setTruncatedTimestamp(message.data.truncatedTimestamp); - if (leftOffTop === null) { - setLeftOffTop(message.data.leftOff - 1); - } - break; - case "startTime": - setStartTime(message.data); - break; - default: - console.error( - `unsupported websocket message type, Got: ${message.messageType}` - ); + ws.current = new WebSocket(webSocketUrl); + ws.current.onopen = () => { + setWsConnection(WsConnectionStatus.Connected); + sendQueryWhenWsOpen(query); } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [message]); - - useEffect(() => { - onmessage(message) - }, [message, onmessage]) - - useEffect(() => { - onerror(error) - }, [error]) - - - useEffect(() => { - isWebSocketOpen ? setWsConnection(WsConnectionStatus.Connected) : setWsConnection(WsConnectionStatus.Closed) - trafficViewerApiProp.webSocket.sendQuery(queryToSend) - }, [isWebSocketOpen, queryToSend, setWsConnection]) - - const onerror = (event) => { - console.error("WebSocket error:", event); - if (query) { - openWebSocket(`(${query}) and leftOff(${leftOffBottom})`, false); - } else { - openWebSocket(`leftOff(${leftOffBottom})`, false); + ws.current.onclose = () => { + setWsConnection(WsConnectionStatus.Closed); + } + ws.current.onerror = (event) => { + console.error("WebSocket error:", event); + if (query) { + openWebSocket(`(${query}) and leftOff(${leftOffBottom})`, false); + } else { + openWebSocket(`leftOff(${leftOffBottom})`, false); + } } } + const sendQueryWhenWsOpen = (query) => { + setTimeout(() => { + if (ws?.current?.readyState === WebSocket.OPEN) { + ws.current.send(JSON.stringify({"query": query, "enableFullEntries": false})); + } else { + sendQueryWhenWsOpen(query); + } + }, 500) + } + + if (ws.current) { + ws.current.onmessage = (e) => { + if (!e?.data) return; + const message = JSON.parse(e.data); + switch (message.messageType) { + case "entry": + const entry = message.data; + if (!focusedEntryId) setFocusedEntryId(entry.id.toString()); + const newEntries = [...entries, entry]; + if (newEntries.length === 10001) { + setLeftOffTop(newEntries[0].entry.id); + newEntries.shift(); + setNoMoreDataTop(false); + } + setEntries(newEntries); + break; + case "status": + setTappingStatus(message.tappingStatus); + break; + case "analyzeStatus": + setAnalyzeStatus(message.analyzeStatus); + break; + case "outboundLink": + onTLSDetected(message.Data.DstIP); + break; + case "toast": + toast[message.data.type](message.data.text, { + position: "bottom-right", + theme: "colored", + autoClose: message.data.autoClose, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: true, + draggable: true, + progress: undefined, + }); + break; + case "queryMetadata": + setQueriedCurrent(queriedCurrent + message.data.current); + setQueriedTotal(message.data.total); + setLeftOffBottom(message.data.leftOff); + setTruncatedTimestamp(message.data.truncatedTimestamp); + if (leftOffTop === null) { + setLeftOffTop(message.data.leftOff - 1); + } + break; + case "startTime": + setStartTime(message.data); + break; + default: + console.error( + `unsupported websocket message type, Got: ${message.messageType}` + ); + } + }; + } + useEffect(() => { + setTrafficViewerApiState({...trafficViewerApiProp, webSocket : {close : () => ws.current.close()}}); (async () => { - setTrafficViewerApiState(trafficViewerApiProp) openWebSocket("leftOff(-1)", true); - try { + try{ const tapStatusResponse = await trafficViewerApiProp.tapStatus(); setTappingStatus(tapStatusResponse); - if (setAnalyzeStatus) { + if(setAnalyzeStatus) { const analyzeStatusResponse = await trafficViewerApiProp.analyzeStatus(); setAnalyzeStatus(analyzeStatusResponse); } @@ -220,8 +218,8 @@ const TrafficViewer: React.FC = ({ setAnalyzeStatus, message }, []); const toggleConnection = () => { - if (wsConnection === WsConnectionStatus.Closed) { - + ws.current.close(); + if (wsConnection !== WsConnectionStatus.Connected) { if (query) { openWebSocket(`(${query}) and leftOff(-1)`, true); } else { @@ -230,10 +228,6 @@ const TrafficViewer: React.FC = ({ setAnalyzeStatus, message scrollableRef.current.jumpToBottom(); setIsSnappedToBottom(true); } - else if (wsConnection === WsConnectionStatus.Connected) { - trafficViewerApiProp.webSocket.close() - setWsConnection(WsConnectionStatus.Closed); - } } const onTLSDetected = (destAddress: string) => { @@ -270,7 +264,7 @@ const TrafficViewer: React.FC = ({ setAnalyzeStatus, message const onSnapBrokenEvent = () => { setIsSnappedToBottom(false); if (wsConnection === WsConnectionStatus.Connected) { - trafficViewerApiProp.webSocket.close() + ws.current.close(); } } @@ -330,16 +324,17 @@ const TrafficViewer: React.FC = ({ setAnalyzeStatus, message setAddressesWithTLS={setAddressesWithTLS} userDismissedTLSWarning={userDismissedTLSWarning} setUserDismissedTLSWarning={setUserDismissedTLSWarning} /> + ); }; const MemoiedTrafficViewer = React.memo(TrafficViewer) -const TrafficViewerContainer: React.FC = ({ setAnalyzeStatus, message, isWebSocketOpen, trafficViewerApiProp, actionButtons, isShowStatusBar = true }) => { +const TrafficViewerContainer: React.FC = ({ setAnalyzeStatus, trafficViewerApiProp, actionButtons, isShowStatusBar = true ,webSocketUrl}) => { return - } -export default TrafficViewerContainer \ No newline at end of file +export default TrafficViewerContainer diff --git a/ui-common/src/components/TrafficViewer/TrafficViewerApi.ts b/ui-common/src/components/TrafficViewer/TrafficViewerApi.ts index 0aa753bd4..e05744e7a 100644 --- a/ui-common/src/components/TrafficViewer/TrafficViewerApi.ts +++ b/ui-common/src/components/TrafficViewer/TrafficViewerApi.ts @@ -6,10 +6,8 @@ type TrafficViewerApi = { getEntry : (entryId : any, query:string) => any getRecentTLSLinks : () => any, webSocket : { - open : () => {}, - close : () => {}, - sendQuery : (query:string) => {} + close : () => {} } } - export default TrafficViewerApi \ No newline at end of file + export default TrafficViewerApi diff --git a/ui-common/src/components/TrafficViewer/assets/ebpf.png b/ui-common/src/components/TrafficViewer/assets/ebpf.png new file mode 100644 index 0000000000000000000000000000000000000000..53424467e781dff1d34cab577096d853db74adad GIT binary patch literal 21879 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 = ({query, style, iconStyle, className, useTool {flipped && addButton} {children} {!flipped && addButton} - {useTooltip && showTooltip && {query}} + {useTooltip && showTooltip && {query}} ); }; diff --git a/ui-common/src/components/UI/StatusBar.tsx b/ui-common/src/components/UI/StatusBar.tsx index 4e6265d08..b7f09cbca 100644 --- a/ui-common/src/components/UI/StatusBar.tsx +++ b/ui-common/src/components/UI/StatusBar.tsx @@ -11,15 +11,14 @@ const pluralize = (noun: string, amount: number) => { } export const StatusBar = () => { - const tappingStatus = useRecoilValue(tappingStatusAtom); const [expandedBar, setExpandedBar] = useState(false); const {uniqueNamespaces, amountOfPods, amountOfTappedPods, amountOfUntappedPods} = useRecoilValue(tappingStatusDetails); - return
setExpandedBar(true)} onMouseLeave={() => setExpandedBar(false)}> + return
setExpandedBar(true)} onMouseLeave={() => setExpandedBar(false)} data-cy="expandedStatusBar">
{tappingStatus.some(pod => !pod.isTapped) && warning} - + {`Tapping ${amountOfUntappedPods > 0 ? amountOfTappedPods + " / " + amountOfPods : amountOfPods} ${pluralize('pod', amountOfPods)} in ${pluralize('namespace', uniqueNamespaces.length)} ${uniqueNamespaces.join(", ")}`}
diff --git a/ui-common/src/hooks/useWS.tsx b/ui-common/src/hooks/useWS.tsx index 49c03b40a..f972cc0ca 100644 --- a/ui-common/src/hooks/useWS.tsx +++ b/ui-common/src/hooks/useWS.tsx @@ -18,7 +18,9 @@ const useWS = (wsUrl: string) => { const onMessage = (e) => { setMessage(e) } const onError = (e) => setError(e) const onOpen = () => { setisOpen(true) } - const onClose = () => setisOpen(false) + const onClose = () => { + setisOpen(false) + } const openSocket = () => { ws.current = new WebSocket(wsUrl) @@ -36,13 +38,17 @@ const useWS = (wsUrl: string) => { ws.current.removeEventListener("close", onClose) } - const sendQuery = (query: string) => { - if (ws.current && (ws.current.readyState === WebSocketReadyState.OPEN)) { - ws.current.send(JSON.stringify({ "query": query, "enableFullEntries": false })); - } + const sendQueryWhenWsOpen = (query) => { + setTimeout(() => { + if (ws?.current?.readyState === WebSocket.OPEN) { + ws.current.send(JSON.stringify({"query": query, "enableFullEntries": false})); + } else { + sendQueryWhenWsOpen(query); + } + }, 500) } - return { message, error, isOpen, openSocket, closeSocket, sendQuery } + return { message, error, isOpen, openSocket, closeSocket, sendQueryWhenWsOpen } } -export default useWS \ No newline at end of file +export default useWS diff --git a/ui/package.json b/ui/package.json index c9e8cc50e..80cf4f7b8 100644 --- a/ui/package.json +++ b/ui/package.json @@ -13,7 +13,7 @@ "@types/jest": "^26.0.22", "@types/node": "^12.20.10", "@uiw/react-textarea-code-editor": "^1.4.12", - "@up9/mizu-common": "1.0.125", + "@up9/mizu-common": "^1.0.128", "axios": "^0.25.0", "core-js": "^3.20.2", "craco-babel-loader": "^1.0.3", diff --git a/ui/src/components/Pages/TrafficPage/TrafficPage.tsx b/ui/src/components/Pages/TrafficPage/TrafficPage.tsx index a881ae37b..60b307aba 100644 --- a/ui/src/components/Pages/TrafficPage/TrafficPage.tsx +++ b/ui/src/components/Pages/TrafficPage/TrafficPage.tsx @@ -1,11 +1,11 @@ -import React, { useEffect } from "react"; +import React, {useEffect} from "react"; import { Button } from "@material-ui/core"; -import Api, {getWebsocketUrl} from "../../../helpers/api"; +import Api,{getWebsocketUrl} from "../../../helpers/api"; import debounce from 'lodash/debounce'; import {useSetRecoilState, useRecoilState} from "recoil"; import {useCommonStyles} from "../../../helpers/commonStyle" import serviceMapModalOpenAtom from "../../../recoil/serviceMapModalOpen"; -import TrafficViewer ,{useWS,DEFAULT_QUERY} from "@up9/mizu-common" +import TrafficViewer from "@up9/mizu-common" import "@up9/mizu-common/dist/index.css" import oasModalOpenAtom from "../../../recoil/oasModalOpen/atom"; import serviceMap from "../../assets/serviceMap.svg"; @@ -22,11 +22,10 @@ export const TrafficPage: React.FC = ({setAnalyzeStatus}) => { const setServiceMapModalOpen = useSetRecoilState(serviceMapModalOpenAtom); const [openOasModal, setOpenOasModal] = useRecoilState(oasModalOpenAtom); - const {message,error,isOpen, openSocket, closeSocket, sendQuery} = useWS(getWebsocketUrl()) - const trafficViewerApi = {...api, webSocket:{open : openSocket, close: closeSocket, sendQuery: sendQuery}} +const trafficViewerApi = {...api} const handleOpenOasModal = () => { - closeSocket() + //closeSocket() -- Todo: Add Close webSocket setOpenOasModal(true); } @@ -56,18 +55,16 @@ export const TrafficPage: React.FC = ({setAnalyzeStatus}) => { }
- sendQuery(DEFAULT_QUERY); - useEffect(() => { return () => { - closeSocket() + //closeSocket() } },[]) return ( <> - ); -}; \ No newline at end of file +};