UI Infra - Support multiple entry types + refactoring (#211)

* no message

* change local api path

* generic entry list item + rename files and vars

* entry detailed generic

* fix api file

* clean warnings

* switch

* empty lines

* fix scroll to end feature

Co-authored-by: Roee Gadot <roee.gadot@up9.com>
This commit is contained in:
lirazyehezkel 2021-08-15 12:09:56 +03:00 committed by GitHub
parent 6d2e9af5d7
commit f74a52d4dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 712 additions and 738 deletions

View File

@ -146,6 +146,7 @@ Web interface is now available at http://localhost:8899
^C ^C
``` ```
Any request that contains `User-Agent` header with one of the specified values (`kube-probe` or `prometheus`) will not be captured Any request that contains `User-Agent` header with one of the specified values (`kube-probe` or `prometheus`) will not be captured
### API Rules validation ### API Rules validation
@ -155,3 +156,15 @@ Such validation may test response for specific JSON fields, headers, etc.
Please see [API RULES](docs/POLICY_RULES.md) page for more details and syntax. Please see [API RULES](docs/POLICY_RULES.md) page for more details and syntax.
## How to Run local UI
- run from mizu/agent `go run main.go --hars-read --hars-dir <folder>`
- copy Har files into the folder from last command
- change `MizuWebsocketURL` and `apiURL` in `api.js` file
- run from mizu/ui - `npm run start`
- open browser on `localhost:3000`

View File

@ -26,14 +26,17 @@ var apiServerMode = flag.Bool("api-server", false, "Run in API server mode with
var standaloneMode = flag.Bool("standalone", false, "Run in standalone tapper and API mode") var standaloneMode = flag.Bool("standalone", false, "Run in standalone tapper and API mode")
var apiServerAddress = flag.String("api-server-address", "", "Address of mizu API server") var apiServerAddress = flag.String("api-server-address", "", "Address of mizu API server")
var namespace = flag.String("namespace", "", "Resolve IPs if they belong to resources in this namespace (default is all)") var namespace = flag.String("namespace", "", "Resolve IPs if they belong to resources in this namespace (default is all)")
var harsReaderMode = flag.Bool("hars-read", false, "Run in hars-read mode")
var harsDir = flag.String("hars-dir", "", "Directory to read hars from")
func main() { func main() {
flag.Parse() flag.Parse()
hostMode := os.Getenv(shared.HostModeEnvVar) == "1" hostMode := os.Getenv(shared.HostModeEnvVar) == "1"
tapOpts := &tap.TapOpts{HostMode: hostMode} tapOpts := &tap.TapOpts{HostMode: hostMode}
if !*tapperMode && !*apiServerMode && !*standaloneMode {
panic("One of the flags --tap, --api or --standalone must be provided") if !*tapperMode && !*apiServerMode && !*standaloneMode && !*harsReaderMode{
panic("One of the flags --tap, --api or --standalone or --hars-read must be provided")
} }
if *standaloneMode { if *standaloneMode {
@ -77,6 +80,13 @@ func main() {
go api.StartReadingEntries(filteredHarChannel, nil) go api.StartReadingEntries(filteredHarChannel, nil)
hostApi(socketHarOutChannel) hostApi(socketHarOutChannel)
} else if *harsReaderMode {
socketHarOutChannel := make(chan *tap.OutputChannelItem, 1000)
filteredHarChannel := make(chan *tap.OutputChannelItem)
go filterHarItems(socketHarOutChannel, filteredHarChannel, getTrafficFilteringOptions())
go api.StartReadingEntries(filteredHarChannel, harsDir)
hostApi(nil)
} }
signalChan := make(chan os.Signal, 1) signalChan := make(chan os.Signal, 1)

View File

@ -1,4 +1,4 @@
@import 'components/style/variables.module' @import 'src/variables.module'
.mizuApp .mizuApp
background-color: $main-background-color background-color: $main-background-color

View File

@ -2,8 +2,8 @@ import React, {useEffect, useState} from 'react';
import './App.sass'; import './App.sass';
import logo from './components/assets/Mizu-logo.svg'; import logo from './components/assets/Mizu-logo.svg';
import {Button, Snackbar} from "@material-ui/core"; import {Button, Snackbar} from "@material-ui/core";
import {HarPage} from "./components/HarPage"; import {TrafficPage} from "./components/TrafficPage";
import Tooltip from "./components/Tooltip"; import Tooltip from "./components/UI/Tooltip";
import {makeStyles} from "@material-ui/core/styles"; import {makeStyles} from "@material-ui/core/styles";
import MuiAlert from '@material-ui/lab/Alert'; import MuiAlert from '@material-ui/lab/Alert';
import Api from "./helpers/api"; import Api from "./helpers/api";
@ -38,6 +38,7 @@ const App = () => {
} }
})(); })();
// eslint-disable-next-line
}, []); }, []);
const onTLSDetected = (destAddress: string) => { const onTLSDetected = (destAddress: string) => {
@ -116,7 +117,7 @@ const App = () => {
</Tooltip> </Tooltip>
} }
</div> </div>
<HarPage setAnalyzeStatus={setAnalyzeStatus} onTLSDetected={onTLSDetected}/> <TrafficPage setAnalyzeStatus={setAnalyzeStatus} onTLSDetected={onTLSDetected}/>
<Snackbar open={showTLSWarning && !userDismissedTLSWarning}> <Snackbar open={showTLSWarning && !userDismissedTLSWarning}>
<MuiAlert elevation={6} variant="filled" onClose={() => setUserDismissedTLSWarning(true)} severity="warning"> <MuiAlert elevation={6} variant="filled" onClose={() => setUserDismissedTLSWarning(true)} severity="warning">
Mizu is detecting TLS traffic{addressesWithTLS.size ? ` (directed to ${Array.from(addressesWithTLS).join(", ")})` : ''}, this type of traffic will not be displayed. Mizu is detecting TLS traffic{addressesWithTLS.size ? ` (directed to ${Array.from(addressesWithTLS).join(", ")})` : ''}, this type of traffic will not be displayed.

View File

@ -1,17 +1,17 @@
import {HarEntry} from "./HarEntry"; import {EntryItem} from "./EntryListItem/EntryListItem";
import React, {useCallback, useEffect, useMemo, useState} from "react"; import React, {useCallback, useEffect, useMemo, useRef, useState} from "react";
import styles from './style/HarEntriesList.module.sass'; import styles from './style/EntriesList.module.sass';
import spinner from './assets/spinner.svg'; import spinner from './assets/spinner.svg';
import ScrollableFeed from "react-scrollable-feed"; import ScrollableFeed from "react-scrollable-feed";
import {StatusType} from "./HarFilters"; import {StatusType} from "./Filters";
import Api from "../helpers/api"; import Api from "../helpers/api";
import uninon from "./assets/union.svg"; import down from "./assets/downImg.svg";
interface HarEntriesListProps { interface HarEntriesListProps {
entries: any[]; entries: any[];
setEntries: (entries: any[]) => void; setEntries: (entries: any[]) => void;
focusedEntryId: string; focusedEntry: any;
setFocusedEntryId: (id: string) => void; setFocusedEntry: (entry: any) => void;
connectionOpen: boolean; connectionOpen: boolean;
noMoreDataTop: boolean; noMoreDataTop: boolean;
setNoMoreDataTop: (flag: boolean) => void; setNoMoreDataTop: (flag: boolean) => void;
@ -32,11 +32,12 @@ enum FetchOperator {
const api = new Api(); const api = new Api();
export const HarEntriesList: React.FC<HarEntriesListProps> = ({entries, setEntries, focusedEntryId, setFocusedEntryId, connectionOpen, noMoreDataTop, setNoMoreDataTop, noMoreDataBottom, setNoMoreDataBottom, methodsFilter, statusFilter, pathFilter, listEntryREF, onScrollEvent, scrollableList}) => { export const EntriesList: React.FC<HarEntriesListProps> = ({entries, setEntries, focusedEntry, setFocusedEntry, connectionOpen, noMoreDataTop, setNoMoreDataTop, noMoreDataBottom, setNoMoreDataBottom, methodsFilter, statusFilter, pathFilter, listEntryREF, onScrollEvent, scrollableList}) => {
const [loadMoreTop, setLoadMoreTop] = useState(false); const [loadMoreTop, setLoadMoreTop] = useState(false);
const [isLoadingTop, setIsLoadingTop] = useState(false); const [isLoadingTop, setIsLoadingTop] = useState(false);
const scrollableRef = useRef(null);
useEffect(() => { useEffect(() => {
const list = document.getElementById('list').firstElementChild; const list = document.getElementById('list').firstElementChild;
list.addEventListener('scroll', (e) => { list.addEventListener('scroll', (e) => {
@ -110,28 +111,24 @@ export const HarEntriesList: React.FC<HarEntriesListProps> = ({entries, setEntri
return <> return <>
<div className={styles.list}> <div className={styles.list}>
<div id="list" ref={listEntryREF} className={styles.list}> <div id="list" ref={listEntryREF} className={styles.list} >
{isLoadingTop && <div className={styles.spinnerContainer}> {isLoadingTop && <div className={styles.spinnerContainer}>
<img alt="spinner" src={spinner} style={{height: 25}}/> <img alt="spinner" src={spinner} style={{height: 25}}/>
</div>} </div>}
<ScrollableFeed onScroll={(isAtBottom) => onScrollEvent(isAtBottom)}> <ScrollableFeed ref={scrollableRef} onScroll={(isAtBottom) => onScrollEvent(isAtBottom)}>
{noMoreDataTop && !connectionOpen && <div id="noMoreDataTop" className={styles.noMoreDataAvailable}>No more data available</div>} {noMoreDataTop && !connectionOpen && <div id="noMoreDataTop" className={styles.noMoreDataAvailable}>No more data available</div>}
{filteredEntries.map(entry => <HarEntry key={entry.id} {filteredEntries.map(entry => <EntryItem key={entry.id}
entry={entry} entry={entry}
setFocusedEntryId={setFocusedEntryId} setFocusedEntry = {setFocusedEntry}
isSelected={focusedEntryId === entry.id}/>)} isSelected={focusedEntry.id === entry.id}/>)}
{!connectionOpen && !noMoreDataBottom && <div className={styles.fetchButtonContainer}> {!connectionOpen && !noMoreDataBottom && <div className={styles.fetchButtonContainer}>
<div className={styles.styledButton} onClick={() => getNewEntries()}>Fetch more entries</div> <div className={styles.styledButton} onClick={() => getNewEntries()}>Fetch more entries</div>
</div>} </div>}
</ScrollableFeed> </ScrollableFeed>
<button type="button" <button type="button"
className={`${styles.btnLive} ${scrollableList ? styles.showButton : styles.hideButton}`} className={`${styles.btnLive} ${scrollableList ? styles.showButton : styles.hideButton}`}
onClick={(_) => { onClick={(_) => scrollableRef.current.scrollToBottom()}>
const list = listEntryREF.current.firstChild; <img alt="down" src={down} />
if(list instanceof HTMLElement) {
list.scrollTo({ top: list.scrollHeight, behavior: 'smooth' })
}
}}><img src={uninon} />
</button> </button>
</div> </div>

View File

@ -0,0 +1,23 @@
@import "src/variables.module"
.content
font-family: "Source Sans Pro", Lucida Grande, Tahoma, sans-serif
height: calc(100% - 56px)
overflow-y: auto
width: 100%
.body
background: $main-background-color
color: $blue-gray
border-radius: 4px
padding: 10px
.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,56 @@
import React from "react";
import styles from './EntryDetailed.module.sass';
import {makeStyles} from "@material-ui/core";
import {EntryType} from "../EntryListItem/EntryListItem";
import {RestEntryDetailsTitle} from "./Rest/RestEntryDetailsTitle";
import {KafkaEntryDetailsTitle} from "./Kafka/KafkaEntryDetailsTitle";
import {RestEntryDetailsContent} from "./Rest/RestEntryDetailsContent";
import {KafkaEntryDetailsContent} from "./Kafka/KafkaEntryDetailsContent";
const useStyles = makeStyles(() => ({
entryTitle: {
display: 'flex',
minHeight: 46,
maxHeight: 46,
alignItems: 'center',
marginBottom: 8,
padding: 5,
paddingBottom: 0
}
}));
interface EntryDetailedProps {
entryData: any;
classes?: any;
entryType: string;
}
export const EntryDetailed: React.FC<EntryDetailedProps> = ({classes, entryData, entryType}) => {
const classesTitle = useStyles();
let title, content;
switch (entryType) {
case EntryType.Rest:
title = <RestEntryDetailsTitle entryData={entryData}/>;
content = <RestEntryDetailsContent entryData={entryData}/>;
break;
case EntryType.Kafka:
title = <KafkaEntryDetailsTitle entryData={entryData}/>;
content = <KafkaEntryDetailsContent entryData={entryData}/>;
break;
default:
title = <RestEntryDetailsTitle entryData={entryData}/>;
content = <RestEntryDetailsContent entryData={entryData}/>;
break;
}
return <>
<div className={classesTitle.entryTitle}>{title}</div>
<div className={styles.content}>
<div className={styles.body}>
{content}
</div>
</div>
</>
};

View File

@ -1,4 +1,4 @@
@import '../style/variables.module' @import 'src/variables.module'
.title .title
display: flex display: flex

View File

@ -0,0 +1,213 @@
import styles from "./EntrySections.module.sass";
import React, {useState} from "react";
import {SyntaxHighlighter} from "../UI/SyntaxHighlighter";
import CollapsibleContainer from "../UI/CollapsibleContainer";
import FancyTextDisplay from "../UI/FancyTextDisplay";
import Checkbox from "../UI/Checkbox";
import ProtobufDecoder from "protobuf-decoder";
interface ViewLineProps {
label: string;
value: number | string;
}
const ViewLine: React.FC<ViewLineProps> = ({label, value}) => {
return (label && value && <tr className={styles.dataLine}>
<td className={styles.dataKey}>{label}</td>
<td>
<FancyTextDisplay
className={styles.dataValue}
text={value}
applyTextEllipsis={false}
flipped={true}
displayIconOnMouseOver={true}
/>
</td>
</tr>) || null;
}
interface SectionCollapsibleTitleProps {
title: string;
isExpanded: boolean;
}
const SectionCollapsibleTitle: React.FC<SectionCollapsibleTitleProps> = ({title, isExpanded}) => {
return <div className={styles.title}>
<span className={`${styles.button} ${isExpanded ? styles.expanded : ''}`}>
{isExpanded ? '-' : '+'}
</span>
<span>{title}</span>
</div>
}
interface SectionContainerProps {
title: string;
}
export const SectionContainer: React.FC<SectionContainerProps> = ({title, children}) => {
const [expanded, setExpanded] = useState(true);
return <CollapsibleContainer
className={styles.collapsibleContainer}
isExpanded={expanded}
onClick={() => setExpanded(!expanded)}
title={<SectionCollapsibleTitle title={title} isExpanded={expanded}/>}
>
{children}
</CollapsibleContainer>
}
interface BodySectionProps {
content: any;
encoding?: string;
contentType?: string;
}
export const BodySection: React.FC<BodySectionProps> = ({content, encoding, contentType}) => {
const MAXIMUM_BYTES_TO_HIGHLIGHT = 10000; // The maximum of chars to highlight in body, in case the response can be megabytes
const supportedLanguages = [['html', 'html'], ['json', 'json'], ['application/grpc', 'json']]; // [[indicator, languageToUse],...]
const jsonLikeFormats = ['json'];
const protobufFormats = ['application/grpc'];
const [isWrapped, setIsWrapped] = useState(false);
const formatTextBody = (body): string => {
const chunk = body.slice(0, MAXIMUM_BYTES_TO_HIGHLIGHT);
const bodyBuf = encoding === 'base64' ? atob(chunk) : chunk;
try {
if (jsonLikeFormats.some(format => content?.mimeType?.indexOf(format) > -1)) {
return JSON.stringify(JSON.parse(bodyBuf), null, 2);
} else if (protobufFormats.some(format => content?.mimeType?.indexOf(format) > -1)) {
// Replace all non printable characters (ASCII)
const protobufDecoder = new ProtobufDecoder(bodyBuf, true);
return JSON.stringify(protobufDecoder.decode().toSimple(), null, 2);
}
} catch (error) {
console.error(error);
}
return bodyBuf;
}
const getLanguage = (mimetype) => {
const chunk = content.text?.slice(0, 100);
if (chunk.indexOf('html') > 0 || chunk.indexOf('HTML') > 0) return supportedLanguages[0][1];
const language = supportedLanguages.find(el => (mimetype + contentType).indexOf(el[0]) > -1);
return language ? language[1] : 'default';
}
return <React.Fragment>
{content && content.text?.length > 0 && <SectionContainer title='Body'>
<table>
<tbody>
<ViewLine label={'Mime type'} value={content?.mimeType}/>
<ViewLine label={'Encoding'} value={encoding}/>
</tbody>
</table>
<div style={{display: 'flex', alignItems: 'center', alignContent: 'center', margin: "5px 0"}} onClick={() => setIsWrapped(!isWrapped)}>
<div style={{paddingTop: 3}}>
<Checkbox checked={isWrapped} onToggle={() => {}}/>
</div>
<span style={{marginLeft: '.5rem'}}>Wrap text</span>
</div>
<SyntaxHighlighter
isWrapped={isWrapped}
code={formatTextBody(content.text)}
language={content?.mimeType ? getLanguage(content.mimeType) : 'default'}
/>
</SectionContainer>}
</React.Fragment>
}
interface TableSectionProps {
title: string,
arrayToIterate: any[],
}
export const TableSection: React.FC<TableSectionProps> = ({title, arrayToIterate}) => {
return <React.Fragment>
{
arrayToIterate && arrayToIterate.length > 0 ?
<SectionContainer title={title}>
<table>
<tbody>
{arrayToIterate.map(({name, value}, index) => <ViewLine key={index} label={name}
value={value}/>)}
</tbody>
</table>
</SectionContainer> : <span/>
}
</React.Fragment>
}
interface HAREntryPolicySectionProps {
service: string,
title: string,
response: any,
latency?: number,
arrayToIterate: any[],
}
interface HAREntryPolicySectionCollapsibleTitleProps {
label: string;
matched: string;
isExpanded: boolean;
}
const HAREntryPolicySectionCollapsibleTitle: React.FC<HAREntryPolicySectionCollapsibleTitleProps> = ({label, matched, isExpanded}) => {
return <div className={styles.title}>
<span className={`${styles.button} ${isExpanded ? styles.expanded : ''}`}>
{isExpanded ? '-' : '+'}
</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 HAREntryPolicySectionContainerProps {
label: string;
matched: string;
children?: any;
}
export const HAREntryPolicySectionContainer: React.FC<HAREntryPolicySectionContainerProps> = ({label, matched, children}) => {
const [expanded, setExpanded] = useState(false);
return <CollapsibleContainer
className={styles.collapsibleContainer}
isExpanded={expanded}
onClick={() => setExpanded(!expanded)}
title={<HAREntryPolicySectionCollapsibleTitle label={label} matched={matched} isExpanded={expanded}/>}
>
{children}
</CollapsibleContainer>
}
export const HAREntryTablePolicySection: React.FC<HAREntryPolicySectionProps> = ({service, title, response, latency, arrayToIterate}) => {
return <React.Fragment>
{arrayToIterate && arrayToIterate.length > 0 ? <>
<SectionContainer title={title}>
<table>
<tbody>
{arrayToIterate.map(({rule, matched}, index) => {
return (<HAREntryPolicySectionContainer key={index} label={rule.Name} matched={matched && (rule.Type === 'latency' ? rule.Latency >= latency : true)? "Success" : "Failure"}>
<>
{rule.Key && <tr className={styles.dataValue}><td><b>Key</b>:</td><td>{rule.Key}</td></tr>}
{rule.Latency && <tr className={styles.dataValue}><td><b>Latency:</b></td> <td>{rule.Latency}</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>{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>}
</>
</HAREntryPolicySectionContainer>)})}
</tbody>
</table>
</SectionContainer>
</> : <span className={styles.noRules}>No rules could be applied to this request.</span>}
</React.Fragment>
}

View File

@ -0,0 +1,6 @@
import React from "react";
export const KafkaEntryDetailsContent: React.FC<any> = ({entryData}) => {
return <></>;
}

View File

@ -0,0 +1,6 @@
import React from "react";
export const KafkaEntryDetailsTitle: React.FC<any> = ({entryData}) => {
return <></>
}

View File

@ -0,0 +1,43 @@
import React, {useState} from "react";
import styles from "../EntryDetailed.module.sass";
import Tabs from "../../UI/Tabs";
import {BodySection, HAREntryTablePolicySection, TableSection} from "../EntrySections";
import {singleEntryToHAR} from "../../../helpers/utils";
const MIME_TYPE_KEY = 'mimeType';
export const RestEntryDetailsContent: React.FC<any> = ({entryData}) => {
const har = singleEntryToHAR(entryData);
const {request, response, timings: {receive}} = har.log.entries[0].entry;
const rulesMatched = har.log.entries[0].rulesMatched
const TABS = [
{tab: 'request'},
{tab: 'response'},
{tab: 'Rules'},
];
const [currentTab, setCurrentTab] = useState(TABS[0].tab);
return <>
<div className={styles.bodyHeader}>
<Tabs tabs={TABS} currentTab={currentTab} onChange={setCurrentTab} leftAligned/>
{request?.url && <a className={styles.endpointURL} href={request.url} target='_blank' rel="noreferrer">{request.url}</a>}
</div>
{currentTab === TABS[0].tab && <>
<TableSection title={'Headers'} arrayToIterate={request.headers}/>
<TableSection title={'Cookies'} arrayToIterate={request.cookies}/>
{request?.postData && <BodySection content={request.postData} encoding={request.postData.comment} contentType={request.postData[MIME_TYPE_KEY]}/>}
<TableSection title={'Query'} arrayToIterate={request.queryString}/>
</>
}
{currentTab === TABS[1].tab && <>
<TableSection title={'Headers'} arrayToIterate={response.headers}/>
<BodySection content={response.content} encoding={response.content?.encoding} contentType={response.content?.mimeType}/>
<TableSection title={'Cookies'} arrayToIterate={response.cookies}/>
</>}
{currentTab === TABS[2].tab && <>
<HAREntryTablePolicySection service={har.log.entries[0].service} title={'Rule'} latency={receive} response={response} arrayToIterate={rulesMatched ? rulesMatched : []}/>
</>}
</>;
}

View File

@ -0,0 +1,27 @@
import React from "react";
import {singleEntryToHAR} from "../../../helpers/utils";
import StatusCode from "../../UI/StatusCode";
import {EndpointPath} from "../../UI/EndpointPath";
const formatSize = (n: number) => n > 1000 ? `${Math.round(n / 1000)}KB` : `${n} B`;
export const RestEntryDetailsTitle: React.FC<any> = ({entryData}) => {
const har = singleEntryToHAR(entryData);
const {log: {entries}} = har;
const {response, request, timings: {receive}} = entries[0].entry;
const {status, statusText, bodySize} = response;
return har && <>
{status && <div style={{marginRight: 8}}>
<StatusCode statusCode={status}/>
</div>}
<div style={{flexGrow: 1, overflow: 'hidden'}}>
<EndpointPath method={request?.method} path={request?.url}/>
</div>
<div style={{margin: "0 18px", opacity: 0.5}}>{formatSize(bodySize)}</div>
<div style={{marginRight: 18, opacity: 0.5}}>{status} {statusText}</div>
<div style={{marginRight: 18, opacity: 0.5}}>{Math.round(receive)}ms</div>
<div style={{opacity: 0.5}}>{'rulesMatched' in entries[0] ? entries[0].rulesMatched?.length : '0'} Rules Applied</div>
</>
}

View File

@ -1,4 +1,4 @@
@import 'variables.module' @import 'src/variables.module'
.row .row
display: flex display: flex
@ -43,20 +43,20 @@
.ruleNumberTextFailure .ruleNumberTextFailure
color: #DB2156 color: #DB2156
font-family: Source Sans Pro; font-family: Source Sans Pro
font-style: normal; font-style: normal
font-weight: 600; font-weight: 600
font-size: 12px; font-size: 12px
line-height: 15px; line-height: 15px
padding-right: 12px padding-right: 12px
.ruleNumberTextSuccess .ruleNumberTextSuccess
color: #219653 color: #219653
font-family: Source Sans Pro; font-family: Source Sans Pro
font-style: normal; font-style: normal
font-weight: 600; font-weight: 600
font-size: 12px; font-size: 12px
line-height: 15px; line-height: 15px
padding-right: 12px padding-right: 12px
.service .service
@ -73,10 +73,11 @@
.timestamp .timestamp
font-size: 12px font-size: 12px
color: $secondary-font-color color: $secondary-font-color
padding-left: 12px
flex-shrink: 0 flex-shrink: 0
width: 145px width: 145px
text-align: left text-align: left
border-left: 1px solid $data-background-color
padding: 6px 0 6px 12px
.endpointServiceContainer .endpointServiceContainer
display: flex display: flex
@ -88,6 +89,12 @@
.directionContainer .directionContainer
display: flex display: flex
border-right: 1px solid $data-background-color padding: 4px 12px 4px 4px
padding: 4px
padding-right: 12px .icon
height: 14px
width: 50px
padding: 5px
background-color: white
border-radius: 15px
box-shadow: 1px 1px 9px -4px black

View File

@ -0,0 +1,85 @@
import React from "react";
import styles from './EntryListItem.module.sass';
import restIcon from '../assets/restIcon.svg';
import kafkaIcon from '../assets/kafkaIcon.svg';
import {RestEntry, RestEntryContent} from "./RestEntryContent";
import {KafkaEntry, KafkaEntryContent} from "./KafkaEntryContent";
export interface BaseEntry {
type: string;
timestamp: Date;
id: string;
rules: Rules;
latency: number;
}
interface Rules {
status: boolean;
latency: number;
numberOfRules: number;
}
interface EntryProps {
entry: RestEntry | KafkaEntry | any;
setFocusedEntry: (entry: RestEntry | KafkaEntry) => void;
isSelected?: boolean;
}
export enum EntryType {
Rest = "rest",
Kafka = "kafka"
}
export const EntryItem: React.FC<EntryProps> = ({entry, setFocusedEntry, isSelected}) => {
let additionalRulesProperties = "";
let rule = 'latency' in entry.rules
if (rule) {
if (entry.rules.latency !== -1) {
if (entry.rules.latency >= entry.latency) {
additionalRulesProperties = styles.ruleSuccessRow
} else {
additionalRulesProperties = styles.ruleFailureRow
}
if (isSelected) {
additionalRulesProperties += ` ${entry.rules.latency >= entry.latency ? styles.ruleSuccessRowSelected : styles.ruleFailureRowSelected}`
}
} else {
if (entry.rules.status) {
additionalRulesProperties = styles.ruleSuccessRow
} else {
additionalRulesProperties = styles.ruleFailureRow
}
if (isSelected) {
additionalRulesProperties += ` ${entry.rules.status ? styles.ruleSuccessRowSelected : styles.ruleFailureRowSelected}`
}
}
}
let icon, content;
switch (entry.type) {
case EntryType.Rest:
content = <RestEntryContent entry={entry}/>;
icon = restIcon;
break;
case EntryType.Kafka:
content = <KafkaEntryContent entry={entry}/>;
icon = kafkaIcon;
break;
default:
content = <RestEntryContent entry={entry}/>;
icon = restIcon;
break;
}
return <>
<div id={entry.id} className={`${styles.row} ${isSelected ? styles.rowSelected : additionalRulesProperties}`}
onClick={() => setFocusedEntry(entry)}>
{icon && <div style={{width: 80}}>{<img className={styles.icon} alt="icon" src={icon}/>}</div>}
{content}
<div className={styles.timestamp}>{new Date(+entry.timestamp)?.toLocaleString()}</div>
</div>
</>
};

View File

@ -0,0 +1,15 @@
import {BaseEntry} from "./EntryListItem";
import React from "react";
export interface KafkaEntry extends BaseEntry{
}
interface KafkaEntryContentProps {
entry: KafkaEntry;
}
export const KafkaEntryContent: React.FC<KafkaEntryContentProps> = ({entry}) => {
return <>
</>
}

View File

@ -0,0 +1,82 @@
import React from "react";
import StatusCode, {getClassification, StatusCodeClassification} from "../UI/StatusCode";
import ingoingIconSuccess from "../assets/ingoing-traffic-success.svg";
import outgoingIconSuccess from "../assets/outgoing-traffic-success.svg";
import ingoingIconFailure from "../assets/ingoing-traffic-failure.svg";
import outgoingIconFailure from "../assets/outgoing-traffic-failure.svg";
import ingoingIconNeutral from "../assets/ingoing-traffic-neutral.svg";
import outgoingIconNeutral from "../assets/outgoing-traffic-neutral.svg";
import styles from "./EntryListItem.module.sass";
import {EndpointPath} from "../UI/EndpointPath";
import {BaseEntry} from "./EntryListItem";
export interface RestEntry extends BaseEntry{
method?: string,
path: string,
service: string,
statusCode?: number;
url?: string;
isCurrentRevision?: boolean;
isOutgoing?: boolean;
}
interface RestEntryContentProps {
entry: RestEntry;
}
export const RestEntryContent: React.FC<RestEntryContentProps> = ({entry}) => {
const classification = getClassification(entry.statusCode)
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 ruleSuccess: boolean;
let rule = 'latency' in entry.rules
if (rule) {
if (entry.rules.latency !== -1) {
ruleSuccess = entry.rules.latency >= entry.latency;
} else {
ruleSuccess = entry.rules.status;
}
}
return <>
{entry.statusCode && <div>
<StatusCode statusCode={entry.statusCode}/>
</div>}
<div className={styles.endpointServiceContainer}>
<EndpointPath method={entry.method} path={entry.path}/>
<div className={styles.service}>
{entry.service}
</div>
</div>
{rule && <div className={`${ruleSuccess ? styles.ruleNumberTextSuccess : styles.ruleNumberTextFailure}`}>
{`Rules (${numberOfRules})`}
</div>}
<div className={styles.directionContainer}>
{entry.isOutgoing ?
<img src={outgoingIcon} alt="outgoing traffic" title="outgoing"/>
:
<img src={ingoingIcon} alt="ingoing traffic" title="ingoing"/>
}
</div>
</>
}

View File

@ -1,8 +1,8 @@
import React from "react"; import React from "react";
import styles from './style/HarFilters.module.sass'; import styles from './style/Filters.module.sass';
import {HARFilterSelect} from "./HARFilterSelect"; import {FilterSelect} from "./UI/FilterSelect";
import {TextField} from "@material-ui/core"; import {TextField} from "@material-ui/core";
import {ALL_KEY} from "./Select"; import {ALL_KEY} from "./UI/Select";
interface HarFiltersProps { interface HarFiltersProps {
methodsFilter: Array<string>; methodsFilter: Array<string>;
@ -13,7 +13,7 @@ interface HarFiltersProps {
setPathFilter: (val: string) => void; setPathFilter: (val: string) => void;
} }
export const HarFilters: React.FC<HarFiltersProps> = ({methodsFilter, setMethodsFilter, statusFilter, setStatusFilter, pathFilter, setPathFilter}) => { export const Filters: React.FC<HarFiltersProps> = ({methodsFilter, setMethodsFilter, statusFilter, setStatusFilter, pathFilter, setPathFilter}) => {
return <div className={styles.container}> return <div className={styles.container}>
<MethodFilter methodsFilter={methodsFilter} setMethodsFilter={setMethodsFilter}/> <MethodFilter methodsFilter={methodsFilter} setMethodsFilter={setMethodsFilter}/>
@ -59,7 +59,7 @@ const MethodFilter: React.FC<MethodFilterProps> = ({methodsFilter, setMethodsFil
} }
return <FilterContainer> return <FilterContainer>
<HARFilterSelect <FilterSelect
items={Object.values(HTTPMethod)} items={Object.values(HTTPMethod)}
allowMultiple={true} allowMultiple={true}
value={methodsFilter} value={methodsFilter}
@ -91,7 +91,7 @@ const StatusTypesFilter: React.FC<StatusTypesFilterProps> = ({statusFilter, setS
} }
return <FilterContainer> return <FilterContainer>
<HARFilterSelect <FilterSelect
items={Object.values(StatusType)} items={Object.values(StatusType)}
allowMultiple={true} allowMultiple={true}
value={statusFilter} value={statusFilter}

View File

@ -1,116 +0,0 @@
import React from "react";
import styles from './style/HarEntry.module.sass';
import StatusCode, {getClassification, StatusCodeClassification} from "./StatusCode";
import {EndpointPath} from "./EndpointPath";
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"
interface HAREntry {
method?: string,
path: string,
service: string,
id: string,
statusCode?: number;
url?: string;
isCurrentRevision?: boolean;
timestamp: Date;
isOutgoing?: boolean;
latency: number;
rules: Rules;
}
interface Rules {
status: boolean;
latency: number;
numberOfRules: number;
}
interface HAREntryProps {
entry: HAREntry;
setFocusedEntryId: (id: string) => void;
isSelected?: boolean;
}
export const HarEntry: React.FC<HAREntryProps> = ({entry, setFocusedEntryId, isSelected}) => {
const classification = getClassification(entry.statusCode)
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: boolean;
let rule = 'latency' in entry.rules
if (rule) {
if (entry.rules.latency !== -1) {
if (entry.rules.latency >= entry.latency) {
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}`
}
}
}
return <>
<div id={entry.id} className={`${styles.row} ${isSelected && !rule ? styles.rowSelected : additionalRulesProperties}`} onClick={() => setFocusedEntryId(entry.id)}>
{entry.statusCode && <div>
<StatusCode statusCode={entry.statusCode}/>
</div>}
<div className={styles.endpointServiceContainer}>
<EndpointPath method={entry.method} path={entry.path}/>
<div className={styles.service}>
{entry.service}
</div>
</div>
{
rule ?
<div className={`${ruleSuccess ? styles.ruleNumberTextSuccess : styles.ruleNumberTextFailure}`}>
{`Rules (${numberOfRules})`}
</div>
: ""
}
<div className={styles.directionContainer}>
{entry.isOutgoing ?
<img src={outgoingIcon} alt="outgoing traffic" title="outgoing"/>
:
<img src={ingoingIcon} alt="ingoing traffic" title="ingoing"/>
}
</div>
<div className={styles.timestamp}>{new Date(+entry.timestamp)?.toLocaleString()}</div>
</div>
</>
};

View File

@ -1,61 +0,0 @@
import React from "react";
import {singleEntryToHAR} from "./utils";
import styles from './style/HarEntryDetailed.module.sass';
import HAREntryViewer from "./HarEntryViewer/HAREntryViewer";
import {makeStyles} from "@material-ui/core";
import StatusCode from "./StatusCode";
import {EndpointPath} from "./EndpointPath";
const useStyles = makeStyles(() => ({
entryTitle: {
display: 'flex',
minHeight: 46,
maxHeight: 46,
alignItems: 'center',
marginBottom: 8,
padding: 5,
paddingBottom: 0
}
}));
interface HarEntryDetailedProps {
harEntry: any;
classes?: any;
}
export const formatSize = (n: number) => n > 1000 ? `${Math.round(n / 1000)}KB` : `${n} B`;
const HarEntryTitle: React.FC<any> = ({har}) => {
const classes = useStyles();
const {log: {entries}} = har;
const {response, request, timings: {receive}} = entries[0].entry;
const {status, statusText, bodySize} = response;
return <div className={classes.entryTitle}>
{status && <div style={{marginRight: 8}}>
<StatusCode statusCode={status}/>
</div>}
<div style={{flexGrow: 1, overflow: 'hidden'}}>
<EndpointPath method={request?.method} path={request?.url}/>
</div>
<div style={{margin: "0 18px", opacity: 0.5}}>{formatSize(bodySize)}</div>
<div style={{marginRight: 18, opacity: 0.5}}>{status} {statusText}</div>
<div style={{marginRight: 18, opacity: 0.5}}>{Math.round(receive)}ms</div>
</div>;
};
export const HAREntryDetailed: React.FC<HarEntryDetailedProps> = ({classes, harEntry}) => {
const har = singleEntryToHAR(harEntry);
return <>
{har && <HarEntryTitle har={har}/>}
<>
{har && <HAREntryViewer
harObject={har}
className={classes?.root ?? styles.har}
/>}
</>
</>
};

View File

@ -1,266 +0,0 @@
import styles from "./HAREntrySections.module.sass";
import React, {useState} from "react";
import {SyntaxHighlighter} from "../SyntaxHighlighter/index";
import CollapsibleContainer from "../CollapsibleContainer";
import FancyTextDisplay from "../FancyTextDisplay";
import Checkbox from "../Checkbox";
import ProtobufDecoder from "protobuf-decoder";
var jp = require('jsonpath');
interface HAREntryViewLineProps {
label: string;
value: number | string;
}
const HAREntryViewLine: React.FC<HAREntryViewLineProps> = ({label, value}) => {
return (label && value && <tr className={styles.dataLine}>
<td className={styles.dataKey}>{label}</td>
<td>
<FancyTextDisplay
className={styles.dataValue}
text={value}
applyTextEllipsis={false}
flipped={true}
displayIconOnMouseOver={true}
/>
</td>
</tr>) || null;
}
interface HAREntrySectionCollapsibleTitleProps {
title: string;
isExpanded: boolean;
}
const HAREntrySectionCollapsibleTitle: React.FC<HAREntrySectionCollapsibleTitleProps> = ({title, isExpanded}) => {
return <div className={styles.title}>
<span className={`${styles.button} ${isExpanded ? styles.expanded : ''}`}>
{isExpanded ? '-' : '+'}
</span>
<span>{title}</span>
</div>
}
interface HAREntrySectionContainerProps {
title: string;
}
export const HAREntrySectionContainer: React.FC<HAREntrySectionContainerProps> = ({title, children}) => {
const [expanded, setExpanded] = useState(true);
return <CollapsibleContainer
className={styles.collapsibleContainer}
isExpanded={expanded}
onClick={() => setExpanded(!expanded)}
title={<HAREntrySectionCollapsibleTitle title={title} isExpanded={expanded}/>}
>
{children}
</CollapsibleContainer>
}
interface HAREntryBodySectionProps {
content: any;
encoding?: string;
contentType?: string;
}
export const HAREntryBodySection: React.FC<HAREntryBodySectionProps> = ({
content,
encoding,
contentType,
}) => {
const MAXIMUM_BYTES_TO_HIGHLIGHT = 10000; // The maximum of chars to highlight in body, in case the response can be megabytes
const supportedLanguages = [['html', 'html'], ['json', 'json'], ['application/grpc', 'json']]; // [[indicator, languageToUse],...]
const jsonLikeFormats = ['json'];
const protobufFormats = ['application/grpc'];
const [isWrapped, setIsWrapped] = useState(false);
const formatTextBody = (body): string => {
const chunk = body.slice(0, MAXIMUM_BYTES_TO_HIGHLIGHT);
const bodyBuf = encoding === 'base64' ? atob(chunk) : chunk;
try {
if (jsonLikeFormats.some(format => content?.mimeType?.indexOf(format) > -1)) {
return JSON.stringify(JSON.parse(bodyBuf), null, 2);
} else if (protobufFormats.some(format => content?.mimeType?.indexOf(format) > -1)) {
// Replace all non printable characters (ASCII)
const protobufDecoder = new ProtobufDecoder(bodyBuf, true);
return JSON.stringify(protobufDecoder.decode().toSimple(), null, 2);
}
} catch (error) {
console.error(error);
}
return bodyBuf;
}
const getLanguage = (mimetype) => {
const chunk = content.text?.slice(0, 100);
if (chunk.indexOf('html') > 0 || chunk.indexOf('HTML') > 0) return supportedLanguages[0][1];
const language = supportedLanguages.find(el => (mimetype + contentType).indexOf(el[0]) > -1);
return language ? language[1] : 'default';
}
return <React.Fragment>
{content && content.text?.length > 0 && <HAREntrySectionContainer title='Body'>
<table>
<tbody>
<HAREntryViewLine label={'Mime type'} value={content?.mimeType}/>
<HAREntryViewLine label={'Encoding'} value={encoding}/>
</tbody>
</table>
<div style={{display: 'flex', alignItems: 'center', alignContent: 'center', margin: "5px 0"}} onClick={() => setIsWrapped(!isWrapped)}>
<div style={{paddingTop: 3}}>
<Checkbox checked={isWrapped} onToggle={() => {}}/>
</div>
<span style={{marginLeft: '.5rem'}}>Wrap text</span>
</div>
<SyntaxHighlighter
isWrapped={isWrapped}
code={formatTextBody(content.text)}
language={content?.mimeType ? getLanguage(content.mimeType) : 'default'}
/>
</HAREntrySectionContainer>}
</React.Fragment>
}
interface HAREntrySectionProps {
title: string,
arrayToIterate: any[],
}
export const HAREntryTableSection: React.FC<HAREntrySectionProps> = ({title, arrayToIterate}) => {
return <React.Fragment>
{
arrayToIterate && arrayToIterate.length > 0 ?
<HAREntrySectionContainer title={title}>
<table>
<tbody>
{arrayToIterate.map(({name, value}, index) => <HAREntryViewLine key={index} label={name}
value={value}/>)}
</tbody>
</table>
</HAREntrySectionContainer> : <span/>
}
</React.Fragment>
}
interface HAREntryPolicySectionProps {
service: string,
title: string,
response: any,
latency?: number,
arrayToIterate: any[],
}
interface HAREntryPolicySectionCollapsibleTitleProps {
label: string;
matched: string;
isExpanded: boolean;
}
const HAREntryPolicySectionCollapsibleTitle: React.FC<HAREntryPolicySectionCollapsibleTitleProps> = ({label, matched, isExpanded}) => {
return <div className={styles.title}>
<span className={`${styles.button} ${isExpanded ? styles.expanded : ''}`}>
{isExpanded ? '-' : '+'}
</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 HAREntryPolicySectionContainerProps {
label: string;
matched: string;
children?: any;
}
export const HAREntryPolicySectionContainer: React.FC<HAREntryPolicySectionContainerProps> = ({label, matched, children}) => {
const [expanded, setExpanded] = useState(false);
return <CollapsibleContainer
className={styles.collapsibleContainer}
isExpanded={expanded}
onClick={() => setExpanded(!expanded)}
title={<HAREntryPolicySectionCollapsibleTitle label={label} matched={matched} isExpanded={expanded}/>}
>
{children}
</CollapsibleContainer>
}
export const HAREntryTablePolicySection: React.FC<HAREntryPolicySectionProps> = ({service, title, response, latency, arrayToIterate}) => {
const base64ToJson = response.content.mimeType === "application/json; charset=utf-8" ? JSON.parse(Buffer.from(response.content.text, "base64").toString()) : {};
return <React.Fragment>
{
arrayToIterate && arrayToIterate.length > 0 ?
<>
<HAREntrySectionContainer title={title}>
<table>
<tbody>
{arrayToIterate.map(({rule, matched}, index) => {
return (
<HAREntryPolicySectionContainer key={index} label={rule.Name} matched={matched && (rule.Type === 'latency' ? rule.Latency >= latency : true)? "Success" : "Failure"}>
{
<>
{
rule.Key != "" ?
<tr className={styles.dataValue}><td><b>Key</b>:</td><td>{rule.Key}</td></tr>
: null
}
{
rule.Latency != "" ?
<tr className={styles.dataValue}><td><b>Latency:</b></td> <td>{rule.Latency}</td></tr>
: null
}
{
rule.Method != "" ?
<tr className={styles.dataValue}><td><b>Method:</b></td> <td>{rule.Method}</td></tr>
: null
}
{
rule.Path != "" ?
<tr className={styles.dataValue}><td><b>Path:</b></td> <td>{rule.Path}</td></tr>
: null
}
{
rule.Service != "" ?
<tr className={styles.dataValue}><td><b>Service:</b></td> <td>{service}</td></tr>
: null
}
{
rule.Type != "" ?
<tr className={styles.dataValue}><td><b>Type:</b></td> <td>{rule.Type}</td></tr>
: null
}
{
rule.Value != "" ?
<tr className={styles.dataValue}><td><b>Value:</b></td> <td>{rule.Value}</td></tr>
: null
}
</>
}
</HAREntryPolicySectionContainer>
)
}
)
}
</tbody>
</table>
</HAREntrySectionContainer>
</> : <span className={styles.noRules}>No rules could be applied to this request.</span>
}
</React.Fragment>
}

View File

@ -1,60 +0,0 @@
@import "../style/variables.module"
.harEntry
font-family: "Source Sans Pro", Lucida Grande, Tahoma, sans-serif
height: 100%
width: 100%
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
background: $main-background-color
color: $blue-gray
border-radius: 4px
padding: 10px
.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

@ -1,71 +0,0 @@
import React, {useState} from 'react';
import styles from './HAREntryViewer.module.sass';
import Tabs from "../Tabs";
import {HAREntryTableSection, HAREntryBodySection, HAREntryTablePolicySection} from "./HAREntrySections";
const MIME_TYPE_KEY = 'mimeType';
const HAREntryDisplay: React.FC<any> = ({har, entry, isCollapsed: initialIsCollapsed, isResponseMocked}) => {
const {request, response, timings: {receive}} = entry;
const rulesMatched = har.log.entries[0].rulesMatched
const TABS = [
{tab: 'request'},
{
tab: 'response',
badge: <>{isResponseMocked && <span className="smallBadge virtual mock">MOCK</span>}</>
},
{
tab: 'Rules',
},
];
const [currentTab, setCurrentTab] = useState(TABS[0].tab);
return <div className={styles.harEntry}>
{!initialIsCollapsed && <div className={styles.body}>
<div className={styles.bodyHeader}>
<Tabs tabs={TABS} currentTab={currentTab} onChange={setCurrentTab} leftAligned/>
{request?.url && <a className={styles.endpointURL} href={request.url} target='_blank' rel="noreferrer">{request.url}</a>}
</div>
{
currentTab === TABS[0].tab && <React.Fragment>
<HAREntryTableSection title={'Headers'} arrayToIterate={request.headers}/>
<HAREntryTableSection title={'Cookies'} arrayToIterate={request.cookies}/>
{request?.postData && <HAREntryBodySection content={request.postData} encoding={request.postData.comment} contentType={request.postData[MIME_TYPE_KEY]}/>}
<HAREntryTableSection title={'Query'} arrayToIterate={request.queryString}/>
</React.Fragment>
}
{currentTab === TABS[1].tab && <React.Fragment>
<HAREntryTableSection title={'Headers'} arrayToIterate={response.headers}/>
<HAREntryBodySection content={response.content} encoding={response.content?.encoding} contentType={response.content?.mimeType}/>
<HAREntryTableSection title={'Cookies'} arrayToIterate={response.cookies}/>
</React.Fragment>}
{currentTab === TABS[2].tab && <React.Fragment>
<HAREntryTablePolicySection service={har.log.entries[0].service} title={'Rule'} latency={receive} response={response} arrayToIterate={rulesMatched ? rulesMatched : []}/>
</React.Fragment>}
</div>}
</div>;
}
interface Props {
harObject: any;
className?: string;
isResponseMocked?: boolean;
showTitle?: boolean;
}
const HAREntryViewer: React.FC<Props> = ({harObject, className, isResponseMocked, showTitle=true}) => {
const {log: {entries}} = harObject;
const isCollapsed = entries.length > 1;
return <div className={`${className ? className : ''}`}>
{Object.keys(entries).map((entry: any, index) => <HAREntryDisplay har={harObject} isCollapsed={isCollapsed} key={index} entry={entries[entry].entry} isResponseMocked={isResponseMocked} showTitle={showTitle}/>)}
</div>
};
export default HAREntryViewer;

View File

@ -1,27 +0,0 @@
import prevIcon from "./assets/icon-prev.svg";
import nextIcon from "./assets/icon-next.svg";
import {Box} from "@material-ui/core";
import React from "react";
import styles from './style/HarPaging.module.sass'
import numeral from 'numeral';
interface HarPagingProps {
showPageNumber?: boolean;
}
export const HarPaging: React.FC<HarPagingProps> = ({showPageNumber=false}) => {
return <Box className={styles.HarPaging} display='flex'>
<img src={prevIcon} onClick={() => {
// harStore.data.moveBack(); todo
}} alt="back"/>
{showPageNumber && <span className={styles.text}>
Page <span className={styles.pageNumber}>
{/*{numeral(harStore.data.currentPage).format(0, 0)}*/} //todo
</span>
</span>}
<img src={nextIcon} onClick={() => {
// harStore.data.moveNext(); todo
}} alt="next"/>
</Box>
};

View File

@ -1,14 +1,14 @@
import React, {useEffect, useRef, useState} from "react"; import React, {useEffect, useRef, useState} from "react";
import {HarFilters} from "./HarFilters"; import {Filters} from "./Filters";
import {HarEntriesList} from "./HarEntriesList"; import {EntriesList} from "./EntriesList";
import {makeStyles} from "@material-ui/core"; import {makeStyles} from "@material-ui/core";
import "./style/HarPage.sass"; import "./style/TrafficPage.sass";
import styles from './style/HarEntriesList.module.sass'; import styles from './style/EntriesList.module.sass';
import {HAREntryDetailed} from "./HarEntryDetailed"; import {EntryDetailed} from "./EntryDetailed/EntryDetailed";
import playIcon from './assets/run.svg'; import playIcon from './assets/run.svg';
import pauseIcon from './assets/pause.svg'; import pauseIcon from './assets/pause.svg';
import variables from './style/variables.module.scss'; import variables from '../variables.module.scss';
import {StatusBar} from "./StatusBar"; import {StatusBar} from "./UI/StatusBar";
import Api, {MizuWebsocketURL} from "../helpers/api"; import Api, {MizuWebsocketURL} from "../helpers/api";
const useLayoutStyles = makeStyles(() => ({ const useLayoutStyles = makeStyles(() => ({
@ -43,13 +43,13 @@ interface HarPageProps {
const api = new Api(); const api = new Api();
export const HarPage: React.FC<HarPageProps> = ({setAnalyzeStatus, onTLSDetected}) => { export const TrafficPage: React.FC<HarPageProps> = ({setAnalyzeStatus, onTLSDetected}) => {
const classes = useLayoutStyles(); const classes = useLayoutStyles();
const [entries, setEntries] = useState([] as any); const [entries, setEntries] = useState([] as any);
const [focusedEntryId, setFocusedEntryId] = useState(null); const [focusedEntry, setFocusedEntry] = useState(null);
const [selectedHarEntry, setSelectedHarEntry] = useState(null); const [selectedEntryData, setSelectedEntryData] = useState(null);
const [connection, setConnection] = useState(ConnectionStatus.Closed); const [connection, setConnection] = useState(ConnectionStatus.Closed);
const [noMoreDataTop, setNoMoreDataTop] = useState(false); const [noMoreDataTop, setNoMoreDataTop] = useState(false);
const [noMoreDataBottom, setNoMoreDataBottom] = useState(false); const [noMoreDataBottom, setNoMoreDataBottom] = useState(false);
@ -83,7 +83,7 @@ export const HarPage: React.FC<HarPageProps> = ({setAnalyzeStatus, onTLSDetected
setNoMoreDataBottom(false) setNoMoreDataBottom(false)
return; return;
} }
if (!focusedEntryId) setFocusedEntryId(entry.id) if (!focusedEntry) setFocusedEntry(entry)
let newEntries = [...entries]; let newEntries = [...entries];
if (entries.length === 1000) { if (entries.length === 1000) {
newEntries = newEntries.splice(1); newEntries = newEntries.splice(1);
@ -128,17 +128,17 @@ export const HarPage: React.FC<HarPageProps> = ({setAnalyzeStatus, onTLSDetected
useEffect(() => { useEffect(() => {
if (!focusedEntryId) return; if (!focusedEntry) return;
setSelectedHarEntry(null); setSelectedEntryData(null);
(async () => { (async () => {
try { try {
const entryData = await api.getEntry(focusedEntryId); const entryData = await api.getEntry(focusedEntry.id);
setSelectedHarEntry(entryData); setSelectedEntryData(entryData);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
})() })()
}, [focusedEntryId]) }, [focusedEntry])
const toggleConnection = () => { const toggleConnection = () => {
setConnection(connection === ConnectionStatus.Connected ? ConnectionStatus.Paused : ConnectionStatus.Connected); setConnection(connection === ConnectionStatus.Connected ? ConnectionStatus.Paused : ConnectionStatus.Connected);
@ -172,7 +172,7 @@ export const HarPage: React.FC<HarPageProps> = ({setAnalyzeStatus, onTLSDetected
} }
const isScrollable = (element) => { const isScrollable = (element) => {
return element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight; return element.scrollHeight > element.clientHeight;
}; };
return ( return (
@ -189,35 +189,34 @@ export const HarPage: React.FC<HarPageProps> = ({setAnalyzeStatus, onTLSDetected
</div> </div>
{entries.length > 0 && <div className="HarPage-Container"> {entries.length > 0 && <div className="HarPage-Container">
<div className="HarPage-ListContainer"> <div className="HarPage-ListContainer">
<HarFilters methodsFilter={methodsFilter} <Filters methodsFilter={methodsFilter}
setMethodsFilter={setMethodsFilter} setMethodsFilter={setMethodsFilter}
statusFilter={statusFilter} statusFilter={statusFilter}
setStatusFilter={setStatusFilter} setStatusFilter={setStatusFilter}
pathFilter={pathFilter} pathFilter={pathFilter}
setPathFilter={setPathFilter} setPathFilter={setPathFilter}
/> />
<div className={styles.container}> <div className={styles.container}>
<HarEntriesList entries={entries} <EntriesList entries={entries}
setEntries={setEntries} setEntries={setEntries}
focusedEntryId={focusedEntryId} focusedEntry={focusedEntry}
setFocusedEntryId={setFocusedEntryId} setFocusedEntry={setFocusedEntry}
connectionOpen={connection === ConnectionStatus.Connected} connectionOpen={connection === ConnectionStatus.Connected}
noMoreDataBottom={noMoreDataBottom} noMoreDataBottom={noMoreDataBottom}
setNoMoreDataBottom={setNoMoreDataBottom} setNoMoreDataBottom={setNoMoreDataBottom}
noMoreDataTop={noMoreDataTop} noMoreDataTop={noMoreDataTop}
setNoMoreDataTop={setNoMoreDataTop} setNoMoreDataTop={setNoMoreDataTop}
methodsFilter={methodsFilter} methodsFilter={methodsFilter}
statusFilter={statusFilter} statusFilter={statusFilter}
pathFilter={pathFilter} pathFilter={pathFilter}
listEntryREF={listEntry} listEntryREF={listEntry}
onScrollEvent={onScrollEvent} onScrollEvent={onScrollEvent}
scrollableList={disableScrollList} scrollableList={disableScrollList}
/> />
</div> </div>
</div> </div>
<div className={classes.details}> <div className={classes.details}>
{selectedHarEntry && {selectedEntryData && <EntryDetailed entryData={selectedEntryData} entryType={focusedEntry?.type} classes={{root: classes.harViewer}}/>}
<HAREntryDetailed harEntry={selectedHarEntry} classes={{root: classes.harViewer}}/>}
</div> </div>
</div>} </div>}
{tappingStatus?.pods != null && <StatusBar tappingStatus={tappingStatus}/>} {tappingStatus?.pods != null && <StatusBar tappingStatus={tappingStatus}/>}

View File

@ -1,6 +1,6 @@
import React, {useState} from "react"; import React, {useState} from "react";
import collapsedImg from "./assets/collapsed.svg"; import collapsedImg from "../assets/collapsed.svg";
import expandedImg from "./assets/expanded.svg"; import expandedImg from "../assets/expanded.svg";
import "./style/CollapsibleContainer.sass"; import "./style/CollapsibleContainer.sass";
interface Props { interface Props {

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard'; import { CopyToClipboard } from 'react-copy-to-clipboard';
import duplicateImg from "./assets/duplicate.svg"; import duplicateImg from "../assets/duplicate.svg";
import './style/FancyTextDisplay.sass'; import './style/FancyTextDisplay.sass';
interface Props { interface Props {

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { MenuItem } from '@material-ui/core'; import { MenuItem } from '@material-ui/core';
import style from './style/HARFilterSelect.module.sass'; import style from './style/FilterSelect.module.sass';
import { Select, SelectProps } from "./Select"; import { Select, SelectProps } from "./Select";
interface HARFilterSelectProps extends SelectProps { interface HARFilterSelectProps extends SelectProps {
@ -12,7 +12,7 @@ interface HARFilterSelectProps extends SelectProps {
transformDisplay?: (string) => string; transformDisplay?: (string) => string;
} }
export const HARFilterSelect: React.FC<HARFilterSelectProps> = ({items, value, onChange, label, allowMultiple= false, transformDisplay}) => { export const FilterSelect: React.FC<HARFilterSelectProps> = ({items, value, onChange, label, allowMultiple= false, transformDisplay}) => {
return <Select return <Select
value={value} value={value}
multiple={allowMultiple} multiple={allowMultiple}

View File

@ -1,4 +1,4 @@
import {ReactComponent as DefaultIconDown} from './assets/default_icon_down.svg'; import {ReactComponent as DefaultIconDown} from '../assets/default_icon_down.svg';
import {MenuItem, Select as MUISelect} from '@material-ui/core'; import {MenuItem, Select as MUISelect} from '@material-ui/core';
import React from 'react'; import React from 'react';
import {SelectProps as MUISelectProps} from '@material-ui/core/Select/Select'; import {SelectProps as MUISelectProps} from '@material-ui/core/Select/Select';

View File

@ -1,7 +1,7 @@
import Tooltip from "./Tooltip"; import Tooltip from "./Tooltip";
import React from "react"; import React from "react";
import {makeStyles} from '@material-ui/core/styles'; import {makeStyles} from '@material-ui/core/styles';
import variables from './style/variables.module.scss'; import variables from '../../variables.module.scss';
interface Tab { interface Tab {
tab: string, tab: string,

View File

@ -1,4 +1,4 @@
@import 'variables.module.scss' @import 'src/variables.module'
.statusBar .statusBar
position: absolute position: absolute

View File

@ -1,4 +1,4 @@
@import 'variables.module' @import 'src/variables.module'
.base .base
border-radius: 4px border-radius: 4px

View File

@ -1,4 +1,4 @@
@import 'variables.module' @import 'src/variables.module'
.protocol .protocol
border-radius: 4px border-radius: 4px

View File

Before

Width:  |  Height:  |  Size: 301 B

After

Width:  |  Height:  |  Size: 301 B

View File

@ -0,0 +1,16 @@
<svg width="97" height="29" viewBox="0 0 97 29" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M35.4823 21H39.2623L33.1223 13.24L38.9223 7H35.3223L29.1223 13.54V7H25.9023V21H29.1223V17.46L31.0023 15.5L35.4823 21Z" fill="#090B14"/>
<path d="M50.542 21H53.942L47.682 7H44.482L38.242 21H41.562L42.802 18H49.302L50.542 21ZM43.842 15.54L46.062 10.18L48.282 15.54H43.842Z" fill="#090B14"/>
<path d="M65.9745 9.6V7H55.3945V21H58.6345V15.9H65.1145V13.3H58.6345V9.6H65.9745Z" fill="#090B14"/>
<path d="M77.748 21H81.528L75.388 13.24L81.188 7H77.588L71.388 13.54V7H68.168V21H71.388V17.46L73.268 15.5L77.748 21Z" fill="#090B14"/>
<path d="M92.8077 21H96.2077L89.9477 7H86.7477L80.5077 21H83.8277L85.0677 18H91.5677L92.8077 21ZM86.1077 15.54L88.3277 10.18L90.5477 15.54H86.1077Z" fill="#090B14"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.33333 2.07143C3.41286 2.07143 2.66667 2.84427 2.66667 3.79762C2.66667 4.75097 3.41286 5.52381 4.33333 5.52381C5.25381 5.52381 6 4.75097 6 3.79762C6 2.84427 5.25381 2.07143 4.33333 2.07143ZM0.666667 3.79762C0.666667 1.70025 2.30829 0 4.33333 0C6.35838 0 8 1.70025 8 3.79762C8 5.89499 6.35838 7.59524 4.33333 7.59524C2.30829 7.59524 0.666667 5.89499 0.666667 3.79762Z" fill="#090B14"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.33333 23.4762C3.41286 23.4762 2.66667 24.249 2.66667 25.2024C2.66667 26.1557 3.41286 26.9286 4.33333 26.9286C5.25381 26.9286 6 26.1557 6 25.2024C6 24.249 5.25381 23.4762 4.33333 23.4762ZM0.666667 25.2024C0.666667 23.105 2.30829 21.4048 4.33333 21.4048C6.35838 21.4048 8 23.105 8 25.2024C8 27.2997 6.35838 29 4.33333 29C2.30829 29 0.666667 27.2997 0.666667 25.2024Z" fill="#090B14"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.33333 12.0833C3.04467 12.0833 2 13.1653 2 14.5C2 15.8347 3.04467 16.9167 4.33333 16.9167C5.622 16.9167 6.66667 15.8347 6.66667 14.5C6.66667 13.1653 5.622 12.0833 4.33333 12.0833ZM0 14.5C0 12.0213 1.9401 10.0119 4.33333 10.0119C6.72657 10.0119 8.66667 12.0213 8.66667 14.5C8.66667 16.9787 6.72657 18.9881 4.33333 18.9881C1.9401 18.9881 0 16.9787 0 14.5Z" fill="#090B14"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.3333 7.25C12.4129 7.25 11.6667 8.02284 11.6667 8.97619C11.6667 9.92954 12.4129 10.7024 13.3333 10.7024C14.2538 10.7024 15 9.92954 15 8.97619C15 8.02284 14.2538 7.25 13.3333 7.25ZM9.66667 8.97619C9.66667 6.87882 11.3083 5.17857 13.3333 5.17857C15.3584 5.17857 17 6.87882 17 8.97619C17 11.0736 15.3584 12.7738 13.3333 12.7738C11.3083 12.7738 9.66667 11.0736 9.66667 8.97619Z" fill="#090B14"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.3333 18.2976C12.4129 18.2976 11.6667 19.0705 11.6667 20.0238C11.6667 20.9772 12.4129 21.75 13.3333 21.75C14.2538 21.75 15 20.9772 15 20.0238C15 19.0705 14.2538 18.2976 13.3333 18.2976ZM9.66667 20.0238C9.66667 17.9264 11.3083 16.2262 13.3333 16.2262C15.3584 16.2262 17 17.9264 17 20.0238C17 22.1212 15.3584 23.8214 13.3333 23.8214C11.3083 23.8214 9.66667 22.1212 9.66667 20.0238Z" fill="#090B14"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.66667 10.3571V6.55952H5V10.3571H3.66667Z" fill="#090B14"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.33336 11.9107L10.5088 10.0119L11.1755 11.2078L8.00003 13.1067L7.33336 11.9107Z" fill="#090B14"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.00003 15.881L11.1755 17.7798L10.5088 18.9757L7.33336 17.0769L8.00003 15.881Z" fill="#090B14"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.66667 22.4405V18.6429H5V22.4405H3.66667Z" fill="#090B14"/>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -1,4 +1,4 @@
@import "variables.module" @import "src/variables.module"
.list .list
overflow: scroll overflow: scroll

View File

@ -1,4 +1,4 @@
@import "variables.module" @import "src/variables.module"
.container .container
display: flex display: flex

View File

@ -1,7 +0,0 @@
.loader
margin: 30px auto 0
.har
display: flex
overflow: scroll
height: calc(100% - 1.75rem)

View File

@ -1,16 +0,0 @@
.HarPaging
justify-content: center
align-items: center
padding-bottom: 10px
img
cursor: pointer
.text
color: #8f9bb2
font-size: 14px
padding: 0 10px
.pageNumber
color: #fff
font-weight: 600

View File

@ -1,4 +1,4 @@
@import 'variables.module.scss' @import 'src/variables.module'
.HarPage .HarPage
width: 100% width: 100%

View File

@ -3,7 +3,7 @@ import * as axios from "axios";
const mizuAPIPathPrefix = "/mizu"; const mizuAPIPathPrefix = "/mizu";
// When working locally (with npm run start) change to: // When working locally (with npm run start) change to:
// export const MizuWebsocketURL = `ws://localhost:8899${mizuAPIPathPrefix}/ws`; // export const MizuWebsocketURL = `ws://localhost:8899/ws`;
export const MizuWebsocketURL = `ws://${window.location.host}${mizuAPIPathPrefix}/ws`; export const MizuWebsocketURL = `ws://${window.location.host}${mizuAPIPathPrefix}/ws`;
export default class Api { export default class Api {
@ -11,7 +11,7 @@ export default class Api {
constructor() { constructor() {
// When working locally (with npm run start) change to: // When working locally (with npm run start) change to:
// const apiURL = `http://localhost:8899/${mizuAPIPathPrefix}/api/`; // const apiURL = `http://localhost:8899/api/`;
const apiURL = `${window.location.origin}${mizuAPIPathPrefix}/api/`; const apiURL = `${window.location.origin}${mizuAPIPathPrefix}/api/`;
this.client = axios.create({ this.client = axios.create({

View File

@ -1,10 +0,0 @@
import {useState} from "react";
export default function useToggle(initialState: boolean = false): [boolean, () => void] {
const [isToggled, setToggled] = useState(initialState);
return [isToggled, () => {
setToggled(!isToggled)
}];
}

View File

@ -1,4 +1,4 @@
@import 'src/components/style/variables.module' @import 'src/variables.module'
html, html,
body body