first commit

This commit is contained in:
Leon
2022-02-09 16:02:50 +02:00
parent db427d91cc
commit b38964dc1b
42 changed files with 3238 additions and 0 deletions

2
.gitignore vendored
View File

@@ -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
View 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
View File

@@ -0,0 +1 @@
# traffic-viewer

View 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"
]
}

View File

@@ -0,0 +1,5 @@
{
"env": {
"jest": true
}
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -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

View 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>
</>;
};

View 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}
/>}
</>
</>
};

View File

@@ -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

View 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>
}

View File

@@ -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

View 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;

View File

@@ -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

View 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>
</>
}

View 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

View 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>
</>
}

View 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

View 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>);
}

View 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

View 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>
);
};

View 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)

View 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;

View 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

View 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;

View 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

View 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;

View 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

View 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;

View 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

View 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>;
}

View File

@@ -0,0 +1,6 @@
.container
display: flex
align-items: center
.summary
white-space: nowrap

View 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>
};

View File

@@ -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;
}

View 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;

View 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
View File

@@ -0,0 +1,8 @@
declare module "*.svg" {
const content: any;
export default content;
}
declare module '*.scss';
declare module '*.sass';
declare module '*.png';

View File

@@ -0,0 +1,6 @@
import React from 'react'
import { TrafficPage } from './components/TrafficPage'
export const TrafficViewer = ({ text }) => {
return <TrafficPage></TrafficPage>
}

View File

@@ -0,0 +1,7 @@
import { ExampleComponent } from '.'
describe('ExampleComponent', () => {
it('is truthy', () => {
expect(ExampleComponent).toBeTruthy()
})
})

View 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"]
}