mirror of
https://github.com/kubeshark/kubeshark.git
synced 2025-09-26 04:54:36 +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
|
||||
tap/extensions/*/bin
|
||||
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