diff --git a/.gitignore b/.gitignore index 81261416e..bbe9edb9c 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,5 @@ cypress.env.json # Ignore test data in extensions tap/extensions/*/bin tap/extensions/*/expect +traffic-viewer/example +traffic-viewer/dist diff --git a/traffic-viewer/LICENSE b/traffic-viewer/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/traffic-viewer/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/traffic-viewer/README.md b/traffic-viewer/README.md new file mode 100644 index 000000000..09ba10e52 --- /dev/null +++ b/traffic-viewer/README.md @@ -0,0 +1 @@ +# traffic-viewer \ No newline at end of file diff --git a/traffic-viewer/package.json b/traffic-viewer/package.json new file mode 100644 index 000000000..64a4085c0 --- /dev/null +++ b/traffic-viewer/package.json @@ -0,0 +1,55 @@ +{ + "name": "traffic-viewer", + "version": "1.0.0", + "description": "Mizu Traffic Viewer", + "author": "", + "license": "MIT", + "repository": "https://github.com/up9inc/traffic-viewer", + "main": "dist/index.js", + "module": "dist/index.modern.js", + "source": "src/index.js", + "engines": { + "node": ">=10" + }, + "scripts": { + "build": "microbundle-crl --no-compress --format modern,cjs", + "start": "microbundle-crl watch --no-compress --format modern,cjs", + "prepare": "run-s build", + "test": "run-s test:unit test:lint test:build", + "test:build": "run-s build", + "test:lint": "eslint .", + "test:unit": "cross-env CI=1 react-scripts test --env=jsdom", + "test:watch": "react-scripts test --env=jsdom", + "predeploy": "cd example && npm install && npm run build", + "deploy": "gh-pages -d example/build" + }, + "peerDependencies": { + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-scripts": "^3.4.1" + }, + "dependencies":{ + + }, + "devDependencies": { + "microbundle-crl": "^0.13.10", + "babel-eslint": "^10.0.3", + "cross-env": "^7.0.2", + "eslint": "^6.8.0", + "eslint-config-prettier": "^6.7.0", + "eslint-config-standard": "^14.1.0", + "eslint-config-standard-react": "^9.2.0", + "eslint-plugin-import": "^2.18.2", + "eslint-plugin-node": "^11.0.0", + "eslint-plugin-prettier": "^3.1.1", + "eslint-plugin-promise": "^4.2.1", + "eslint-plugin-react": "^7.17.0", + "eslint-plugin-standard": "^4.0.1", + "gh-pages": "^2.2.0", + "npm-run-all": "^4.1.5", + "prettier": "^2.0.4" + }, + "files": [ + "dist" + ] +} diff --git a/traffic-viewer/src/.eslintrc b/traffic-viewer/src/.eslintrc new file mode 100644 index 000000000..55f121d15 --- /dev/null +++ b/traffic-viewer/src/.eslintrc @@ -0,0 +1,5 @@ +{ + "env": { + "jest": true + } +} diff --git a/traffic-viewer/src/assets/downImg.svg b/traffic-viewer/src/assets/downImg.svg new file mode 100644 index 000000000..f37699d88 --- /dev/null +++ b/traffic-viewer/src/assets/downImg.svg @@ -0,0 +1,3 @@ + + + diff --git a/traffic-viewer/src/assets/filter-ui-example-1.png b/traffic-viewer/src/assets/filter-ui-example-1.png new file mode 100644 index 000000000..6f4d88430 Binary files /dev/null and b/traffic-viewer/src/assets/filter-ui-example-1.png differ diff --git a/traffic-viewer/src/assets/filter-ui-example-2.png b/traffic-viewer/src/assets/filter-ui-example-2.png new file mode 100644 index 000000000..e8ae1a5ec Binary files /dev/null and b/traffic-viewer/src/assets/filter-ui-example-2.png differ diff --git a/traffic-viewer/src/components/EntriesList/EntriesList.module.sass b/traffic-viewer/src/components/EntriesList/EntriesList.module.sass new file mode 100644 index 000000000..5295dd656 --- /dev/null +++ b/traffic-viewer/src/components/EntriesList/EntriesList.module.sass @@ -0,0 +1,81 @@ +@import "../../variables.module" + +.list + overflow: scroll + display: flex + flex-grow: 1 + flex-direction: column + justify-content: space-between + position: relative + +.container + position: relative + display: flex + flex-direction: column + overflow: hidden + flex-grow: 1 + +.footer + display: flex + justify-content: space-between + border-top: 1px solid #BCC6DD + align-items: center + padding-top: 10px + margin-right: 15px + +.styledButton + cursor: pointer + line-height: 1 + border-radius: 20px + letter-spacing: .02857em + color: #627ef7 + border: 1px solid rgba(98, 126, 247, 0.5) + padding: 5px 18px + transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,border 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms + font-weight: 600 + +.styledButton:hover + border: 1px solid #627ef7 + background-color: rgba(255, 255, 255, 0.06) + +.spinnerContainer + display: flex + justify-content: center + margin-bottom: 10px + +.noMoreDataAvailable + text-align: center + font-weight: 600 + color: $secondary-font-color + +.btnOld + position: absolute + top: 20px + right: 10px + background: #205CF5 + border-radius: 50% + height: 35px + width: 35px + border: none + cursor: pointer + z-index: 1 + img + height: 10px + transform: scaleY(-1) + +.btnLive + position: absolute + bottom: 10px + right: 10px + background: #205CF5 + border-radius: 50% + height: 35px + width: 35px + border: none + cursor: pointer + img + height: 10px +.hideButton + display: none +.showButton + display: block diff --git a/traffic-viewer/src/components/EntriesList/EntriesList.tsx b/traffic-viewer/src/components/EntriesList/EntriesList.tsx new file mode 100644 index 000000000..e4b1afd59 --- /dev/null +++ b/traffic-viewer/src/components/EntriesList/EntriesList.tsx @@ -0,0 +1,156 @@ +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 down from "../assets/downImg.svg"; +import spinner from '../assets/spinner.svg'; +import Api from "../helpers/api"; +import {useRecoilState, useRecoilValue} from "recoil"; +import entriesAtom from "../recoil/entries"; +import wsConnectionAtom, {WsConnectionStatus} from "../recoil/wsConnection"; +import queryAtom from "../recoil/query"; + +interface EntriesListProps { + listEntryREF: any; + onSnapBrokenEvent: () => void; + isSnappedToBottom: boolean; + setIsSnappedToBottom: any; + queriedCurrent: number; + setQueriedCurrent: any; + queriedTotal: number; + setQueriedTotal: any; + startTime: number; + noMoreDataTop: boolean; + setNoMoreDataTop: (flag: boolean) => void; + leftOffTop: number; + setLeftOffTop: (leftOffTop: number) => void; + ws: any; + openWebSocket: (query: string, resetEntries: boolean) => void; + leftOffBottom: number; + truncatedTimestamp: number; + setTruncatedTimestamp: any; + scrollableRef: any; +} + +const api = Api.getInstance(); + +export const EntriesList: React.FC = ({listEntryREF, onSnapBrokenEvent, isSnappedToBottom, setIsSnappedToBottom, queriedCurrent, setQueriedCurrent, queriedTotal, setQueriedTotal, startTime, noMoreDataTop, setNoMoreDataTop, leftOffTop, setLeftOffTop, ws, openWebSocket, leftOffBottom, truncatedTimestamp, setTruncatedTimestamp, scrollableRef}) => { + + const [entries, setEntries] = useRecoilState(entriesAtom); + const wsConnection = useRecoilValue(wsConnectionAtom); + const query = useRecoilValue(queryAtom); + const isWsConnectionClosed = wsConnection === WsConnectionStatus.Closed; + + const [loadMoreTop, setLoadMoreTop] = useState(false); + const [isLoadingTop, setIsLoadingTop] = useState(false); + + useEffect(() => { + const list = document.getElementById('list').firstElementChild; + list.addEventListener('scroll', (e) => { + const el: any = e.target; + if(el.scrollTop === 0) { + setLoadMoreTop(true); + } else { + setNoMoreDataTop(false); + setLoadMoreTop(false); + } + }); + }, [setLoadMoreTop, setNoMoreDataTop]); + + const memoizedEntries = useMemo(() => { + return entries; + },[entries]); + + const getOldEntries = useCallback(async () => { + setLoadMoreTop(false); + if (leftOffTop === null || leftOffTop <= 0) { + return; + } + setIsLoadingTop(true); + const data = await api.fetchEntries(leftOffTop, -1, query, 100, 3000); + if (!data || data.data === null || data.meta === null) { + setNoMoreDataTop(true); + setIsLoadingTop(false); + return; + } + setLeftOffTop(data.meta.leftOff); + + let scrollTo: boolean; + if (data.meta.leftOff === 0) { + setNoMoreDataTop(true); + scrollTo = false; + } else { + scrollTo = true; + } + setIsLoadingTop(false); + + const newEntries = [...data.data.reverse(), ...entries]; + setEntries(newEntries); + + setQueriedCurrent(queriedCurrent + data.meta.current); + setQueriedTotal(data.meta.total); + setTruncatedTimestamp(data.meta.truncatedTimestamp); + + if (scrollTo) { + scrollableRef.current.scrollToIndex(data.data.length - 1); + } + },[setLoadMoreTop, setIsLoadingTop, entries, setEntries, query, setNoMoreDataTop, leftOffTop, setLeftOffTop, queriedCurrent, setQueriedCurrent, setQueriedTotal, setTruncatedTimestamp, scrollableRef]); + + useEffect(() => { + if(!isWsConnectionClosed || !loadMoreTop || noMoreDataTop) return; + getOldEntries(); + }, [loadMoreTop, noMoreDataTop, getOldEntries, isWsConnectionClosed]); + + const scrollbarVisible = scrollableRef.current?.childWrapperRef.current.clientHeight > scrollableRef.current?.wrapperRef.current.clientHeight; + + return <> +
+
+ {isLoadingTop &&
+ spinner +
} + {noMoreDataTop &&
No more data available
} + + {false /* It's because the first child is ignored by ScrollableFeedVirtualized */} + {memoizedEntries.map(entry => )} + + + +
+ +
+
Displaying {entries?.length} results out of {queriedTotal} total
+ {startTime !== 0 &&
Started listening at {Moment(truncatedTimestamp ? truncatedTimestamp : startTime).utc().format('MM/DD/YYYY, h:mm:ss.SSS A')}
} +
+
+ ; +}; diff --git a/traffic-viewer/src/components/EntryDetailed.tsx b/traffic-viewer/src/components/EntryDetailed.tsx new file mode 100644 index 000000000..ac9106a8c --- /dev/null +++ b/traffic-viewer/src/components/EntryDetailed.tsx @@ -0,0 +1,135 @@ +import React, {useEffect, useState} from "react"; +import EntryViewer from "./EntryDetailed/EntryViewer"; +import {EntryItem} from "./EntryListItem/EntryListItem"; +import {makeStyles} from "@material-ui/core"; +import Protocol from "./UI/Protocol" +import Queryable from "./UI/Queryable"; +import {toast} from "react-toastify"; +import {useRecoilValue} from "recoil"; +import focusedEntryIdAtom from "../recoil/focusedEntryId"; +import Api from "../helpers/api"; + +const useStyles = makeStyles(() => ({ + entryTitle: { + display: 'flex', + minHeight: 20, + maxHeight: 46, + alignItems: 'center', + marginBottom: 4, + marginLeft: 6, + padding: 2, + paddingBottom: 0 + }, + entrySummary: { + display: 'flex', + minHeight: 36, + maxHeight: 46, + alignItems: 'center', + marginBottom: 4, + padding: 5, + paddingBottom: 0 + } +})); + +export const formatSize = (n: number) => n > 1000 ? `${Math.round(n / 1000)}KB` : `${n} B`; + +const EntryTitle: React.FC = ({protocol, data, bodySize, elapsedTime}) => { + const classes = useStyles(); + const response = data.response; + + return
+ +
+ {response && +
+ {formatSize(bodySize)} +
+
} + {response && = ${elapsedTime}`} + style={{marginRight: 18}} + displayIconOnMouseOver={true} + > +
+ {Math.round(elapsedTime)}ms +
+
} +
+
; +}; + +const EntrySummary: React.FC = ({entry}) => { + return ; +}; + +const api = Api.getInstance(); + +export const EntryDetailed = () => { + + const focusedEntryId = useRecoilValue(focusedEntryIdAtom); + const [entryData, setEntryData] = useState(null); + + useEffect(() => { + if (!focusedEntryId) return; + setEntryData(null); + (async () => { + try { + const entryData = await api.getEntry(focusedEntryId); + setEntryData(entryData); + } catch (error) { + if (error.response?.data?.type) { + toast[error.response.data.type](`Entry[${focusedEntryId}]: ${error.response.data.msg}`, { + position: "bottom-right", + theme: "colored", + autoClose: error.response.data.autoClose, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: true, + draggable: true, + progress: undefined, + }); + } + console.error(error); + } + })(); + // eslint-disable-next-line + }, [focusedEntryId]); + + return <> + {entryData && } + {entryData && } + <> + {entryData && } + + +}; diff --git a/traffic-viewer/src/components/EntryDetailed/EntrySections.module.sass b/traffic-viewer/src/components/EntryDetailed/EntrySections.module.sass new file mode 100644 index 000000000..b1b8471f6 --- /dev/null +++ b/traffic-viewer/src/components/EntryDetailed/EntrySections.module.sass @@ -0,0 +1,95 @@ +@import '../../variables.module' + +.title + display: flex + align-items: center + font-weight: 800 + +.button + display: flex + align-content: center + justify-content: space-around + width: .75rem + height: .75rem + border-radius: 4px + font-size: .75rem + line-height: 0.92 + margin-right: .5rem + font-weight: 800 + color: $main-background-color + background-color: $light-blue-color + &.expanded + @extend .button + line-height: .75rem + background-color: $blue-color + +.dataLine + font-weight: 600 + font-size: .75rem + line-height: 1.2 + margin-bottom: -2px + + .dataKey + color: $blue-gray + margin: 0 0.5rem 0 0 + text-align: right + overflow: hidden + text-overflow: ellipsis + width: 1% + max-width: 15rem + + .rulesTitleSuccess + color: #0C0B1A + + .rulesMatchedSuccess + background: #E8FFF1 + padding: 5px + border-radius: 4px + color: #219653 + font-style: normal + font-size: 0.7rem + font-weight: 600 + + .rulesMatchedFailure + background: #FFE9EF + padding: 5px + border-radius: 4px + color: #DB2156 + font-style: normal + font-size: 0.7rem + font-weight: 600 + + .dataValue + color: $blue-gray + margin: 0 + font-weight: normal + > span:first-child + word-break: break-all + max-width: calc(100% - 1.5rem) + > span:nth-child(2) + border-radius: .2rem + background-color: #344073 + display: block + margin-left: .5rem + margin-right: 0 + transition: all .3s + width: 1rem + height: 1rem + &:hover + background-color: #42518f + img + position: relative + width: 100% + +.collapsibleContainer + border-top: 1px solid $light-blue-color + padding: 1rem + background: none + table + width: 100% + tr td:first-child + white-space: nowrap + padding-right: .5rem + +.noRules + padding: 0 1rem 1rem diff --git a/traffic-viewer/src/components/EntryDetailed/EntrySections.tsx b/traffic-viewer/src/components/EntryDetailed/EntrySections.tsx new file mode 100644 index 000000000..b5127fa17 --- /dev/null +++ b/traffic-viewer/src/components/EntryDetailed/EntrySections.tsx @@ -0,0 +1,365 @@ +import styles from "./EntrySections.module.sass"; +import React, {useState} from "react"; +import {SyntaxHighlighter} from "../UI/SyntaxHighlighter/index"; +import CollapsibleContainer from "../UI/CollapsibleContainer"; +import FancyTextDisplay from "../UI/FancyTextDisplay"; +import Queryable from "../UI/Queryable"; +import Checkbox from "../UI/Checkbox"; +import ProtobufDecoder from "protobuf-decoder"; +import {default as jsonBeautify} from "json-beautify"; +import {default as xmlBeautify} from "xml-formatter"; + +interface EntryViewLineProps { + label: string; + value: number | string; + selector?: string; + overrideQueryValue?: string; + displayIconOnMouseOver?: boolean; + useTooltip?: boolean; +} + +const EntryViewLine: React.FC = ({label, value, selector = "", overrideQueryValue = "", displayIconOnMouseOver = true, useTooltip = true}) => { + let query: string; + if (!selector) { + query = ""; + } else if (overrideQueryValue) { + query = `${selector} == ${overrideQueryValue}`; + } else if (typeof(value) == "string") { + query = `${selector} == "${JSON.stringify(value).slice(1, -1)}"`; + } else { + query = `${selector} == ${value}`; + } + return (label && + + + {label} + + + + + + ) || null; +} + + +interface EntrySectionCollapsibleTitleProps { + title: string, + color: string, + expanded: boolean, + setExpanded: any, + query?: string, +} + +const EntrySectionCollapsibleTitle: React.FC = ({title, color, expanded, setExpanded, query = ""}) => { + return
+
{ + setExpanded(!expanded) + }} + > + {expanded ? '-' : '+'} +
+ + {title} + +
+} + +interface EntrySectionContainerProps { + title: string, + color: string, + query?: string, +} + +export const EntrySectionContainer: React.FC = ({title, color, children, query = ""}) => { + const [expanded, setExpanded] = useState(true); + return } + > + {children} + +} + +interface EntryBodySectionProps { + title: string, + content: any, + color: string, + encoding?: string, + contentType?: string, + selector?: string, +} + +export const EntryBodySection: React.FC = ({ + title, + color, + content, + encoding, + contentType, + selector, +}) => { + const MAXIMUM_BYTES_TO_FORMAT = 1000000; // The maximum of chars to highlight in body, in case the response can be megabytes + const jsonLikeFormats = ['json', 'yaml', 'yml']; + const xmlLikeFormats = ['xml', 'html']; + const protobufFormats = ['application/grpc']; + const supportedFormats = jsonLikeFormats.concat(xmlLikeFormats, protobufFormats); + + const [isPretty, setIsPretty] = useState(true); + const [showLineNumbers, setShowLineNumbers] = useState(true); + const [decodeBase64, setDecodeBase64] = useState(true); + + const isBase64Encoding = encoding === 'base64'; + const supportsPrettying = supportedFormats.some(format => contentType?.indexOf(format) > -1); + + const formatTextBody = (body: any): string => { + if (!decodeBase64) return body; + + const chunk = body.slice(0, MAXIMUM_BYTES_TO_FORMAT); + const bodyBuf = isBase64Encoding ? atob(chunk) : chunk; + + try { + if (jsonLikeFormats.some(format => contentType?.indexOf(format) > -1)) { + if (!isPretty) return bodyBuf; + return jsonBeautify(JSON.parse(bodyBuf), null, 2, 80); + } else if (xmlLikeFormats.some(format => contentType?.indexOf(format) > -1)) { + if (!isPretty) return bodyBuf; + return xmlBeautify(bodyBuf, { + indentation: ' ', + filter: (node) => node.type !== 'Comment', + collapseContent: true, + lineSeparator: '\n' + }); + } else if (protobufFormats.some(format => contentType?.indexOf(format) > -1)) { + // Replace all non printable characters (ASCII) + const protobufDecoder = new ProtobufDecoder(bodyBuf, true); + const protobufDecoded = protobufDecoder.decode().toSimple(); + if (!isPretty) return JSON.stringify(protobufDecoded); + return jsonBeautify(protobufDecoded, null, 2, 80); + } + } catch (error) { + console.error(error); + } + return bodyBuf; + } + + return + {content && content?.length > 0 && +
+ {supportsPrettying &&
+ {setIsPretty(!isPretty)}}/> +
} + {supportsPrettying && Pretty} + +
+ {setShowLineNumbers(!showLineNumbers)}}/> +
+ Line numbers + + {isBase64Encoding &&
+ {setDecodeBase64(!decodeBase64)}}/> +
} + {isBase64Encoding && Decode Base64} +
+ + +
} +
+} + +interface EntrySectionProps { + title: string, + color: string, + arrayToIterate: any[], +} + +export const EntryTableSection: React.FC = ({title, color, arrayToIterate}) => { + let arrayToIterateSorted: any[]; + if (arrayToIterate) { + arrayToIterateSorted = arrayToIterate.sort((a, b) => { + if (a.name > b.name) { + return 1; + } + + if (a.name < b.name) { + return -1; + } + + return 0; + }); + } + return + { + arrayToIterate && arrayToIterate.length > 0 ? + + + + {arrayToIterateSorted.map(({name, value, selector}, index) => )} + +
+
: + } +
+} + +interface EntryPolicySectionProps { + title: string, + color: string, + latency?: number, + arrayToIterate: any[], +} + +interface EntryPolicySectionCollapsibleTitleProps { + label: string; + matched: string; + expanded: boolean; + setExpanded: any; +} + +const EntryPolicySectionCollapsibleTitle: React.FC = ({label, matched, expanded, setExpanded}) => { + return
+ { + setExpanded(!expanded) + }} + > + {expanded ? '-' : '+'} + + + + {label} + {matched} + + +
+} + +interface EntryPolicySectionContainerProps { + label: string; + matched: string; + children?: any; +} + +export const EntryPolicySectionContainer: React.FC = ({label, matched, children}) => { + const [expanded, setExpanded] = useState(false); + return } + > + {children} + +} + +export const EntryTablePolicySection: React.FC = ({title, color, latency, arrayToIterate}) => { + return + { + arrayToIterate && arrayToIterate.length > 0 ? + <> + + + + {arrayToIterate.map(({rule, matched}, index) => { + return ( + = latency : true)? "Success" : "Failure"}> + { + <> + { + rule.Key && + + } + { + rule.ResponseTime !== 0 && + + } + { + rule.Method && + + } + { + rule.Path && + + } + { + rule.Service && + + } + { + rule.Type && + + } + { + rule.Value && + + } + + } + + ) + } + ) + } + +
Key: {rule.Key}
Response Time: {rule.ResponseTime}
Method: {rule.Method}
Path: {rule.Path}
Service: {rule.Service}
Type: {rule.Type}
Value: {rule.Value}
+
+ : No rules could be applied to this request. + } +
+} + +interface EntryContractSectionProps { + color: string, + requestReason: string, + responseReason: string, + contractContent: string, +} + +export const EntryContractSection: React.FC = ({color, requestReason, responseReason, contractContent}) => { + return + {requestReason && + {requestReason} + } + {responseReason && + {responseReason} + } + {contractContent && + + } + +} diff --git a/traffic-viewer/src/components/EntryDetailed/EntryViewer.module.sass b/traffic-viewer/src/components/EntryDetailed/EntryViewer.module.sass new file mode 100644 index 000000000..92317867e --- /dev/null +++ b/traffic-viewer/src/components/EntryDetailed/EntryViewer.module.sass @@ -0,0 +1,64 @@ +@import "../../variables.module" + +.Entry + font-family: "Source Sans Pro", Lucida Grande, Tahoma, sans-serif + height: calc(100% - 70px) + width: 100% + margin-top: 10px + + h3, + h4 + font-family: "Source Sans Pro", Lucida Grande, Tahoma, sans-serif + + .header + background-color: rgb(55, 65, 111) + padding: 0.5rem .75rem .65rem .75rem + border-top-left-radius: 0.25rem + border-top-right-radius: 0.25rem + display: flex + font-size: .75rem + align-items: center + .description + min-width: 25rem + display: flex + align-items: center + justify-content: space-between + .method + padding: 0 .25rem + font-size: 0.75rem + font-weight: bold + border-radius: 0.25rem + border: 0.0625rem solid rgba(255, 255, 255, 0.16) + margin-right: .5rem + > span + margin-left: .5rem + .timing + border-left: 1px solid #627ef7 + margin-left: .3rem + padding-left: .3rem + + .headerClickable + cursor: pointer + &:hover + background: lighten(rgb(55, 65, 111), 10%) + border-top-left-radius: 0 + border-top-right-radius: 0 + + .body + height: 100% + overflow-y: auto + background: $main-background-color + color: $blue-gray + border-radius: 4px + padding: 10px + position: relative + .bodyHeader + padding: 0 1rem + .endpointURL + font-size: .75rem + display: block + color: $blue-color + text-decoration: none + margin-bottom: .5rem + overflow-wrap: anywhere + padding: 5px 0 diff --git a/traffic-viewer/src/components/EntryDetailed/EntryViewer.tsx b/traffic-viewer/src/components/EntryDetailed/EntryViewer.tsx new file mode 100644 index 000000000..71cd99cb5 --- /dev/null +++ b/traffic-viewer/src/components/EntryDetailed/EntryViewer.tsx @@ -0,0 +1,129 @@ +import React, {useState} from 'react'; +import styles from './EntryViewer.module.sass'; +import Tabs from "../UI/Tabs"; +import {EntryTableSection, EntryBodySection, EntryTablePolicySection, EntryContractSection} from "./EntrySections"; + +enum SectionTypes { + SectionTable = "table", + SectionBody = "body", +} + +const SectionsRepresentation: React.FC = ({data, color}) => { + const sections = [] + + if (data) { + for (const [i, row] of data.entries()) { + switch (row.type) { + case SectionTypes.SectionTable: + sections.push( + + ) + break; + case SectionTypes.SectionBody: + sections.push( + + ) + break; + default: + break; + } + } + } + + return <>{sections}; +} + +const AutoRepresentation: React.FC = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color}) => { + var TABS = [ + { + tab: 'Request' + } + ]; + const [currentTab, setCurrentTab] = useState(TABS[0].tab); + + // Don't fail even if `representation` is an empty string + if (!representation) { + return <>; + } + + const {request, response} = JSON.parse(representation); + + let responseTabIndex = 0; + let rulesTabIndex = 0; + let contractTabIndex = 0; + + if (response) { + TABS.push( + { + tab: 'Response', + } + ); + responseTabIndex = TABS.length - 1; + } + + if (isRulesEnabled) { + TABS.push( + { + tab: 'Rules', + } + ); + rulesTabIndex = TABS.length - 1; + } + + if (contractStatus !== 0 && contractContent) { + TABS.push( + { + tab: 'Contract', + } + ); + contractTabIndex = TABS.length - 1; + } + + return
+ {
+
+ +
+ {currentTab === TABS[0].tab && + + } + {response && currentTab === TABS[responseTabIndex].tab && + + } + {isRulesEnabled && currentTab === TABS[rulesTabIndex].tab && + + } + {contractStatus !== 0 && contractContent && currentTab === TABS[contractTabIndex].tab && + + } +
} +
; +} + +interface Props { + representation: any; + isRulesEnabled: boolean; + rulesMatched: any; + contractStatus: number; + requestReason: string; + responseReason: string; + contractContent: string; + color: string; + elapsedTime: number; +} + +const EntryViewer: React.FC = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color}) => { + return +}; + +export default EntryViewer; diff --git a/traffic-viewer/src/components/EntryListItem/EntryListItem.module.sass b/traffic-viewer/src/components/EntryListItem/EntryListItem.module.sass new file mode 100644 index 000000000..f74a187e0 --- /dev/null +++ b/traffic-viewer/src/components/EntryListItem/EntryListItem.module.sass @@ -0,0 +1,103 @@ +@import '../../variables.module' + +.row + display: flex + background: $main-background-color + min-height: 46px + max-height: 46px + align-items: center + padding: 0 8px + border-radius: 4px + cursor: pointer + border: solid 1px transparent + margin-right: 10px + &:not(:first-child) + margin-top: 10px + + &:hover + border: solid 1px lighten(#4253a5, 20%) + +.rowSelected + border: 1px $blue-color solid + +.ruleSuccessRow + background: #E8FFF1 + +.ruleSuccessRowSelected + border: 1px #6FCF97 solid + border-left: 5px #6FCF97 solid + +.ruleFailureRow + background: #FFE9EF + +.ruleFailureRowSelected + border: 1px $failure-color solid + border-left: 5px $failure-color solid + +.ruleNumberText + font-size: 12px + font-weight: 600 + white-space: nowrap + +.ruleNumberTextFailure + color: #DB2156 + +.ruleNumberTextSuccess + color: #219653 + +.resolvedName + text-overflow: ellipsis + white-space: nowrap + color: $secondary-font-color + padding-left: 4px + padding-right: 10px + display: flex + font-size: 12px + +.timestamp + font-size: 12px + color: $secondary-font-color + padding-left: 12px + flex-shrink: 0 + width: 185px + text-align: left + +.endpointServiceContainer + display: flex + flex-direction: column + overflow: hidden + padding-right: 10px + padding-top: 4px + flex-grow: 1 + +.separatorRight + display: flex + border-right: 1px solid $data-background-color + padding-right: 12px + +.separatorLeft + display: flex + padding: 4px + padding-left: 12px + +.tcpInfo + font-size: 12px + color: $secondary-font-color + margin-top: 5px + margin-bottom: 5px + +.port + margin-right: 5px + +.ip + margin-left: 5px + +@media (max-width: 1760px) + .timestamp + display: none + .separatorRight + border-right: 0px + +@media (max-width: 1340px) + .separatorRight + display: none diff --git a/traffic-viewer/src/components/EntryListItem/EntryListItem.tsx b/traffic-viewer/src/components/EntryListItem/EntryListItem.tsx new file mode 100644 index 000000000..20f9c1f88 --- /dev/null +++ b/traffic-viewer/src/components/EntryListItem/EntryListItem.tsx @@ -0,0 +1,306 @@ +import React from "react"; +import Moment from 'moment'; +import SwapHorizIcon from '@material-ui/icons/SwapHoriz'; +import styles from './EntryListItem.module.sass'; +import StatusCode, {getClassification, StatusCodeClassification} from "../UI/StatusCode"; +import Protocol, {ProtocolInterface} from "../UI/Protocol" +import {Summary} from "../UI/Summary"; +import Queryable from "../UI/Queryable"; +import ingoingIconSuccess from "../assets/ingoing-traffic-success.svg" +import ingoingIconFailure from "../assets/ingoing-traffic-failure.svg" +import ingoingIconNeutral from "../assets/ingoing-traffic-neutral.svg" +import outgoingIconSuccess from "../assets/outgoing-traffic-success.svg" +import outgoingIconFailure from "../assets/outgoing-traffic-failure.svg" +import outgoingIconNeutral from "../assets/outgoing-traffic-neutral.svg" +import {useRecoilState} from "recoil"; +import focusedEntryIdAtom from "../../recoil/focusedEntryId"; +import queryAtom from "../../recoil/query"; + +interface TCPInterface { + ip: string + port: string + name: string +} + +interface Entry { + proto: ProtocolInterface, + method?: string, + summary: string, + id: number, + status?: number; + timestamp: Date; + src: TCPInterface, + dst: TCPInterface, + isOutgoing?: boolean; + latency: number; + rules: Rules; + contractStatus: number, +} + +interface Rules { + status: boolean; + latency: number; + numberOfRules: number; +} + +interface EntryProps { + entry: Entry; + style: object; + headingMode: boolean; +} + +export const EntryItem: React.FC = ({entry, style, headingMode}) => { + + const [focusedEntryId, setFocusedEntryId] = useRecoilState(focusedEntryIdAtom); + const [queryState, setQuery] = useRecoilState(queryAtom); + const isSelected = focusedEntryId === entry.id.toString(); + + const classification = getClassification(entry.status) + const numberOfRules = entry.rules.numberOfRules + let ingoingIcon; + let outgoingIcon; + switch(classification) { + case StatusCodeClassification.SUCCESS: { + ingoingIcon = ingoingIconSuccess; + outgoingIcon = outgoingIconSuccess; + break; + } + case StatusCodeClassification.FAILURE: { + ingoingIcon = ingoingIconFailure; + outgoingIcon = outgoingIconFailure; + break; + } + case StatusCodeClassification.NEUTRAL: { + ingoingIcon = ingoingIconNeutral; + outgoingIcon = outgoingIconNeutral; + break; + } + } + let additionalRulesProperties = ""; + let ruleSuccess = true; + let rule = 'latency' in entry.rules + if (rule) { + if (entry.rules.latency !== -1) { + if (entry.rules.latency >= entry.latency || !('latency' in entry)) { + additionalRulesProperties = styles.ruleSuccessRow + ruleSuccess = true + } else { + additionalRulesProperties = styles.ruleFailureRow + ruleSuccess = false + } + if (isSelected) { + additionalRulesProperties += ` ${entry.rules.latency >= entry.latency ? styles.ruleSuccessRowSelected : styles.ruleFailureRowSelected}` + } + } else { + if (entry.rules.status) { + additionalRulesProperties = styles.ruleSuccessRow + ruleSuccess = true + } else { + additionalRulesProperties = styles.ruleFailureRow + ruleSuccess = false + } + if (isSelected) { + additionalRulesProperties += ` ${entry.rules.status ? styles.ruleSuccessRowSelected : styles.ruleFailureRowSelected}` + } + } + } + + let contractEnabled = true; + let contractText = ""; + switch (entry.contractStatus) { + case 0: + contractEnabled = false; + break; + case 1: + additionalRulesProperties = styles.ruleSuccessRow + ruleSuccess = true + contractText = "No Breaches" + break; + case 2: + additionalRulesProperties = styles.ruleFailureRow + ruleSuccess = false + contractText = "Breach" + break; + default: + break; + } + + + const isStatusCodeEnabled = ((entry.proto.name === "http" && "status" in entry) || entry.status !== 0); + let endpointServiceContainer = "10px"; + if (!isStatusCodeEnabled) endpointServiceContainer = "20px"; + + return <> +
{ + if (!setFocusedEntryId) return; + setFocusedEntryId(entry.id.toString()); + }} + style={{ + border: isSelected && !headingMode ? `1px ${entry.proto.backgroundColor} solid` : "1px transparent solid", + position: !headingMode ? "absolute" : "unset", + top: style['top'], + marginTop: !headingMode ? style['marginTop'] : "10px", + width: !headingMode ? "calc(100% - 25px)" : "calc(100% - 18px)", + }} + > + {!headingMode ? : null} + {isStatusCodeEnabled &&
+ +
} +
+ +
+ + + {entry.src.name ? entry.src.name : "[Unresolved]"} + + + + + + {entry.dst.name ? entry.dst.name : "[Unresolved]"} + + +
+
+ { + rule ? +
+ {`Rules (${numberOfRules})`} +
+ : "" + } + { + contractEnabled ? +
+ {contractText} +
+ : "" + } +
+ + + {entry.src.ip} + + + {entry.src.port ? ":" : ""} + + + {entry.src.port} + + + {entry.isOutgoing ? + + Ingoing traffic + + : + + Outgoing traffic { + const query = `outgoing == false`; + setQuery(queryState ? `${queryState} and ${query}` : query); + }} + /> + + } + + + {entry.dst.ip} + + + : + + + {entry.dst.port} + + +
+
+ = datetime("${Moment(+entry.timestamp)?.utc().format('MM/DD/YYYY, h:mm:ss.SSS A')}")`} + displayIconOnMouseOver={true} + flipped={false} + > + + {Moment(+entry.timestamp)?.utc().format('MM/DD/YYYY, h:mm:ss.SSS A')} + + +
+
+ + +} diff --git a/traffic-viewer/src/components/Filters/Filters.module.sass b/traffic-viewer/src/components/Filters/Filters.module.sass new file mode 100644 index 000000000..05ca06971 --- /dev/null +++ b/traffic-viewer/src/components/Filters/Filters.module.sass @@ -0,0 +1,44 @@ +@import "../../variables.module" + +.container + display: flex + flex-direction: row + align-items: center + padding: .5rem 0 + border-bottom: 1px solid #BCC6DD + margin-right: 20px + +.filterLabel + color: #8f9bb2 + margin-right: 6px + font-size: 11px + margin-bottom: 4px + +.icon + fill: #627ef7 + +.filterContainer + padding-right: 14px + display: flex + flex-direction: column + +.filterText + input + padding: 4px 12px + background: $main-background-color + border-radius: 4px + font-size: 14px + border: 1px solid #BCC6DD + fieldset + border: none + +$divider-breakpoint-1: 1055px +$divider-breakpoint-2: 1453px + +@media (max-width: $divider-breakpoint-1) + .divider1 + display: none + +@media (max-width: $divider-breakpoint-2) + .divider2 + display: none diff --git a/traffic-viewer/src/components/Filters/Filters.tsx b/traffic-viewer/src/components/Filters/Filters.tsx new file mode 100644 index 000000000..8796bf7e9 --- /dev/null +++ b/traffic-viewer/src/components/Filters/Filters.tsx @@ -0,0 +1,318 @@ +import React, {useRef, useState} from "react"; +import styles from './style/Filters.module.sass'; +import {Button, Grid, Modal, Box, Typography, Backdrop, Fade, Divider} from "@material-ui/core"; +import CodeEditor from '@uiw/react-textarea-code-editor'; +import MenuBookIcon from '@material-ui/icons/MenuBook'; +import {SyntaxHighlighter} from "../UI/SyntaxHighlighter/index"; +import filterUIExample1 from "./assets/filter-ui-example-1.png" +import filterUIExample2 from "./assets/filter-ui-example-2.png" +import variables from '../variables.module.scss'; +import {useRecoilState} from "recoil"; +import queryAtom from "../recoil/query"; +import useKeyPress from "../hooks/useKeyPress" +import shortcutsKeyboard from "../configs/shortcutsKeyboard" + +interface FiltersProps { + backgroundColor: string + ws: any + openWebSocket: (query: string, resetEntries: boolean) => void; +} + +export const Filters: React.FC = ({backgroundColor, ws, openWebSocket}) => { + return
+ +
; +}; + +interface QueryFormProps { + backgroundColor: string + ws: any + openWebSocket: (query: string, resetEntries: boolean) => void; +} + +export const modalStyle = { + position: 'absolute', + top: '10%', + left: '50%', + transform: 'translate(-50%, 0%)', + width: '80vw', + bgcolor: 'background.paper', + borderRadius: '5px', + boxShadow: 24, + outline: "none", + p: 4, + color: '#000', +}; + +export const QueryForm: React.FC = ({backgroundColor, ws, openWebSocket}) => { + + const formRef = useRef(null); + const [query, setQuery] = useRecoilState(queryAtom); + + const [openModal, setOpenModal] = useState(false); + + const handleOpenModal = () => setOpenModal(true); + const handleCloseModal = () => setOpenModal(false); + + const handleChange = async (e) => { + setQuery(e.target.value.trim()); + } + + const handleSubmit = (e) => { + ws.close(); + if (query) { + openWebSocket(`(${query}) and leftOff(-1)`, true); + } else { + openWebSocket(`leftOff(-1)`, true); + } + e.preventDefault(); + } + + useKeyPress(shortcutsKeyboard.ctrlEnter, handleSubmit, formRef.current); + + return <> +
+ + + + + + + + + +
+ + + + + + Filtering Guide (Cheatsheet) + + +

Mizu has a rich filtering syntax that let's you query the results both flexibly and efficiently.

+

Here are some examples that you can try;

+
+ + + + This is a simple query that matches to HTTP packets with request path "/catalogue": + + + + The same query can be negated for HTTP path and written like this: + + + + The syntax supports regular expressions. Here is a query that matches the HTTP requests that send JSON to a server: + + + + Here is another query that matches HTTP responses with status code 4xx: + + + + The same exact query can be as integer comparison: + + = 400`} + language="python" + /> + + The results can be queried based on their timestamps: + + + + + + + Since Mizu supports various protocols like gRPC, AMQP, Kafka and Redis. It's possible to write complex queries that match multiple protocols like this: + + + + By clicking the plus icon that appears beside the queryable UI elements on hovering in both left-pane and right-pane, you can automatically select a field and update the query: + + Clicking to UI elements (left-pane) + + Such that; clicking this icon in left-pane, would append the query below: + + + + Another queriable UI element example, this time from the right-pane: + + Clicking to UI elements (right-pane) + + A query that compares one selector to another is also a valid query: + + + + + + + There are a few helper methods included the in the filter language* to help building queries more easily. + +

+ + true if the given selector's value starts with (similarly endsWith, contains) the string: + + + + a field that contains a JSON encoded string can be filtered based a JSONPath: + + + + fields that contain sensitive information can be redacted: + + + + returns the UNIX timestamp which is the equivalent of the time that's provided by the string. Invalid input evaluates to false: + + = datetime("10/19/2021, 6:29:02.593 PM")`} + language="python" + /> + + limits the number of records that are streamed back as a result of a query. Always evaluates to true: + + +
+
+

