mirror of
https://github.com/kubeshark/kubeshark.git
synced 2025-09-26 13:04:13 +00:00
first commit
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -48,3 +48,5 @@ cypress.env.json
|
|||||||
# Ignore test data in extensions
|
# Ignore test data in extensions
|
||||||
tap/extensions/*/bin
|
tap/extensions/*/bin
|
||||||
tap/extensions/*/expect
|
tap/extensions/*/expect
|
||||||
|
traffic-viewer/example
|
||||||
|
traffic-viewer/dist
|
||||||
|
201
traffic-viewer/LICENSE
Normal file
201
traffic-viewer/LICENSE
Normal file
@@ -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.
|
1
traffic-viewer/README.md
Normal file
1
traffic-viewer/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# traffic-viewer
|
55
traffic-viewer/package.json
Normal file
55
traffic-viewer/package.json
Normal file
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
5
traffic-viewer/src/.eslintrc
Normal file
5
traffic-viewer/src/.eslintrc
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"jest": true
|
||||||
|
}
|
||||||
|
}
|
3
traffic-viewer/src/assets/downImg.svg
Normal file
3
traffic-viewer/src/assets/downImg.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 2.82846L7.82843 3.6478e-05L9.24264 1.41425L5 5.65689L4.99997 5.65686L3.58579 4.24268L0.75733 1.41422L2.17154 5.00679e-06L5 2.82846Z" fill="white"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 301 B |
BIN
traffic-viewer/src/assets/filter-ui-example-1.png
Normal file
BIN
traffic-viewer/src/assets/filter-ui-example-1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
BIN
traffic-viewer/src/assets/filter-ui-example-2.png
Normal file
BIN
traffic-viewer/src/assets/filter-ui-example-2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
@@ -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
|
156
traffic-viewer/src/components/EntriesList/EntriesList.tsx
Normal file
156
traffic-viewer/src/components/EntriesList/EntriesList.tsx
Normal file
@@ -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<EntriesListProps> = ({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 <>
|
||||||
|
<div className={styles.list}>
|
||||||
|
<div id="list" ref={listEntryREF} className={styles.list}>
|
||||||
|
{isLoadingTop && <div className={styles.spinnerContainer}>
|
||||||
|
<img alt="spinner" src={spinner} style={{height: 25}}/>
|
||||||
|
</div>}
|
||||||
|
{noMoreDataTop && <div id="noMoreDataTop" className={styles.noMoreDataAvailable}>No more data available</div>}
|
||||||
|
<ScrollableFeedVirtualized ref={scrollableRef} itemHeight={48} marginTop={10} onSnapBroken={onSnapBrokenEvent}>
|
||||||
|
{false /* It's because the first child is ignored by ScrollableFeedVirtualized */}
|
||||||
|
{memoizedEntries.map(entry => <EntryItem
|
||||||
|
key={`entry-${entry.id}`}
|
||||||
|
entry={entry}
|
||||||
|
style={{}}
|
||||||
|
headingMode={false}
|
||||||
|
/>)}
|
||||||
|
</ScrollableFeedVirtualized>
|
||||||
|
<button type="button"
|
||||||
|
title="Fetch old records"
|
||||||
|
className={`${styles.btnOld} ${!scrollbarVisible && leftOffTop > 0 ? styles.showButton : styles.hideButton}`}
|
||||||
|
onClick={(_) => {
|
||||||
|
ws.close();
|
||||||
|
getOldEntries();
|
||||||
|
}}>
|
||||||
|
<img alt="down" src={down} />
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
title="Snap to bottom"
|
||||||
|
className={`${styles.btnLive} ${isSnappedToBottom && !isWsConnectionClosed ? styles.hideButton : styles.showButton}`}
|
||||||
|
onClick={(_) => {
|
||||||
|
if (isWsConnectionClosed) {
|
||||||
|
if (query) {
|
||||||
|
openWebSocket(`(${query}) and leftOff(${leftOffBottom})`, false);
|
||||||
|
} else {
|
||||||
|
openWebSocket(`leftOff(${leftOffBottom})`, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scrollableRef.current.jumpToBottom();
|
||||||
|
setIsSnappedToBottom(true);
|
||||||
|
}}>
|
||||||
|
<img alt="down" src={down} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.footer}>
|
||||||
|
<div>Displaying <b id="entries-length">{entries?.length}</b> results out of <b id="total-entries">{queriedTotal}</b> total</div>
|
||||||
|
{startTime !== 0 && <div>Started listening at <span style={{marginRight: 5, fontWeight: 600, fontSize: 13}}>{Moment(truncatedTimestamp ? truncatedTimestamp : startTime).utc().format('MM/DD/YYYY, h:mm:ss.SSS A')}</span></div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>;
|
||||||
|
};
|
135
traffic-viewer/src/components/EntryDetailed.tsx
Normal file
135
traffic-viewer/src/components/EntryDetailed.tsx
Normal file
@@ -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<any> = ({protocol, data, bodySize, elapsedTime}) => {
|
||||||
|
const classes = useStyles();
|
||||||
|
const response = data.response;
|
||||||
|
|
||||||
|
return <div className={classes.entryTitle}>
|
||||||
|
<Protocol protocol={protocol} horizontal={true}/>
|
||||||
|
<div style={{right: "30px", position: "absolute", display: "flex"}}>
|
||||||
|
{response && <Queryable
|
||||||
|
query={`response.bodySize == ${bodySize}`}
|
||||||
|
style={{margin: "0 18px"}}
|
||||||
|
displayIconOnMouseOver={true}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{opacity: 0.5}}
|
||||||
|
id="entryDetailedTitleBodySize"
|
||||||
|
>
|
||||||
|
{formatSize(bodySize)}
|
||||||
|
</div>
|
||||||
|
</Queryable>}
|
||||||
|
{response && <Queryable
|
||||||
|
query={`elapsedTime >= ${elapsedTime}`}
|
||||||
|
style={{marginRight: 18}}
|
||||||
|
displayIconOnMouseOver={true}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{opacity: 0.5}}
|
||||||
|
id="entryDetailedTitleElapsedTime"
|
||||||
|
>
|
||||||
|
{Math.round(elapsedTime)}ms
|
||||||
|
</div>
|
||||||
|
</Queryable>}
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EntrySummary: React.FC<any> = ({entry}) => {
|
||||||
|
return <EntryItem
|
||||||
|
key={`entry-${entry.id}`}
|
||||||
|
entry={entry}
|
||||||
|
style={{}}
|
||||||
|
headingMode={true}
|
||||||
|
/>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 && <EntryTitle
|
||||||
|
protocol={entryData.protocol}
|
||||||
|
data={entryData.data}
|
||||||
|
bodySize={entryData.bodySize}
|
||||||
|
elapsedTime={entryData.data.elapsedTime}
|
||||||
|
/>}
|
||||||
|
{entryData && <EntrySummary entry={entryData.data}/>}
|
||||||
|
<>
|
||||||
|
{entryData && <EntryViewer
|
||||||
|
representation={entryData.representation}
|
||||||
|
isRulesEnabled={entryData.isRulesEnabled}
|
||||||
|
rulesMatched={entryData.rulesMatched}
|
||||||
|
contractStatus={entryData.data.contractStatus}
|
||||||
|
requestReason={entryData.data.contractRequestReason}
|
||||||
|
responseReason={entryData.data.contractResponseReason}
|
||||||
|
contractContent={entryData.data.contractContent}
|
||||||
|
elapsedTime={entryData.data.elapsedTime}
|
||||||
|
color={entryData.protocol.backgroundColor}
|
||||||
|
/>}
|
||||||
|
</>
|
||||||
|
</>
|
||||||
|
};
|
@@ -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
|
365
traffic-viewer/src/components/EntryDetailed/EntrySections.tsx
Normal file
365
traffic-viewer/src/components/EntryDetailed/EntrySections.tsx
Normal file
@@ -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<EntryViewLineProps> = ({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 && <tr className={styles.dataLine}>
|
||||||
|
<td className={`${styles.dataKey}`}>
|
||||||
|
<Queryable
|
||||||
|
query={query}
|
||||||
|
style={{float: "right", height: "18px"}}
|
||||||
|
iconStyle={{marginRight: "20px"}}
|
||||||
|
flipped={true}
|
||||||
|
useTooltip={useTooltip}
|
||||||
|
displayIconOnMouseOver={displayIconOnMouseOver}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Queryable>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<FancyTextDisplay
|
||||||
|
className={styles.dataValue}
|
||||||
|
text={value}
|
||||||
|
applyTextEllipsis={false}
|
||||||
|
flipped={true}
|
||||||
|
displayIconOnMouseOver={true}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface EntrySectionCollapsibleTitleProps {
|
||||||
|
title: string,
|
||||||
|
color: string,
|
||||||
|
expanded: boolean,
|
||||||
|
setExpanded: any,
|
||||||
|
query?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const EntrySectionCollapsibleTitle: React.FC<EntrySectionCollapsibleTitleProps> = ({title, color, expanded, setExpanded, query = ""}) => {
|
||||||
|
return <div className={styles.title}>
|
||||||
|
<div
|
||||||
|
className={`${styles.button} ${expanded ? styles.expanded : ''}`}
|
||||||
|
style={{backgroundColor: color}}
|
||||||
|
onClick={() => {
|
||||||
|
setExpanded(!expanded)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{expanded ? '-' : '+'}
|
||||||
|
</div>
|
||||||
|
<Queryable
|
||||||
|
query={query}
|
||||||
|
useTooltip={!!query}
|
||||||
|
displayIconOnMouseOver={!!query}
|
||||||
|
>
|
||||||
|
<span>{title}</span>
|
||||||
|
</Queryable>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EntrySectionContainerProps {
|
||||||
|
title: string,
|
||||||
|
color: string,
|
||||||
|
query?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EntrySectionContainer: React.FC<EntrySectionContainerProps> = ({title, color, children, query = ""}) => {
|
||||||
|
const [expanded, setExpanded] = useState(true);
|
||||||
|
return <CollapsibleContainer
|
||||||
|
className={styles.collapsibleContainer}
|
||||||
|
expanded={expanded}
|
||||||
|
title={<EntrySectionCollapsibleTitle title={title} color={color} expanded={expanded} setExpanded={setExpanded} query={query}/>}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</CollapsibleContainer>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EntryBodySectionProps {
|
||||||
|
title: string,
|
||||||
|
content: any,
|
||||||
|
color: string,
|
||||||
|
encoding?: string,
|
||||||
|
contentType?: string,
|
||||||
|
selector?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EntryBodySection: React.FC<EntryBodySectionProps> = ({
|
||||||
|
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 <React.Fragment>
|
||||||
|
{content && content?.length > 0 && <EntrySectionContainer
|
||||||
|
title={title}
|
||||||
|
color={color}
|
||||||
|
query={`${selector} == r".*"`}
|
||||||
|
>
|
||||||
|
<div style={{display: 'flex', alignItems: 'center', alignContent: 'center', margin: "5px 0"}}>
|
||||||
|
{supportsPrettying && <div style={{paddingTop: 3}}>
|
||||||
|
<Checkbox checked={isPretty} onToggle={() => {setIsPretty(!isPretty)}}/>
|
||||||
|
</div>}
|
||||||
|
{supportsPrettying && <span style={{marginLeft: '.2rem'}}>Pretty</span>}
|
||||||
|
|
||||||
|
<div style={{paddingTop: 3, paddingLeft: supportsPrettying ? 20 : 0}}>
|
||||||
|
<Checkbox checked={showLineNumbers} onToggle={() => {setShowLineNumbers(!showLineNumbers)}}/>
|
||||||
|
</div>
|
||||||
|
<span style={{marginLeft: '.2rem'}}>Line numbers</span>
|
||||||
|
|
||||||
|
{isBase64Encoding && <div style={{paddingTop: 3, paddingLeft: 20}}>
|
||||||
|
<Checkbox checked={decodeBase64} onToggle={() => {setDecodeBase64(!decodeBase64)}}/>
|
||||||
|
</div>}
|
||||||
|
{isBase64Encoding && <span style={{marginLeft: '.2rem'}}>Decode Base64</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SyntaxHighlighter
|
||||||
|
code={formatTextBody(content)}
|
||||||
|
showLineNumbers={showLineNumbers}
|
||||||
|
/>
|
||||||
|
</EntrySectionContainer>}
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EntrySectionProps {
|
||||||
|
title: string,
|
||||||
|
color: string,
|
||||||
|
arrayToIterate: any[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EntryTableSection: React.FC<EntrySectionProps> = ({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 <React.Fragment>
|
||||||
|
{
|
||||||
|
arrayToIterate && arrayToIterate.length > 0 ?
|
||||||
|
<EntrySectionContainer title={title} color={color}>
|
||||||
|
<table>
|
||||||
|
<tbody id={`tbody-${title}`}>
|
||||||
|
{arrayToIterateSorted.map(({name, value, selector}, index) => <EntryViewLine
|
||||||
|
key={index}
|
||||||
|
label={name}
|
||||||
|
value={value}
|
||||||
|
selector={selector}
|
||||||
|
/>)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</EntrySectionContainer> : <span/>
|
||||||
|
}
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EntryPolicySectionProps {
|
||||||
|
title: string,
|
||||||
|
color: string,
|
||||||
|
latency?: number,
|
||||||
|
arrayToIterate: any[],
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EntryPolicySectionCollapsibleTitleProps {
|
||||||
|
label: string;
|
||||||
|
matched: string;
|
||||||
|
expanded: boolean;
|
||||||
|
setExpanded: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EntryPolicySectionCollapsibleTitle: React.FC<EntryPolicySectionCollapsibleTitleProps> = ({label, matched, expanded, setExpanded}) => {
|
||||||
|
return <div className={styles.title}>
|
||||||
|
<span
|
||||||
|
className={`${styles.button}
|
||||||
|
${expanded ? styles.expanded : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
setExpanded(!expanded)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{expanded ? '-' : '+'}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<tr className={styles.dataLine}>
|
||||||
|
<td className={`${styles.dataKey} ${styles.rulesTitleSuccess}`}>{label}</td>
|
||||||
|
<td className={`${styles.dataKey} ${matched === 'Success' ? styles.rulesMatchedSuccess : styles.rulesMatchedFailure}`}>{matched}</td>
|
||||||
|
</tr>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EntryPolicySectionContainerProps {
|
||||||
|
label: string;
|
||||||
|
matched: string;
|
||||||
|
children?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EntryPolicySectionContainer: React.FC<EntryPolicySectionContainerProps> = ({label, matched, children}) => {
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
return <CollapsibleContainer
|
||||||
|
className={styles.collapsibleContainer}
|
||||||
|
expanded={expanded}
|
||||||
|
title={<EntryPolicySectionCollapsibleTitle label={label} matched={matched} expanded={expanded} setExpanded={setExpanded}/>}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</CollapsibleContainer>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EntryTablePolicySection: React.FC<EntryPolicySectionProps> = ({title, color, latency, arrayToIterate}) => {
|
||||||
|
return <React.Fragment>
|
||||||
|
{
|
||||||
|
arrayToIterate && arrayToIterate.length > 0 ?
|
||||||
|
<>
|
||||||
|
<EntrySectionContainer title={title} color={color}>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{arrayToIterate.map(({rule, matched}, index) => {
|
||||||
|
return (
|
||||||
|
<EntryPolicySectionContainer key={index} label={rule.Name} matched={matched && (rule.Type === 'slo' ? rule.ResponseTime >= latency : true)? "Success" : "Failure"}>
|
||||||
|
{
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
rule.Key &&
|
||||||
|
<tr className={styles.dataValue}><td><b>Key:</b></td> <td>{rule.Key}</td></tr>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
rule.ResponseTime !== 0 &&
|
||||||
|
<tr className={styles.dataValue}><td><b>Response Time:</b></td> <td>{rule.ResponseTime}</td></tr>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
rule.Method &&
|
||||||
|
<tr className={styles.dataValue}><td><b>Method:</b></td> <td>{rule.Method}</td></tr>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
rule.Path &&
|
||||||
|
<tr className={styles.dataValue}><td><b>Path:</b></td> <td>{rule.Path}</td></tr>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
rule.Service &&
|
||||||
|
<tr className={styles.dataValue}><td><b>Service:</b></td> <td>{rule.Service}</td></tr>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
rule.Type &&
|
||||||
|
<tr className={styles.dataValue}><td><b>Type:</b></td> <td>{rule.Type}</td></tr>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
rule.Value &&
|
||||||
|
<tr className={styles.dataValue}><td><b>Value:</b></td> <td>{rule.Value}</td></tr>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</EntryPolicySectionContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</EntrySectionContainer>
|
||||||
|
</> : <span className={styles.noRules}>No rules could be applied to this request.</span>
|
||||||
|
}
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EntryContractSectionProps {
|
||||||
|
color: string,
|
||||||
|
requestReason: string,
|
||||||
|
responseReason: string,
|
||||||
|
contractContent: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EntryContractSection: React.FC<EntryContractSectionProps> = ({color, requestReason, responseReason, contractContent}) => {
|
||||||
|
return <React.Fragment>
|
||||||
|
{requestReason && <EntrySectionContainer title="Request" color={color}>
|
||||||
|
{requestReason}
|
||||||
|
</EntrySectionContainer>}
|
||||||
|
{responseReason && <EntrySectionContainer title="Response" color={color}>
|
||||||
|
{responseReason}
|
||||||
|
</EntrySectionContainer>}
|
||||||
|
{contractContent && <EntrySectionContainer title="Contract" color={color}>
|
||||||
|
<SyntaxHighlighter
|
||||||
|
code={contractContent}
|
||||||
|
language={"yaml"}
|
||||||
|
/>
|
||||||
|
</EntrySectionContainer>}
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
@@ -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
|
129
traffic-viewer/src/components/EntryDetailed/EntryViewer.tsx
Normal file
129
traffic-viewer/src/components/EntryDetailed/EntryViewer.tsx
Normal file
@@ -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<any> = ({data, color}) => {
|
||||||
|
const sections = []
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
for (const [i, row] of data.entries()) {
|
||||||
|
switch (row.type) {
|
||||||
|
case SectionTypes.SectionTable:
|
||||||
|
sections.push(
|
||||||
|
<EntryTableSection key={i} title={row.title} color={color} arrayToIterate={JSON.parse(row.data)}/>
|
||||||
|
)
|
||||||
|
break;
|
||||||
|
case SectionTypes.SectionBody:
|
||||||
|
sections.push(
|
||||||
|
<EntryBodySection key={i} title={row.title} color={color} content={row.data} encoding={row.encoding} contentType={row.mimeType} selector={row.selector}/>
|
||||||
|
)
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{sections}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AutoRepresentation: React.FC<any> = ({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 <div className={styles.Entry}>
|
||||||
|
{<div className={styles.body}>
|
||||||
|
<div className={styles.bodyHeader}>
|
||||||
|
<Tabs tabs={TABS} currentTab={currentTab} color={color} onChange={setCurrentTab} leftAligned/>
|
||||||
|
</div>
|
||||||
|
{currentTab === TABS[0].tab && <React.Fragment>
|
||||||
|
<SectionsRepresentation data={request} color={color}/>
|
||||||
|
</React.Fragment>}
|
||||||
|
{response && currentTab === TABS[responseTabIndex].tab && <React.Fragment>
|
||||||
|
<SectionsRepresentation data={response} color={color}/>
|
||||||
|
</React.Fragment>}
|
||||||
|
{isRulesEnabled && currentTab === TABS[rulesTabIndex].tab && <React.Fragment>
|
||||||
|
<EntryTablePolicySection title={'Rule'} color={color} latency={elapsedTime} arrayToIterate={rulesMatched ? rulesMatched : []}/>
|
||||||
|
</React.Fragment>}
|
||||||
|
{contractStatus !== 0 && contractContent && currentTab === TABS[contractTabIndex].tab && <React.Fragment>
|
||||||
|
<EntryContractSection color={color} requestReason={requestReason} responseReason={responseReason} contractContent={contractContent}/>
|
||||||
|
</React.Fragment>}
|
||||||
|
</div>}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
representation: any;
|
||||||
|
isRulesEnabled: boolean;
|
||||||
|
rulesMatched: any;
|
||||||
|
contractStatus: number;
|
||||||
|
requestReason: string;
|
||||||
|
responseReason: string;
|
||||||
|
contractContent: string;
|
||||||
|
color: string;
|
||||||
|
elapsedTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EntryViewer: React.FC<Props> = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color}) => {
|
||||||
|
return <AutoRepresentation
|
||||||
|
representation={representation}
|
||||||
|
isRulesEnabled={isRulesEnabled}
|
||||||
|
rulesMatched={rulesMatched}
|
||||||
|
contractStatus={contractStatus}
|
||||||
|
requestReason={requestReason}
|
||||||
|
responseReason={responseReason}
|
||||||
|
contractContent={contractContent}
|
||||||
|
elapsedTime={elapsedTime}
|
||||||
|
color={color}
|
||||||
|
/>
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EntryViewer;
|
@@ -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
|
306
traffic-viewer/src/components/EntryListItem/EntryListItem.tsx
Normal file
306
traffic-viewer/src/components/EntryListItem/EntryListItem.tsx
Normal file
@@ -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<EntryProps> = ({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 <>
|
||||||
|
<div
|
||||||
|
id={`entry-${entry.id.toString()}`}
|
||||||
|
className={`${styles.row}
|
||||||
|
${isSelected && !rule && !contractEnabled ? styles.rowSelected : additionalRulesProperties}`}
|
||||||
|
onClick={() => {
|
||||||
|
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 ? <Protocol
|
||||||
|
protocol={entry.proto}
|
||||||
|
horizontal={false}
|
||||||
|
/> : null}
|
||||||
|
{isStatusCodeEnabled && <div>
|
||||||
|
<StatusCode statusCode={entry.status}/>
|
||||||
|
</div>}
|
||||||
|
<div className={styles.endpointServiceContainer} style={{paddingLeft: endpointServiceContainer}}>
|
||||||
|
<Summary method={entry.method} summary={entry.summary}/>
|
||||||
|
<div className={styles.resolvedName}>
|
||||||
|
<Queryable
|
||||||
|
query={`src.name == "${entry.src.name}"`}
|
||||||
|
displayIconOnMouseOver={true}
|
||||||
|
flipped={true}
|
||||||
|
style={{marginTop: "-4px", overflow: "visible"}}
|
||||||
|
iconStyle={!headingMode ? {marginTop: "4px", right: "16px", position: "relative"} :
|
||||||
|
entry.proto.name === "http" ? {marginTop: "4px", left: "calc(50vw + 41px)", position: "absolute"} :
|
||||||
|
{marginTop: "4px", left: "calc(50vw - 9px)", position: "absolute"}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
title="Source Name"
|
||||||
|
>
|
||||||
|
{entry.src.name ? entry.src.name : "[Unresolved]"}
|
||||||
|
</span>
|
||||||
|
</Queryable>
|
||||||
|
<SwapHorizIcon style={{color: entry.proto.backgroundColor, marginTop: "-2px",marginLeft:"5px",marginRight:"5px"}}></SwapHorizIcon>
|
||||||
|
<Queryable
|
||||||
|
query={`dst.name == "${entry.dst.name}"`}
|
||||||
|
displayIconOnMouseOver={true}
|
||||||
|
flipped={true}
|
||||||
|
style={{marginTop: "-4px"}}
|
||||||
|
iconStyle={{marginTop: "4px", marginLeft: "-2px",right: "11px", position: "relative"}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
title="Destination Name">
|
||||||
|
{entry.dst.name ? entry.dst.name : "[Unresolved]"}
|
||||||
|
</span>
|
||||||
|
</Queryable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
rule ?
|
||||||
|
<div className={`${styles.ruleNumberText} ${ruleSuccess ? styles.ruleNumberTextSuccess : styles.ruleNumberTextFailure} ${rule && contractEnabled ? styles.separatorRight : ""}`}>
|
||||||
|
{`Rules (${numberOfRules})`}
|
||||||
|
</div>
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
{
|
||||||
|
contractEnabled ?
|
||||||
|
<div className={`${styles.ruleNumberText} ${ruleSuccess ? styles.ruleNumberTextSuccess : styles.ruleNumberTextFailure} ${rule && contractEnabled ? styles.separatorLeft : ""}`}>
|
||||||
|
{contractText}
|
||||||
|
</div>
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
<div className={styles.separatorRight}>
|
||||||
|
<Queryable
|
||||||
|
query={`src.ip == "${entry.src.ip}"`}
|
||||||
|
displayIconOnMouseOver={true}
|
||||||
|
flipped={true}
|
||||||
|
iconStyle={{marginRight: "16px"}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`${styles.tcpInfo} ${styles.ip}`}
|
||||||
|
title="Source IP"
|
||||||
|
>
|
||||||
|
{entry.src.ip}
|
||||||
|
</span>
|
||||||
|
</Queryable>
|
||||||
|
<span className={`${styles.tcpInfo}`} style={{marginTop: "18px"}}>{entry.src.port ? ":" : ""}</span>
|
||||||
|
<Queryable
|
||||||
|
query={`src.port == "${entry.src.port}"`}
|
||||||
|
displayIconOnMouseOver={true}
|
||||||
|
flipped={true}
|
||||||
|
iconStyle={{marginTop: "28px"}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`${styles.tcpInfo} ${styles.port}`}
|
||||||
|
title="Source Port"
|
||||||
|
>
|
||||||
|
{entry.src.port}
|
||||||
|
</span>
|
||||||
|
</Queryable>
|
||||||
|
{entry.isOutgoing ?
|
||||||
|
<Queryable
|
||||||
|
query={`outgoing == true`}
|
||||||
|
displayIconOnMouseOver={true}
|
||||||
|
flipped={true}
|
||||||
|
iconStyle={{marginTop: "28px"}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={outgoingIcon}
|
||||||
|
alt="Ingoing traffic"
|
||||||
|
title="Ingoing"
|
||||||
|
/>
|
||||||
|
</Queryable>
|
||||||
|
:
|
||||||
|
<Queryable
|
||||||
|
query={`outgoing == true`}
|
||||||
|
displayIconOnMouseOver={true}
|
||||||
|
flipped={true}
|
||||||
|
iconStyle={{marginTop: "28px"}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={ingoingIcon}
|
||||||
|
alt="Outgoing traffic"
|
||||||
|
title="Outgoing"
|
||||||
|
onClick={() => {
|
||||||
|
const query = `outgoing == false`;
|
||||||
|
setQuery(queryState ? `${queryState} and ${query}` : query);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Queryable>
|
||||||
|
}
|
||||||
|
<Queryable
|
||||||
|
query={`dst.ip == "${entry.dst.ip}"`}
|
||||||
|
displayIconOnMouseOver={true}
|
||||||
|
flipped={false}
|
||||||
|
iconStyle={{marginTop: "28px"}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`${styles.tcpInfo} ${styles.ip}`}
|
||||||
|
title="Destination IP"
|
||||||
|
>
|
||||||
|
{entry.dst.ip}
|
||||||
|
</span>
|
||||||
|
</Queryable>
|
||||||
|
<span className={`${styles.tcpInfo}`} style={{marginTop: "18px"}}>:</span>
|
||||||
|
<Queryable
|
||||||
|
query={`dst.port == "${entry.dst.port}"`}
|
||||||
|
displayIconOnMouseOver={true}
|
||||||
|
flipped={false}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`${styles.tcpInfo} ${styles.port}`}
|
||||||
|
title="Destination Port"
|
||||||
|
>
|
||||||
|
{entry.dst.port}
|
||||||
|
</span>
|
||||||
|
</Queryable>
|
||||||
|
</div>
|
||||||
|
<div className={styles.timestamp}>
|
||||||
|
<Queryable
|
||||||
|
query={`timestamp >= datetime("${Moment(+entry.timestamp)?.utc().format('MM/DD/YYYY, h:mm:ss.SSS A')}")`}
|
||||||
|
displayIconOnMouseOver={true}
|
||||||
|
flipped={false}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
title="Timestamp (UTC)"
|
||||||
|
>
|
||||||
|
{Moment(+entry.timestamp)?.utc().format('MM/DD/YYYY, h:mm:ss.SSS A')}
|
||||||
|
</span>
|
||||||
|
</Queryable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
|
||||||
|
}
|
44
traffic-viewer/src/components/Filters/Filters.module.sass
Normal file
44
traffic-viewer/src/components/Filters/Filters.module.sass
Normal file
@@ -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
|
318
traffic-viewer/src/components/Filters/Filters.tsx
Normal file
318
traffic-viewer/src/components/Filters/Filters.tsx
Normal file
@@ -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<FiltersProps> = ({backgroundColor, ws, openWebSocket}) => {
|
||||||
|
return <div className={styles.container}>
|
||||||
|
<QueryForm
|
||||||
|
backgroundColor={backgroundColor}
|
||||||
|
ws={ws}
|
||||||
|
openWebSocket={openWebSocket}
|
||||||
|
/>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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<QueryFormProps> = ({backgroundColor, ws, openWebSocket}) => {
|
||||||
|
|
||||||
|
const formRef = useRef<HTMLFormElement>(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 <>
|
||||||
|
<form
|
||||||
|
ref={formRef}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
xs={8}
|
||||||
|
style={{
|
||||||
|
maxHeight: '25vh',
|
||||||
|
overflowY: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<label>
|
||||||
|
<CodeEditor
|
||||||
|
value={query}
|
||||||
|
language="py"
|
||||||
|
placeholder="Mizu Filter Syntax"
|
||||||
|
onChange={handleChange}
|
||||||
|
padding={8}
|
||||||
|
style={{
|
||||||
|
fontSize: 14,
|
||||||
|
backgroundColor: `${backgroundColor}`,
|
||||||
|
fontFamily: 'ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
style={{
|
||||||
|
margin: "2px 0px 0px 0px",
|
||||||
|
backgroundColor: variables.blueColor,
|
||||||
|
fontWeight: 600,
|
||||||
|
borderRadius: "4px",
|
||||||
|
color: "#fff",
|
||||||
|
textTransform: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
title="Open Filtering Guide (Cheatsheet)"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
style={{
|
||||||
|
margin: "2px 0px 0px 10px",
|
||||||
|
minWidth: "26px",
|
||||||
|
backgroundColor: variables.blueColor,
|
||||||
|
fontWeight: 600,
|
||||||
|
borderRadius: "4px",
|
||||||
|
color: "#fff",
|
||||||
|
textTransform: "none",
|
||||||
|
}}
|
||||||
|
onClick={handleOpenModal}
|
||||||
|
>
|
||||||
|
<MenuBookIcon fontSize="inherit"></MenuBookIcon>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
aria-labelledby="transition-modal-title"
|
||||||
|
aria-describedby="transition-modal-description"
|
||||||
|
open={openModal}
|
||||||
|
onClose={handleCloseModal}
|
||||||
|
closeAfterTransition
|
||||||
|
BackdropComponent={Backdrop}
|
||||||
|
BackdropProps={{
|
||||||
|
timeout: 500,
|
||||||
|
}}
|
||||||
|
style={{overflow: 'auto'}}
|
||||||
|
>
|
||||||
|
<Fade in={openModal}>
|
||||||
|
<Box sx={modalStyle}>
|
||||||
|
<Typography id="modal-modal-title" variant="h5" component="h2" style={{textAlign: 'center'}}>
|
||||||
|
Filtering Guide (Cheatsheet)
|
||||||
|
</Typography>
|
||||||
|
<Typography component={'span'} id="modal-modal-description">
|
||||||
|
<p>Mizu has a rich filtering syntax that let's you query the results both flexibly and efficiently.</p>
|
||||||
|
<p>Here are some examples that you can try;</p>
|
||||||
|
</Typography>
|
||||||
|
<Grid container>
|
||||||
|
<Grid item xs style={{margin: "10px"}}>
|
||||||
|
<Typography id="modal-modal-description">
|
||||||
|
This is a simple query that matches to HTTP packets with request path "/catalogue":
|
||||||
|
</Typography>
|
||||||
|
<SyntaxHighlighter
|
||||||
|
showLineNumbers={false}
|
||||||
|
code={`http and request.path == "/catalogue"`}
|
||||||
|
language="python"
|
||||||
|
/>
|
||||||
|
<Typography id="modal-modal-description">
|
||||||
|
The same query can be negated for HTTP path and written like this:
|
||||||
|
</Typography>
|
||||||
|
<SyntaxHighlighter
|
||||||
|
showLineNumbers={false}
|
||||||
|
code={`http and request.path != "/catalogue"`}
|
||||||
|
language="python"
|
||||||
|
/>
|
||||||
|
<Typography id="modal-modal-description">
|
||||||
|
The syntax supports regular expressions. Here is a query that matches the HTTP requests that send JSON to a server:
|
||||||
|
</Typography>
|
||||||
|
<SyntaxHighlighter
|
||||||
|
showLineNumbers={false}
|
||||||
|
code={`http and request.headers["Accept"] == r"application/json.*"`}
|
||||||
|
language="python"
|
||||||
|
/>
|
||||||
|
<Typography id="modal-modal-description">
|
||||||
|
Here is another query that matches HTTP responses with status code 4xx:
|
||||||
|
</Typography>
|
||||||
|
<SyntaxHighlighter
|
||||||
|
showLineNumbers={false}
|
||||||
|
code={`http and response.status == r"4.*"`}
|
||||||
|
language="python"
|
||||||
|
/>
|
||||||
|
<Typography id="modal-modal-description">
|
||||||
|
The same exact query can be as integer comparison:
|
||||||
|
</Typography>
|
||||||
|
<SyntaxHighlighter
|
||||||
|
showLineNumbers={false}
|
||||||
|
code={`http and response.status >= 400`}
|
||||||
|
language="python"
|
||||||
|
/>
|
||||||
|
<Typography id="modal-modal-description">
|
||||||
|
The results can be queried based on their timestamps:
|
||||||
|
</Typography>
|
||||||
|
<SyntaxHighlighter
|
||||||
|
showLineNumbers={false}
|
||||||
|
code={`timestamp < datetime("10/28/2021, 9:13:02.905 PM")`}
|
||||||
|
language="python"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Divider className={styles.divider1} orientation="vertical" flexItem />
|
||||||
|
<Grid item xs style={{margin: "10px"}}>
|
||||||
|
<Typography id="modal-modal-description">
|
||||||
|
Since Mizu supports various protocols like gRPC, AMQP, Kafka and Redis. It's possible to write complex queries that match multiple protocols like this:
|
||||||
|
</Typography>
|
||||||
|
<SyntaxHighlighter
|
||||||
|
showLineNumbers={false}
|
||||||
|
code={`(http and request.method == "PUT") or (amqp and request.queue.startsWith("test"))\n or (kafka and response.payload.errorCode == 2) or (redis and request.key == "example")\n or (grpc and request.headers[":path"] == r".*foo.*")`}
|
||||||
|
language="python"
|
||||||
|
/>
|
||||||
|
<Typography id="modal-modal-description">
|
||||||
|
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:
|
||||||
|
</Typography>
|
||||||
|
<img
|
||||||
|
src={filterUIExample1}
|
||||||
|
width={600}
|
||||||
|
alt="Clicking to UI elements (left-pane)"
|
||||||
|
title="Clicking to UI elements (left-pane)"
|
||||||
|
/>
|
||||||
|
<Typography id="modal-modal-description">
|
||||||
|
Such that; clicking this icon in left-pane, would append the query below:
|
||||||
|
</Typography>
|
||||||
|
<SyntaxHighlighter
|
||||||
|
showLineNumbers={false}
|
||||||
|
code={`and dst.name == "carts.sock-shop"`}
|
||||||
|
language="python"
|
||||||
|
/>
|
||||||
|
<Typography id="modal-modal-description">
|
||||||
|
Another queriable UI element example, this time from the right-pane:
|
||||||
|
</Typography>
|
||||||
|
<img
|
||||||
|
src={filterUIExample2}
|
||||||
|
width={300}
|
||||||
|
alt="Clicking to UI elements (right-pane)"
|
||||||
|
title="Clicking to UI elements (right-pane)"
|
||||||
|
/>
|
||||||
|
<Typography id="modal-modal-description">
|
||||||
|
A query that compares one selector to another is also a valid query:
|
||||||
|
</Typography>
|
||||||
|
<SyntaxHighlighter
|
||||||
|
showLineNumbers={false}
|
||||||
|
code={`http and (request.query["x"] == response.headers["y"]\n or response.content.text.contains(request.query["x"]))`}
|
||||||
|
language="python"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Divider className={styles.divider2} orientation="vertical" flexItem />
|
||||||
|
<Grid item xs style={{margin: "10px"}}>
|
||||||
|
<Typography id="modal-modal-description">
|
||||||
|
There are a few helper methods included the in the filter language* to help building queries more easily.
|
||||||
|
</Typography>
|
||||||
|
<br></br>
|
||||||
|
<Typography id="modal-modal-description">
|
||||||
|
true if the given selector's value starts with (similarly <code style={{fontSize: "14px"}}>endsWith</code>, <code style={{fontSize: "14px"}}>contains</code>) the string:
|
||||||
|
</Typography>
|
||||||
|
<SyntaxHighlighter
|
||||||
|
showLineNumbers={false}
|
||||||
|
code={`request.path.startsWith("something")`}
|
||||||
|
language="python"
|
||||||
|
/>
|
||||||
|
<Typography id="modal-modal-description">
|
||||||
|
a field that contains a JSON encoded string can be filtered based a JSONPath:
|
||||||
|
</Typography>
|
||||||
|
<SyntaxHighlighter
|
||||||
|
showLineNumbers={false}
|
||||||
|
code={`response.content.text.json().some.path == "somevalue"`}
|
||||||
|
language="python"
|
||||||
|
/>
|
||||||
|
<Typography id="modal-modal-description">
|
||||||
|
fields that contain sensitive information can be redacted:
|
||||||
|
</Typography>
|
||||||
|
<SyntaxHighlighter
|
||||||
|
showLineNumbers={false}
|
||||||
|
code={`and redact("request.path", "src.name")`}
|
||||||
|
language="python"
|
||||||
|
/>
|
||||||
|
<Typography id="modal-modal-description">
|
||||||
|
returns the UNIX timestamp which is the equivalent of the time that's provided by the string. Invalid input evaluates to false:
|
||||||
|
</Typography>
|
||||||
|
<SyntaxHighlighter
|
||||||
|
showLineNumbers={false}
|
||||||
|
code={`timestamp >= datetime("10/19/2021, 6:29:02.593 PM")`}
|
||||||
|
language="python"
|
||||||
|
/>
|
||||||
|
<Typography id="modal-modal-description">
|
||||||
|
limits the number of records that are streamed back as a result of a query. Always evaluates to true:
|
||||||
|
</Typography>
|
||||||
|
<SyntaxHighlighter
|
||||||
|
showLineNumbers={false}
|
||||||
|
code={`and limit(100)`}
|
||||||
|
language="python"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<br></br>
|
||||||
|
<Typography id="modal-modal-description" style={{fontSize: 12, fontStyle: 'italic'}}>
|
||||||
|
*The filtering functionality is provided through <b>Basenine</b> database server. Please refer to <a href="https://github.com/up9inc/basenine/wiki/BFL-Syntax-Reference"><b>BFL Syntax Reference</b></a> for more information.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Fade>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
}
|
12
traffic-viewer/src/components/TLSWarning/TLSWarning.sass
Normal file
12
traffic-viewer/src/components/TLSWarning/TLSWarning.sass
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
.httpsDomains
|
||||||
|
display: none
|
||||||
|
margin: 0
|
||||||
|
padding: 0
|
||||||
|
list-style: none
|
||||||
|
|
||||||
|
.customWarningStyle
|
||||||
|
&:hover
|
||||||
|
overflow-y: scroll
|
||||||
|
height: 85px
|
||||||
|
.httpsDomains
|
||||||
|
display: block
|
42
traffic-viewer/src/components/TLSWarning/TLSWarning.tsx
Normal file
42
traffic-viewer/src/components/TLSWarning/TLSWarning.tsx
Normal file
@@ -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<string>
|
||||||
|
setAddressesWithTLS: (addresses: Set<string>) => void
|
||||||
|
userDismissedTLSWarning: boolean
|
||||||
|
setUserDismissedTLSWarning: (flag: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TLSWarning: React.FC<TLSWarningProps> = ({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 (<Snackbar open={showTLSWarning && !userDismissedTLSWarning}>
|
||||||
|
<MuiAlert classes={{filledWarning: 'customWarningStyle'}} elevation={6} variant="filled"
|
||||||
|
onClose={() => setUserDismissedTLSWarning(true)} severity="warning">
|
||||||
|
Mizu is detecting TLS traffic, this type of traffic will not be displayed.
|
||||||
|
{addressesWithTLS.size > 0 &&
|
||||||
|
<ul className="httpsDomains"> {Array.from(addressesWithTLS, address => <li>{address}</li>)} </ul>}
|
||||||
|
</MuiAlert>
|
||||||
|
</Snackbar>);
|
||||||
|
}
|
122
traffic-viewer/src/components/TrafficPage.sass
Normal file
122
traffic-viewer/src/components/TrafficPage.sass
Normal file
@@ -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
|
311
traffic-viewer/src/components/TrafficPage.tsx
Normal file
311
traffic-viewer/src/components/TrafficPage.tsx
Normal file
@@ -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<TrafficPageProps> = ({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<string>());
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="TrafficPage">
|
||||||
|
<div className="TrafficPageHeader">
|
||||||
|
<div className="TrafficPageStreamStatus">
|
||||||
|
<img className="playPauseIcon" style={{ visibility: wsConnection === WsConnectionStatus.Connected ? "visible" : "hidden" }} alt="pause"
|
||||||
|
src={pauseIcon} onClick={toggleConnection} />
|
||||||
|
<img className="playPauseIcon" style={{ position: "absolute", visibility: wsConnection === WsConnectionStatus.Connected ? "hidden" : "visible" }} alt="play"
|
||||||
|
src={playIcon} onClick={toggleConnection} />
|
||||||
|
<div className="connectionText">
|
||||||
|
{getConnectionTitle()}
|
||||||
|
<div className={"indicatorContainer " + getConnectionStatusClass(true)}>
|
||||||
|
<div className={"indicator " + getConnectionStatusClass(false)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{<div className="TrafficPage-Container">
|
||||||
|
<div className="TrafficPage-ListContainer">
|
||||||
|
<Filters
|
||||||
|
backgroundColor={queryBackgroundColor}
|
||||||
|
ws={ws.current}
|
||||||
|
openWebSocket={openWebSocket}
|
||||||
|
/>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<EntriesList
|
||||||
|
listEntryREF={listEntry}
|
||||||
|
onSnapBrokenEvent={onSnapBrokenEvent}
|
||||||
|
isSnappedToBottom={isSnappedToBottom}
|
||||||
|
setIsSnappedToBottom={setIsSnappedToBottom}
|
||||||
|
queriedCurrent={queriedCurrent}
|
||||||
|
setQueriedCurrent={setQueriedCurrent}
|
||||||
|
queriedTotal={queriedTotal}
|
||||||
|
setQueriedTotal={setQueriedTotal}
|
||||||
|
startTime={startTime}
|
||||||
|
noMoreDataTop={noMoreDataTop}
|
||||||
|
setNoMoreDataTop={setNoMoreDataTop}
|
||||||
|
leftOffTop={leftOffTop}
|
||||||
|
setLeftOffTop={setLeftOffTop}
|
||||||
|
ws={ws.current}
|
||||||
|
openWebSocket={openWebSocket}
|
||||||
|
leftOffBottom={leftOffBottom}
|
||||||
|
truncatedTimestamp={truncatedTimestamp}
|
||||||
|
setTruncatedTimestamp={setTruncatedTimestamp}
|
||||||
|
scrollableRef={scrollableRef}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={classes.details} id="rightSideContainer">
|
||||||
|
{focusedEntryId && <EntryDetailed />}
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
|
{tappingStatus && <StatusBar />}
|
||||||
|
<TLSWarning showTLSWarning={showTLSWarning}
|
||||||
|
setShowTLSWarning={setShowTLSWarning}
|
||||||
|
addressesWithTLS={addressesWithTLS}
|
||||||
|
setAddressesWithTLS={setAddressesWithTLS}
|
||||||
|
userDismissedTLSWarning={userDismissedTLSWarning}
|
||||||
|
setUserDismissedTLSWarning={setUserDismissedTLSWarning} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
33
traffic-viewer/src/components/UI/CollapsibleContainer.sass
Normal file
33
traffic-viewer/src/components/UI/CollapsibleContainer.sass
Normal file
@@ -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)
|
43
traffic-viewer/src/components/UI/CollapsibleContainer.tsx
Normal file
43
traffic-viewer/src/components/UI/CollapsibleContainer.tsx
Normal file
@@ -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<Props> = ({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.Fragment>
|
||||||
|
<div
|
||||||
|
className={`CollapsibleContainer-Header ${stickyHeader ? "CollapsibleContainer-Header-Sticky" : ""}
|
||||||
|
${expanded ? "CollapsibleContainer-Header-Expanded" : ""}`}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
React.isValidElement(title)?
|
||||||
|
<React.Fragment>{title}</React.Fragment> :
|
||||||
|
<React.Fragment>
|
||||||
|
<div className={`CollapsibleContainer-Title ${titleClassName ? titleClassName : ''}`}>{title}</div>
|
||||||
|
<img
|
||||||
|
className="CollapsibleContainer-ExpandCollapseButton"
|
||||||
|
src={expanded ? expandedImg : collapsedImg}
|
||||||
|
alt="Expand/Collapse Button"
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{expanded ? children : null}
|
||||||
|
</React.Fragment>;
|
||||||
|
|
||||||
|
return stickyHeader ? content : <div className={classNames}>{content}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CollapsibleContainer;
|
41
traffic-viewer/src/components/UI/FancyTextDisplay.sass
Normal file
41
traffic-viewer/src/components/UI/FancyTextDisplay.sass
Normal file
@@ -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
|
||||||
|
|
63
traffic-viewer/src/components/UI/FancyTextDisplay.tsx
Normal file
63
traffic-viewer/src/components/UI/FancyTextDisplay.tsx
Normal file
@@ -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<Props> = ({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 = <span className={'FancyTextDisplay-Text'}>{text}</span>;
|
||||||
|
|
||||||
|
const copyButton = isPossibleToCopy && text ? <CopyToClipboard text={text} onCopy={onCopy}>
|
||||||
|
<span
|
||||||
|
className={`FancyTextDisplay-Icon`}
|
||||||
|
title={`Copy "${text}" value to clipboard`}
|
||||||
|
>
|
||||||
|
<img src={duplicateImg} alt="Duplicate full value"/>
|
||||||
|
{showCopiedNotification && <span className={'FancyTextDisplay-CopyNotifier'}>Copied</span>}
|
||||||
|
</span>
|
||||||
|
</CopyToClipboard> : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
className={`FancyTextDisplay-Container ${className ? className : ''} ${displayIconOnMouseOver ? 'displayIconOnMouseOver ' : ''} ${applyTextEllipsis ? ' FancyTextDisplay-ContainerEllipsis' : ''}`}
|
||||||
|
title={text}
|
||||||
|
onMouseOver={ e => setShowTooltip(true)}
|
||||||
|
onMouseLeave={ e => setShowTooltip(false)}
|
||||||
|
>
|
||||||
|
{!buttonOnly && flipped && textElement}
|
||||||
|
{copyButton}
|
||||||
|
{!buttonOnly && !flipped && textElement}
|
||||||
|
{useTooltip && showTooltip && <span className={'FancyTextDisplay-CopyNotifier'}>{text}</span>}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FancyTextDisplay;
|
24
traffic-viewer/src/components/UI/Protocol.module.sass
Normal file
24
traffic-viewer/src/components/UI/Protocol.module.sass
Normal file
@@ -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
|
66
traffic-viewer/src/components/UI/Protocol.tsx
Normal file
66
traffic-viewer/src/components/UI/Protocol.tsx
Normal file
@@ -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<ProtocolProps> = ({protocol, horizontal}) => {
|
||||||
|
if (horizontal) {
|
||||||
|
return <Queryable
|
||||||
|
query={protocol.macro}
|
||||||
|
displayIconOnMouseOver={true}
|
||||||
|
>
|
||||||
|
<a target="_blank" rel="noopener noreferrer" href={protocol.referenceLink}>
|
||||||
|
<span
|
||||||
|
className={`${styles.base} ${styles.horizontal}`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: protocol.backgroundColor,
|
||||||
|
color: protocol.foregroundColor,
|
||||||
|
fontSize: 13,
|
||||||
|
}}
|
||||||
|
title={protocol.abbr}
|
||||||
|
>
|
||||||
|
{protocol.longName}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</Queryable>
|
||||||
|
} else {
|
||||||
|
return <Queryable
|
||||||
|
query={protocol.macro}
|
||||||
|
displayIconOnMouseOver={true}
|
||||||
|
flipped={false}
|
||||||
|
iconStyle={{marginTop: "52px", marginRight: "10px", zIndex: 1000}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`${styles.base} ${styles.vertical}`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: protocol.backgroundColor,
|
||||||
|
color: protocol.foregroundColor,
|
||||||
|
fontSize: protocol.fontSize,
|
||||||
|
marginRight: "-20px",
|
||||||
|
}}
|
||||||
|
title={protocol.longName}
|
||||||
|
>
|
||||||
|
{protocol.abbr}
|
||||||
|
</span>
|
||||||
|
</Queryable>
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Protocol;
|
48
traffic-viewer/src/components/UI/Queryable.sass
Normal file
48
traffic-viewer/src/components/UI/Queryable.sass
Normal file
@@ -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
|
||||||
|
|
66
traffic-viewer/src/components/UI/Queryable.tsx
Normal file
66
traffic-viewer/src/components/UI/Queryable.tsx
Normal file
@@ -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<Props> = ({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 ? <CopyToClipboard text={query} onCopy={onCopy}>
|
||||||
|
<span
|
||||||
|
className={`Queryable-Icon`}
|
||||||
|
title={`Add "${query}" to the filter`}
|
||||||
|
style={iconStyle}
|
||||||
|
>
|
||||||
|
<AddCircleIcon fontSize="small" color="inherit"/>
|
||||||
|
{showAddedNotification && <span className={'Queryable-AddNotifier'}>Added</span>}
|
||||||
|
</span>
|
||||||
|
</CopyToClipboard> : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`Queryable-Container displayIconOnMouseOver ${className ? className : ''} ${displayIconOnMouseOver ? 'displayIconOnMouseOver ' : ''}`}
|
||||||
|
style={style}
|
||||||
|
onMouseOver={ e => setShowTooltip(true)}
|
||||||
|
onMouseLeave={ e => setShowTooltip(false)}
|
||||||
|
>
|
||||||
|
{flipped && addButton}
|
||||||
|
{children}
|
||||||
|
{!flipped && addButton}
|
||||||
|
{useTooltip && showTooltip && <span className={'Queryable-Tooltip'}>{query}</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Queryable;
|
51
traffic-viewer/src/components/UI/StatusBar.sass
Normal file
51
traffic-viewer/src/components/UI/StatusBar.sass
Normal file
@@ -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
|
42
traffic-viewer/src/components/UI/StatusBar.tsx
Normal file
42
traffic-viewer/src/components/UI/StatusBar.tsx
Normal file
@@ -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 <div className={'statusBar' + (expandedBar ? ' expandedStatusBar' : "")} onMouseOver={() => setExpandedBar(true)} onMouseLeave={() => setExpandedBar(false)}>
|
||||||
|
<div className="podsCount">
|
||||||
|
{tappingStatus.some(pod => !pod.isTapped) && <img src={warningIcon} alt="warning"/>}
|
||||||
|
{`Tapping ${amountOfUntappedPods > 0 ? amountOfTappedPods + " / " + amountOfPods : amountOfPods} ${pluralize('pod', amountOfPods)} in ${pluralize('namespace', uniqueNamespaces.length)} ${uniqueNamespaces.join(", ")}`}</div>
|
||||||
|
{expandedBar && <div style={{marginTop: 20}}>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{width: "40%"}}>Pod name</th>
|
||||||
|
<th style={{width: "40%"}}>Namespace</th>
|
||||||
|
<th style={{width: "20%", textAlign: "center"}}>Tapping</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{tappingStatus.map(pod => <tr key={pod.name}>
|
||||||
|
<td style={{width: "40%"}}>{pod.name}</td>
|
||||||
|
<td style={{width: "40%"}}>{pod.namespace}</td>
|
||||||
|
<td style={{width: "20%", textAlign: "center"}}><img style={{height: 20}} alt="status" src={pod.isTapped ? successIcon : failIcon}/></td>
|
||||||
|
</tr>)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>}
|
||||||
|
</div>;
|
||||||
|
}
|
6
traffic-viewer/src/components/UI/Summary.module.sass
Normal file
6
traffic-viewer/src/components/UI/Summary.module.sass
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.container
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
|
||||||
|
.summary
|
||||||
|
white-space: nowrap
|
39
traffic-viewer/src/components/UI/Summary.tsx
Normal file
39
traffic-viewer/src/components/UI/Summary.tsx
Normal file
@@ -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<SummaryProps> = ({method, summary}) => {
|
||||||
|
|
||||||
|
return <div className={styles.container}>
|
||||||
|
{method && <Queryable
|
||||||
|
query={`method == "${method}"`}
|
||||||
|
className={`${miscStyles.protocol} ${miscStyles.method}`}
|
||||||
|
displayIconOnMouseOver={true}
|
||||||
|
style={{whiteSpace: "nowrap"}}
|
||||||
|
flipped={true}
|
||||||
|
iconStyle={{zIndex:"5",position:"relative",right:"22px"}}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{method}
|
||||||
|
</span>
|
||||||
|
</Queryable>}
|
||||||
|
{summary && <Queryable
|
||||||
|
query={`summary == "${summary}"`}
|
||||||
|
displayIconOnMouseOver={true}
|
||||||
|
flipped={true}
|
||||||
|
iconStyle={{zIndex:"5",position:"relative",right:"14px"}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`${styles.summary}`}
|
||||||
|
>
|
||||||
|
{summary}
|
||||||
|
</div>
|
||||||
|
</Queryable>}
|
||||||
|
</div>
|
||||||
|
};
|
@@ -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;
|
||||||
|
}
|
47
traffic-viewer/src/components/UI/SyntaxHighlighter/index.tsx
Normal file
47
traffic-viewer/src/components/UI/SyntaxHighlighter/index.tsx
Normal file
@@ -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<Props> = ({
|
||||||
|
code,
|
||||||
|
showLineNumbers = false,
|
||||||
|
language = null
|
||||||
|
}) => {
|
||||||
|
const markers = showLineNumbers ? code.split("\n").map((item, i) => {
|
||||||
|
return {
|
||||||
|
line: i + 1,
|
||||||
|
className: 'hljs-marker-line'
|
||||||
|
}
|
||||||
|
}) : [];
|
||||||
|
|
||||||
|
return <div style={{fontSize: ".75rem"}}><Lowlight language={language ? language : ""} value={code} markers={markers}/></div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SyntaxHighlighter;
|
27
traffic-viewer/src/components/UI/misc.module.sass
Normal file
27
traffic-viewer/src/components/UI/misc.module.sass
Normal file
@@ -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
|
8
traffic-viewer/src/global.d.ts
vendored
Normal file
8
traffic-viewer/src/global.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
declare module "*.svg" {
|
||||||
|
const content: any;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.scss';
|
||||||
|
declare module '*.sass';
|
||||||
|
declare module '*.png';
|
6
traffic-viewer/src/index.js
Normal file
6
traffic-viewer/src/index.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { TrafficPage } from './components/TrafficPage'
|
||||||
|
|
||||||
|
export const TrafficViewer = ({ text }) => {
|
||||||
|
return <TrafficPage></TrafficPage>
|
||||||
|
}
|
7
traffic-viewer/src/index.test.js
Normal file
7
traffic-viewer/src/index.test.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { ExampleComponent } from '.'
|
||||||
|
|
||||||
|
describe('ExampleComponent', () => {
|
||||||
|
it('is truthy', () => {
|
||||||
|
expect(ExampleComponent).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
22
traffic-viewer/tsconfig.json
Normal file
22
traffic-viewer/tsconfig.json
Normal file
@@ -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"]
|
||||||
|
}
|
Reference in New Issue
Block a user