+ + *The filtering functionality is provided through Basenine database server. Please refer to BFL Syntax Reference for more information. + +
+
+
+ +} diff --git a/traffic-viewer/src/components/TLSWarning/TLSWarning.sass b/traffic-viewer/src/components/TLSWarning/TLSWarning.sass new file mode 100644 index 000000000..76f77b401 --- /dev/null +++ b/traffic-viewer/src/components/TLSWarning/TLSWarning.sass @@ -0,0 +1,12 @@ +.httpsDomains + display: none + margin: 0 + padding: 0 + list-style: none + +.customWarningStyle + &:hover + overflow-y: scroll + height: 85px + .httpsDomains + display: block diff --git a/traffic-viewer/src/components/TLSWarning/TLSWarning.tsx b/traffic-viewer/src/components/TLSWarning/TLSWarning.tsx new file mode 100644 index 000000000..492e42044 --- /dev/null +++ b/traffic-viewer/src/components/TLSWarning/TLSWarning.tsx @@ -0,0 +1,42 @@ +import {Snackbar} from "@material-ui/core"; +import MuiAlert from "@material-ui/lab/Alert"; +import React, {useEffect} from "react"; +import Api from "../../helpers/api"; +import './TLSWarning.sass'; + +const api = Api.getInstance(); + +interface TLSWarningProps { + showTLSWarning: boolean + setShowTLSWarning: (show: boolean) => void + addressesWithTLS: Set + setAddressesWithTLS: (addresses: Set) => void + userDismissedTLSWarning: boolean + setUserDismissedTLSWarning: (flag: boolean) => void +} + +export const TLSWarning: React.FC = ({showTLSWarning, setShowTLSWarning, addressesWithTLS, setAddressesWithTLS, userDismissedTLSWarning, setUserDismissedTLSWarning}) => { + + useEffect(() => { + (async () => { + try { + const recentTLSLinks = await api.getRecentTLSLinks(); + if (recentTLSLinks?.length > 0) { + setAddressesWithTLS(new Set(recentTLSLinks)); + setShowTLSWarning(true); + } + } catch (e) { + console.error(e); + } + })(); + }, [setShowTLSWarning, setAddressesWithTLS]); + + return ( + setUserDismissedTLSWarning(true)} severity="warning"> + Mizu is detecting TLS traffic, this type of traffic will not be displayed. + {addressesWithTLS.size > 0 && +
    {Array.from(addressesWithTLS, address =>
  • {address}
  • )}
} +
+
); +} diff --git a/traffic-viewer/src/components/TrafficPage.sass b/traffic-viewer/src/components/TrafficPage.sass new file mode 100644 index 000000000..8f0e28c66 --- /dev/null +++ b/traffic-viewer/src/components/TrafficPage.sass @@ -0,0 +1,122 @@ +@import 'src/variables.module' + +.TrafficPage + width: 100% + display: flex + flex-direction: column + overflow: hidden + flex-grow: 1 + height: calc(100vh - 70px) + + .TrafficPageHeader + padding: 20px 24px + display: flex + align-items: center + background-color: $header-background-color + justify-content: space-between + + .TrafficPageStreamStatus + display: flex + align-items: center + +.TrafficPage-Header + display: flex + height: 2.5% + justify-content: space-between + align-items: center + padding: 18px 15px + +.TrafficPage-Header-Image + width: 22px + height: 22px + +.TrafficPage-Header-Text + margin-left: 10px + font-family: 'Source Sans Pro', serif + font-size: 14px + font-weight: bold + color: #f7f9fc + +.TrafficPage-Header-Actions + margin-left: auto + +.TrafficPage-Header-Actions-Image + width: 22px + height: 22px + cursor: pointer + padding-right: 1.2vw + margin-left: auto + transform: translate(0 ,25%) + +.TrafficPage-Viewer + height: 96.5% + overflow: auto + + > iframe + width: 100% + height: 96.5% + display: block + overflow-y: auto + +.TrafficContent + box-sizing: border-box + height: calc(100% - 60px) + overflow: scroll + +.TrafficPage-Container + display: flex + flex-grow: 1 + overflow: hidden + background-color: $data-background-color + +.TrafficPage-ListContainer + display: flex + flex-grow: 1 + overflow: hidden + padding-left: 24px + flex-direction: column + +.TrafficPage-DetailContainer + width: 45vw + background-color: #171c30 + flex: 0 0 50% + padding: 12px 24px + +.indicatorContainer + border-radius: 50% + padding: 2px + margin-left: 10px + +.indicator + height: 8px + width: 8px + border-radius: 50% + +.greenIndicatorContainer + border: 2px #6fcf9770 solid + +.greenIndicator + background-color: #27AE60 + +.orangeIndicatorContainer + border: 2px #fabd5970 solid + +.orangeIndicator + background-color: #ffb530 + +.redIndicatorContainer + border: 2px #ff3a3045 solid + +.redIndicator + background-color: #ff3a30 + +.connectionText + display: flex + align-items: center + height: 17px + font-size: 16px + +.playPauseIcon + cursor: pointer + margin-right: 15px + height: 30px diff --git a/traffic-viewer/src/components/TrafficPage.tsx b/traffic-viewer/src/components/TrafficPage.tsx new file mode 100644 index 000000000..02e3a7e9f --- /dev/null +++ b/traffic-viewer/src/components/TrafficPage.tsx @@ -0,0 +1,311 @@ +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { Filters } from "./Filters/Filters"; +import { EntriesList } from "./EntriesList/EntriesList"; +import { makeStyles } from "@material-ui/core"; +import "./TrafficPage.sass"; +import styles from './EntriesList.module.sass'; +import {EntryDetailed} from "./EntryDetailed"; +import playIcon from '../../assets/run.svg'; +import pauseIcon from '../../assets/pause.svg'; +import variables from '../../../variables.module.scss'; +import debounce from 'lodash/debounce'; +import {useRecoilState, useRecoilValue} from "recoil"; +import tappingStatusAtom from "../../../recoil/tappingStatus"; +import entriesAtom from "../../../recoil/entries"; +import focusedEntryIdAtom from "../../../recoil/focusedEntryId"; +import websocketConnectionAtom, {WsConnectionStatus} from "../../../recoil/wsConnection"; +import queryAtom from "../../../recoil/query"; +import {TLSWarning} from "./TLSWarning/TLSWarning"; +import {StatusBar} from "./UI/StatusBar"; +import Api, {MizuWebsocketURL} from "../../../helpers/api"; +import { toast } from 'react-toastify'; + +const useLayoutStyles = makeStyles(() => ({ + details: { + flex: "0 0 50%", + width: "45vw", + padding: "12px 24px", + borderRadius: 4, + marginTop: 15, + background: variables.headerBackgroundColor, + }, + + viewer: { + display: "flex", + overflowY: "auto", + height: "calc(100% - 70px)", + padding: 5, + paddingBottom: 0, + overflow: "auto", + }, +})); + +interface TrafficPageProps { + setAnalyzeStatus?: (status: any) => void; +} + +const api = Api.getInstance(); + +export const TrafficPage: React.FC = ({setAnalyzeStatus}) => { + const classes = useLayoutStyles(); + const [tappingStatus, setTappingStatus] = useRecoilState(tappingStatusAtom); + const [entries, setEntries] = useRecoilState(entriesAtom); + const [focusedEntryId, setFocusedEntryId] = useRecoilState(focusedEntryIdAtom); + const [wsConnection, setWsConnection] = useRecoilState(websocketConnectionAtom); + const query = useRecoilValue(queryAtom); + + const [noMoreDataTop, setNoMoreDataTop] = useState(false); + const [isSnappedToBottom, setIsSnappedToBottom] = useState(true); + + const [queryBackgroundColor, setQueryBackgroundColor] = useState("#f5f5f5"); + + const [queriedCurrent, setQueriedCurrent] = useState(0); + const [queriedTotal, setQueriedTotal] = useState(0); + const [leftOffBottom, setLeftOffBottom] = useState(0); + const [leftOffTop, setLeftOffTop] = useState(null); + const [truncatedTimestamp, setTruncatedTimestamp] = useState(0); + + const [startTime, setStartTime] = useState(0); + const scrollableRef = useRef(null); + + const [showTLSWarning, setShowTLSWarning] = useState(false); + const [userDismissedTLSWarning, setUserDismissedTLSWarning] = useState(false); + const [addressesWithTLS, setAddressesWithTLS] = useState(new Set()); + + const handleQueryChange = useMemo( + () => + debounce(async (query: string) => { + if (!query) { + setQueryBackgroundColor("#f5f5f5"); + } else { + const data = await api.validateQuery(query); + if (!data) { + return; + } + if (data.valid) { + setQueryBackgroundColor("#d2fad2"); + } else { + setQueryBackgroundColor("#fad6dc"); + } + } + }, 500), + [] + ) as (query: string) => void; + + useEffect(() => { + handleQueryChange(query); + }, [query, handleQueryChange]); + + const ws = useRef(null); + + const listEntry = useRef(null); + const openWebSocket = (query: string, resetEntries: boolean) => { + if (resetEntries) { + setFocusedEntryId(null); + setEntries([]); + setQueriedCurrent(0); + setLeftOffTop(null); + setNoMoreDataTop(false); + } + ws.current = new WebSocket(MizuWebsocketURL); + ws.current.onopen = () => { + setWsConnection(WsConnectionStatus.Connected); + ws.current.send(query); + } + 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); + } + } + } + + 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(() => { + (async () => { + openWebSocket("leftOff(-1)", true); + try{ + const tapStatusResponse = await api.tapStatus(); + setTappingStatus(tapStatusResponse); + if(setAnalyzeStatus) { + const analyzeStatusResponse = await api.analyzeStatus(); + setAnalyzeStatus(analyzeStatusResponse); + } + } catch (error) { + console.error(error); + } + })() + // eslint-disable-next-line + }, []); + + const toggleConnection = () => { + ws.current.close(); + if (wsConnection !== WsConnectionStatus.Connected) { + if (query) { + openWebSocket(`(${query}) and leftOff(-1)`, true); + } else { + openWebSocket(`leftOff(-1)`, true); + } + scrollableRef.current.jumpToBottom(); + setIsSnappedToBottom(true); + } + } + + const onTLSDetected = (destAddress: string) => { + addressesWithTLS.add(destAddress); + setAddressesWithTLS(new Set(addressesWithTLS)); + + if (!userDismissedTLSWarning) { + setShowTLSWarning(true); + } + }; + + const getConnectionStatusClass = (isContainer) => { + const container = isContainer ? "Container" : ""; + switch (wsConnection) { + case WsConnectionStatus.Connected: + return "greenIndicator" + container; + default: + return "redIndicator" + container; + } + } + + const getConnectionTitle = () => { + switch (wsConnection) { + case WsConnectionStatus.Connected: + return "streaming live traffic" + default: + return "streaming paused"; + } + } + + const onSnapBrokenEvent = () => { + setIsSnappedToBottom(false); + if (wsConnection === WsConnectionStatus.Connected) { + ws.current.close(); + } + } + + + return ( +
+
+
+ pause + play +
+ {getConnectionTitle()} +
+
+
+
+
+
+ {
+
+ +
+ +
+
+
+ {focusedEntryId && } +
+
} + {tappingStatus && } + +
+ ); +}; diff --git a/traffic-viewer/src/components/UI/CollapsibleContainer.sass b/traffic-viewer/src/components/UI/CollapsibleContainer.sass new file mode 100644 index 000000000..0a473da8c --- /dev/null +++ b/traffic-viewer/src/components/UI/CollapsibleContainer.sass @@ -0,0 +1,33 @@ +.CollapsibleContainer-Header + display: flex + align-items: center + cursor: pointer + min-height: 40px + +.CollapsibleContainer-Header-Sticky + background: #1f253f + position: sticky + top: 0 + z-index: 1 + +.CollapsibleContainer-Bigger + padding: 10px + font-size: 14px !important + +.CollapsibleContainer-ExpandCollapseButton + margin-left: auto + padding-right: 2% + +.CollapsibleContainer-Expanded + min-height: 40px + +.CollapsibleContainer-Title + font-weight: 600 + font-family: 'Source Sans Pro', sans-serif + font-size: 12px + +.CollapsibleContainer-Expanded .CollapsibleContainer-Title + color: rgba(186, 199, 255, 1) + +.CollapsibleContainer-Collapsed .CollapsibleContainer-Title + color: rgba(186, 199, 255, 0.75) \ No newline at end of file diff --git a/traffic-viewer/src/components/UI/CollapsibleContainer.tsx b/traffic-viewer/src/components/UI/CollapsibleContainer.tsx new file mode 100644 index 000000000..8f77a8cf8 --- /dev/null +++ b/traffic-viewer/src/components/UI/CollapsibleContainer.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import collapsedImg from "../../assets/collapsed.svg"; +import expandedImg from "../../assets/expanded.svg"; +import "./CollapsibleContainer.sass"; + +interface Props { + title: string | React.ReactNode, + expanded: boolean, + titleClassName?: string, + className?: string, + stickyHeader?: boolean, +} + +const CollapsibleContainer: React.FC = ({title, children, expanded, titleClassName, className, stickyHeader = false}) => { + const classNames = `CollapsibleContainer ${expanded ? "CollapsibleContainer-Expanded" : "CollapsibleContainer-Collapsed"} ${className ? className : ''}`; + + // This is needed to achieve the sticky header feature. + // It is needed an un-contained component for the css to work properly. + const content = +
+ { + React.isValidElement(title)? + {title} : + +
{title}
+ Expand/Collapse Button +
+ } +
+ {expanded ? children : null} +
; + + return stickyHeader ? content :
{content}
; +}; + +export default CollapsibleContainer; diff --git a/traffic-viewer/src/components/UI/FancyTextDisplay.sass b/traffic-viewer/src/components/UI/FancyTextDisplay.sass new file mode 100644 index 000000000..c95a3d123 --- /dev/null +++ b/traffic-viewer/src/components/UI/FancyTextDisplay.sass @@ -0,0 +1,41 @@ +.FancyTextDisplay-Container + display: flex + align-items: center + + &.displayIconOnMouseOver + .FancyTextDisplay-Icon + opacity: 0 + pointer-events: none + &:hover + .FancyTextDisplay-Icon + opacity: 1 + pointer-events: all + + + .FancyTextDisplay-Icon + height: 22px + width: 22px + cursor: pointer + margin-right: 3px + + &:hover + background-color: rgba(255, 255, 255, 0.06) + border-radius: 4px + + &.FancyTextDisplay-ContainerEllipsis + .FancyTextDisplay-Text + text-align: left + text-overflow: ellipsis + overflow: hidden + white-space: nowrap + width: calc(100% - 30px) + + .FancyTextDisplay-CopyNotifier + background-color: #4252a5 + padding: 2px 5px + border-radius: 4px + position: absolute + transform: translate(0, -80%) + color: white + z-index: 1000 + diff --git a/traffic-viewer/src/components/UI/FancyTextDisplay.tsx b/traffic-viewer/src/components/UI/FancyTextDisplay.tsx new file mode 100644 index 000000000..29238b804 --- /dev/null +++ b/traffic-viewer/src/components/UI/FancyTextDisplay.tsx @@ -0,0 +1,63 @@ +import React, { useEffect, useState } from 'react'; +import { CopyToClipboard } from 'react-copy-to-clipboard'; +import duplicateImg from "../../assets/duplicate.svg"; +import './FancyTextDisplay.sass'; + +interface Props { + text: string | number, + className?: string, + isPossibleToCopy?: boolean, + applyTextEllipsis?: boolean, + flipped?: boolean, + useTooltip?: boolean, + displayIconOnMouseOver?: boolean, + buttonOnly?: boolean, +} + +const FancyTextDisplay: React.FC = ({text, className, isPossibleToCopy = true, applyTextEllipsis = true, flipped = false, useTooltip= false, displayIconOnMouseOver = false, buttonOnly = false}) => { + const [showCopiedNotification, setCopied] = useState(false); + const [showTooltip, setShowTooltip] = useState(false); + text = String(text); + + const onCopy = () => { + setCopied(true) + }; + + useEffect(() => { + let timer; + if (showCopiedNotification) { + timer = setTimeout(() => { + setCopied(false); + }, 1000); + } + return () => clearTimeout(timer); + }, [showCopiedNotification]); + + const textElement = {text}; + + const copyButton = isPossibleToCopy && text ? + + Duplicate full value + {showCopiedNotification && Copied} + + : null; + + return ( +

setShowTooltip(true)} + onMouseLeave={ e => setShowTooltip(false)} + > + {!buttonOnly && flipped && textElement} + {copyButton} + {!buttonOnly && !flipped && textElement} + {useTooltip && showTooltip && {text}} +

+ ); +}; + +export default FancyTextDisplay; diff --git a/traffic-viewer/src/components/UI/Protocol.module.sass b/traffic-viewer/src/components/UI/Protocol.module.sass new file mode 100644 index 000000000..9ffeaab14 --- /dev/null +++ b/traffic-viewer/src/components/UI/Protocol.module.sass @@ -0,0 +1,24 @@ +.base + display: inline-block + text-align: center + font-size: 10px + font-weight: 600 + background-color: #000 + color: #fff + margin-left: -8px + +.vertical + line-height: 22px + letter-spacing: 0.5px + width: 22px + height: 48px + border-radius: 0px 4px 4px 0 + writing-mode: vertical-lr + transform: rotate(-180deg) + text-orientation: mixed + +.horizontal + border-radius: 4px + font-size: 22px + padding: 5px 10px + font-weight: 600 diff --git a/traffic-viewer/src/components/UI/Protocol.tsx b/traffic-viewer/src/components/UI/Protocol.tsx new file mode 100644 index 000000000..75f153065 --- /dev/null +++ b/traffic-viewer/src/components/UI/Protocol.tsx @@ -0,0 +1,66 @@ +import React from "react"; +import styles from './Protocol.module.sass'; +import Queryable from "./Queryable"; + +export interface ProtocolInterface { + name: string + longName: string + abbr: string + macro: string + backgroundColor: string + foregroundColor: string + fontSize: number + referenceLink: string + ports: string[] + inbound_ports: string +} + +interface ProtocolProps { + protocol: ProtocolInterface + horizontal: boolean +} + +const Protocol: React.FC = ({protocol, horizontal}) => { + if (horizontal) { + return + + + {protocol.longName} + + + + } else { + return + + {protocol.abbr} + + + } +}; + +export default Protocol; diff --git a/traffic-viewer/src/components/UI/Queryable.sass b/traffic-viewer/src/components/UI/Queryable.sass new file mode 100644 index 000000000..f045f2183 --- /dev/null +++ b/traffic-viewer/src/components/UI/Queryable.sass @@ -0,0 +1,48 @@ +.Queryable-Container + display: flex + align-items: center + + &.displayIconOnMouseOver + .Queryable-Icon + opacity: 0 + width: 0px + pointer-events: none + &:hover + .Queryable-Icon + opacity: 1 + pointer-events: all + + + .Queryable-Icon + height: 22px + width: 22px + cursor: pointer + color: #27AE60 + + &:hover + background-color: rgba(255, 255, 255, 0.06) + border-radius: 4px + color: #1E884B + + .Queryable-AddNotifier + background-color: #1E884B + font-weight: normal + padding: 2px 5px + border-radius: 4px + position: absolute + transform: translate(0, 10%) + color: white + z-index: 1000 + font-size: 11px + + .Queryable-Tooltip + background-color: #1E884B + font-weight: normal + padding: 2px 5px + border-radius: 4px + position: absolute + transform: translate(0, -80%) + color: white + z-index: 1000 + font-size: 11px + diff --git a/traffic-viewer/src/components/UI/Queryable.tsx b/traffic-viewer/src/components/UI/Queryable.tsx new file mode 100644 index 000000000..833a45499 --- /dev/null +++ b/traffic-viewer/src/components/UI/Queryable.tsx @@ -0,0 +1,66 @@ +import React, { useEffect, useState } from 'react'; +import { CopyToClipboard } from 'react-copy-to-clipboard'; +import AddCircleIcon from '@material-ui/icons/AddCircle'; +import './Queryable.sass'; +import {useRecoilState} from "recoil"; +import queryAtom from "../../recoil/query"; + +interface Props { + query: string, + style?: object, + iconStyle?: object, + className?: string, + useTooltip?: boolean, + displayIconOnMouseOver?: boolean, + flipped?: boolean, +} + +const Queryable: React.FC = ({query, style, iconStyle, className, useTooltip= true, displayIconOnMouseOver = false, flipped = false, children}) => { + const [showAddedNotification, setAdded] = useState(false); + const [showTooltip, setShowTooltip] = useState(false); + const [queryState, setQuery] = useRecoilState(queryAtom); + + const onCopy = () => { + setAdded(true) + }; + + useEffect(() => { + let timer; + if (showAddedNotification) { + setQuery(queryState ? `${queryState} and ${query}` : query); + timer = setTimeout(() => { + setAdded(false); + }, 1000); + } + return () => clearTimeout(timer); + + // eslint-disable-next-line + }, [showAddedNotification, query, setQuery]); + + const addButton = query ? + + + {showAddedNotification && Added} + + : null; + + return ( +
setShowTooltip(true)} + onMouseLeave={ e => setShowTooltip(false)} + > + {flipped && addButton} + {children} + {!flipped && addButton} + {useTooltip && showTooltip && {query}} +
+ ); +}; + +export default Queryable; diff --git a/traffic-viewer/src/components/UI/StatusBar.sass b/traffic-viewer/src/components/UI/StatusBar.sass new file mode 100644 index 000000000..043a8b2e7 --- /dev/null +++ b/traffic-viewer/src/components/UI/StatusBar.sass @@ -0,0 +1,51 @@ +@import '../../../variables.module' + +.statusBar + position: absolute + transform: translate(-50%, -3px) + left: 50% + z-index: 9999 + min-width: 200px + background: $blue-color + color: rgba(255,255,255,0.75) + border-bottom-left-radius: 8px + border-bottom-right-radius: 8px + top: 0 + padding: 10px + font-size: 14px + transition: max-height 2s ease-out + width: auto + max-height: 32px + overflow: hidden + + .podsCount + display: flex + justify-content: center + font-weight: 600 + + img + margin-right: 10px + height: 22px + + table + width: 100% + margin-top: 20px + + tbody + max-height: 70vh + overflow-y: auto + display: block + tr + display: table + table-layout: fixed + width: 100% + th + text-align: left + padding-right: 5% + td + text-align: left + padding-right: 5% + +.expandedStatusBar + max-height: 100vh + padding-bottom: 15px diff --git a/traffic-viewer/src/components/UI/StatusBar.tsx b/traffic-viewer/src/components/UI/StatusBar.tsx new file mode 100644 index 000000000..4c1fa5fd2 --- /dev/null +++ b/traffic-viewer/src/components/UI/StatusBar.tsx @@ -0,0 +1,42 @@ +import './style/StatusBar.sass'; +import React, {useState} from "react"; +import warningIcon from '../assets/warning_icon.svg'; +import failIcon from '../assets/failed.svg'; +import successIcon from '../assets/success.svg'; +import {useRecoilValue} from "recoil"; +import tappingStatusAtom, {tappingStatusDetails} from "../../recoil/tappingStatus"; + +const pluralize = (noun: string, amount: number) => { + return `${noun}${amount !== 1 ? 's' : ''}` +} + +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)}> +
+ {tappingStatus.some(pod => !pod.isTapped) && warning} + {`Tapping ${amountOfUntappedPods > 0 ? amountOfTappedPods + " / " + amountOfPods : amountOfPods} ${pluralize('pod', amountOfPods)} in ${pluralize('namespace', uniqueNamespaces.length)} ${uniqueNamespaces.join(", ")}`}
+ {expandedBar &&
+ + + + + + + + + + {tappingStatus.map(pod => + + + + )} + +
Pod nameNamespaceTapping
{pod.name}{pod.namespace}status
+
} +
; +} diff --git a/traffic-viewer/src/components/UI/Summary.module.sass b/traffic-viewer/src/components/UI/Summary.module.sass new file mode 100644 index 000000000..2bce9af67 --- /dev/null +++ b/traffic-viewer/src/components/UI/Summary.module.sass @@ -0,0 +1,6 @@ +.container + display: flex + align-items: center + +.summary + white-space: nowrap diff --git a/traffic-viewer/src/components/UI/Summary.tsx b/traffic-viewer/src/components/UI/Summary.tsx new file mode 100644 index 000000000..7358f83c4 --- /dev/null +++ b/traffic-viewer/src/components/UI/Summary.tsx @@ -0,0 +1,39 @@ +import miscStyles from "./misc.module.sass"; +import React from "react"; +import styles from './Summary.module.sass'; +import Queryable from "./Queryable"; + +interface SummaryProps { + method: string + summary: string +} + +export const Summary: React.FC = ({method, summary}) => { + + return
+ {method && + + {method} + + } + {summary && +
+ {summary} +
+
} +
+}; diff --git a/traffic-viewer/src/components/UI/SyntaxHighlighter/index.scss b/traffic-viewer/src/components/UI/SyntaxHighlighter/index.scss new file mode 100644 index 000000000..d735e6fe6 --- /dev/null +++ b/traffic-viewer/src/components/UI/SyntaxHighlighter/index.scss @@ -0,0 +1,49 @@ +.highlighterContainer { + &.fitScreen { + pre { + max-height: 90vh; + overflow: auto; + } + } + + pre { + code { + font-size: 0.75rem; + + &:first-child { + margin-right: 0.75rem; + background: #F7F9FC; + + .react-syntax-highlighter-line-number { + color: rgb(98, 126, 247); + } + } + + &:last-child { + display: block; + } + } + } +} + +code.hljs { + white-space: pre-wrap; +} + +code.hljs:before { + counter-reset: listing; +} + +code.hljs .hljs-marker-line { + counter-increment: listing; +} + +code.hljs .hljs-marker-line:before { + content: counter(listing) " "; + display: inline-block; + width: 3rem; + padding-left: auto; + margin-left: auto; + text-align: right; + opacity: .5; +} diff --git a/traffic-viewer/src/components/UI/SyntaxHighlighter/index.tsx b/traffic-viewer/src/components/UI/SyntaxHighlighter/index.tsx new file mode 100644 index 000000000..313971bbf --- /dev/null +++ b/traffic-viewer/src/components/UI/SyntaxHighlighter/index.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import Lowlight from 'react-lowlight' +import 'highlight.js/styles/atom-one-light.css' +import './index.scss'; + +import xml from 'highlight.js/lib/languages/xml' +import json from 'highlight.js/lib/languages/json' +import protobuf from 'highlight.js/lib/languages/protobuf' +import javascript from 'highlight.js/lib/languages/javascript' +import actionscript from 'highlight.js/lib/languages/actionscript' +import wasm from 'highlight.js/lib/languages/wasm' +import handlebars from 'highlight.js/lib/languages/handlebars' +import yaml from 'highlight.js/lib/languages/yaml' +import python from 'highlight.js/lib/languages/python' + +Lowlight.registerLanguage('python', python); +Lowlight.registerLanguage('xml', xml); +Lowlight.registerLanguage('json', json); +Lowlight.registerLanguage('yaml', yaml); +Lowlight.registerLanguage('protobuf', protobuf); +Lowlight.registerLanguage('javascript', javascript); +Lowlight.registerLanguage('actionscript', actionscript); +Lowlight.registerLanguage('wasm', wasm); +Lowlight.registerLanguage('handlebars', handlebars); + +interface Props { + code: string; + showLineNumbers?: boolean; + language?: string; +} + +export const SyntaxHighlighter: React.FC = ({ + code, + showLineNumbers = false, + language = null + }) => { + const markers = showLineNumbers ? code.split("\n").map((item, i) => { + return { + line: i + 1, + className: 'hljs-marker-line' + } + }) : []; + + return
; +}; + +export default SyntaxHighlighter; diff --git a/traffic-viewer/src/components/UI/misc.module.sass b/traffic-viewer/src/components/UI/misc.module.sass new file mode 100644 index 000000000..bef317cd3 --- /dev/null +++ b/traffic-viewer/src/components/UI/misc.module.sass @@ -0,0 +1,27 @@ +@import '../../../variables.module' + +.protocol + border-radius: 4px + border: solid 1px $secondary-font-color + margin-left: 4px + padding: 2px 5px + font-family: "Source Sans Pro", sans-serif + font-size: 11px + font-weight: bold + + &.method + margin-right: 10px + height: 12px + + &.filterPlate + border-color: #bcc6dd20 + color: #a0b2ff + font-size: 10px + +.noSelect + -webkit-touch-callout: none + -webkit-user-select: none + -khtml-user-select: none + -moz-user-select: none + -ms-user-select: none + user-select: none diff --git a/traffic-viewer/src/global.d.ts b/traffic-viewer/src/global.d.ts new file mode 100644 index 000000000..c003ba87b --- /dev/null +++ b/traffic-viewer/src/global.d.ts @@ -0,0 +1,8 @@ +declare module "*.svg" { + const content: any; + export default content; + } + + declare module '*.scss'; + declare module '*.sass'; + declare module '*.png'; \ No newline at end of file diff --git a/traffic-viewer/src/index.js b/traffic-viewer/src/index.js new file mode 100644 index 000000000..f0c8267af --- /dev/null +++ b/traffic-viewer/src/index.js @@ -0,0 +1,6 @@ +import React from 'react' +import { TrafficPage } from './components/TrafficPage' + +export const TrafficViewer = ({ text }) => { + return +} diff --git a/traffic-viewer/src/index.test.js b/traffic-viewer/src/index.test.js new file mode 100644 index 000000000..a0f0449b5 --- /dev/null +++ b/traffic-viewer/src/index.test.js @@ -0,0 +1,7 @@ +import { ExampleComponent } from '.' + +describe('ExampleComponent', () => { + it('is truthy', () => { + expect(ExampleComponent).toBeTruthy() + }) +}) diff --git a/traffic-viewer/tsconfig.json b/traffic-viewer/tsconfig.json new file mode 100644 index 000000000..4c74c1129 --- /dev/null +++ b/traffic-viewer/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "outDir": "lib/esm", + "module": "esnext", + "target": "es5", + "lib": ["es6", "dom", "es2016", "es2017"], + "jsx": "react-jsx", + "declaration": true, + "moduleResolution": "node", + "noUnusedLocals": true, + "noUnusedParameters": true, + "esModuleInterop": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": true, + "strictNullChecks": true, + "suppressImplicitAnyIndexErrors": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src/**/*.ts*","src/custom.d.ts"], + "exclude": ["node_modules", "lib"] +} \ No newline at end of file