diff --git a/README.md b/README.md index e540401a4..6993e029b 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,7 @@ Web interface is now available at http://localhost:8899 ^C ``` + Any request that contains `User-Agent` header with one of the specified values (`kube-probe` or `prometheus`) will not be captured ### 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. + +## How to Run local UI + +- run from mizu/agent `go run main.go --hars-read --hars-dir ` + +- 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` diff --git a/agent/main.go b/agent/main.go index f87cba902..8f0ffd4d6 100644 --- a/agent/main.go +++ b/agent/main.go @@ -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 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 harsReaderMode = flag.Bool("hars-read", false, "Run in hars-read mode") +var harsDir = flag.String("hars-dir", "", "Directory to read hars from") func main() { flag.Parse() hostMode := os.Getenv(shared.HostModeEnvVar) == "1" 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 { @@ -77,6 +80,13 @@ func main() { go api.StartReadingEntries(filteredHarChannel, nil) 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) diff --git a/ui/src/App.sass b/ui/src/App.sass index 629011eac..0b409a236 100644 --- a/ui/src/App.sass +++ b/ui/src/App.sass @@ -1,4 +1,4 @@ -@import 'components/style/variables.module' +@import 'src/variables.module' .mizuApp background-color: $main-background-color diff --git a/ui/src/App.tsx b/ui/src/App.tsx index c30d09083..fa5b8153c 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -2,8 +2,8 @@ import React, {useEffect, useState} from 'react'; import './App.sass'; import logo from './components/assets/Mizu-logo.svg'; import {Button, Snackbar} from "@material-ui/core"; -import {HarPage} from "./components/HarPage"; -import Tooltip from "./components/Tooltip"; +import {TrafficPage} from "./components/TrafficPage"; +import Tooltip from "./components/UI/Tooltip"; import {makeStyles} from "@material-ui/core/styles"; import MuiAlert from '@material-ui/lab/Alert'; import Api from "./helpers/api"; @@ -38,6 +38,7 @@ const App = () => { } })(); + // eslint-disable-next-line }, []); const onTLSDetected = (destAddress: string) => { @@ -116,7 +117,7 @@ const App = () => { } - + 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. diff --git a/ui/src/components/HarEntriesList.tsx b/ui/src/components/EntriesList.tsx similarity index 80% rename from ui/src/components/HarEntriesList.tsx rename to ui/src/components/EntriesList.tsx index 9ba52450f..31c2634f3 100644 --- a/ui/src/components/HarEntriesList.tsx +++ b/ui/src/components/EntriesList.tsx @@ -1,17 +1,17 @@ -import {HarEntry} from "./HarEntry"; -import React, {useCallback, useEffect, useMemo, useState} from "react"; -import styles from './style/HarEntriesList.module.sass'; +import {EntryItem} from "./EntryListItem/EntryListItem"; +import React, {useCallback, useEffect, useMemo, useRef, useState} from "react"; +import styles from './style/EntriesList.module.sass'; import spinner from './assets/spinner.svg'; import ScrollableFeed from "react-scrollable-feed"; -import {StatusType} from "./HarFilters"; +import {StatusType} from "./Filters"; import Api from "../helpers/api"; -import uninon from "./assets/union.svg"; +import down from "./assets/downImg.svg"; interface HarEntriesListProps { entries: any[]; setEntries: (entries: any[]) => void; - focusedEntryId: string; - setFocusedEntryId: (id: string) => void; + focusedEntry: any; + setFocusedEntry: (entry: any) => void; connectionOpen: boolean; noMoreDataTop: boolean; setNoMoreDataTop: (flag: boolean) => void; @@ -32,11 +32,12 @@ enum FetchOperator { const api = new Api(); -export const HarEntriesList: React.FC = ({entries, setEntries, focusedEntryId, setFocusedEntryId, connectionOpen, noMoreDataTop, setNoMoreDataTop, noMoreDataBottom, setNoMoreDataBottom, methodsFilter, statusFilter, pathFilter, listEntryREF, onScrollEvent, scrollableList}) => { +export const EntriesList: React.FC = ({entries, setEntries, focusedEntry, setFocusedEntry, connectionOpen, noMoreDataTop, setNoMoreDataTop, noMoreDataBottom, setNoMoreDataBottom, methodsFilter, statusFilter, pathFilter, listEntryREF, onScrollEvent, scrollableList}) => { const [loadMoreTop, setLoadMoreTop] = useState(false); const [isLoadingTop, setIsLoadingTop] = useState(false); - + const scrollableRef = useRef(null); + useEffect(() => { const list = document.getElementById('list').firstElementChild; list.addEventListener('scroll', (e) => { @@ -110,28 +111,24 @@ export const HarEntriesList: React.FC = ({entries, setEntri return <>
-
+
{isLoadingTop &&
spinner
} - onScrollEvent(isAtBottom)}> + onScrollEvent(isAtBottom)}> {noMoreDataTop && !connectionOpen &&
No more data available
} - {filteredEntries.map(entry => )} + setFocusedEntry = {setFocusedEntry} + isSelected={focusedEntry.id === entry.id}/>)} {!connectionOpen && !noMoreDataBottom &&
getNewEntries()}>Fetch more entries
}
diff --git a/ui/src/components/EntryDetailed/EntryDetailed.module.sass b/ui/src/components/EntryDetailed/EntryDetailed.module.sass new file mode 100644 index 000000000..2af3d6a54 --- /dev/null +++ b/ui/src/components/EntryDetailed/EntryDetailed.module.sass @@ -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 \ No newline at end of file diff --git a/ui/src/components/EntryDetailed/EntryDetailed.tsx b/ui/src/components/EntryDetailed/EntryDetailed.tsx new file mode 100644 index 000000000..0db1d1a6a --- /dev/null +++ b/ui/src/components/EntryDetailed/EntryDetailed.tsx @@ -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 = ({classes, entryData, entryType}) => { + const classesTitle = useStyles(); + + let title, content; + + switch (entryType) { + case EntryType.Rest: + title = ; + content = ; + break; + case EntryType.Kafka: + title = ; + content = ; + break; + default: + title = ; + content = ; + break; + } + + return <> +
{title}
+
+
+ {content} +
+
+ +}; \ No newline at end of file diff --git a/ui/src/components/HarEntryViewer/HAREntrySections.module.sass b/ui/src/components/EntryDetailed/EntrySections.module.sass similarity index 98% rename from ui/src/components/HarEntryViewer/HAREntrySections.module.sass rename to ui/src/components/EntryDetailed/EntrySections.module.sass index 06c19f302..f6d73bc7c 100644 --- a/ui/src/components/HarEntryViewer/HAREntrySections.module.sass +++ b/ui/src/components/EntryDetailed/EntrySections.module.sass @@ -1,4 +1,4 @@ -@import '../style/variables.module' +@import 'src/variables.module' .title display: flex diff --git a/ui/src/components/EntryDetailed/EntrySections.tsx b/ui/src/components/EntryDetailed/EntrySections.tsx new file mode 100644 index 000000000..3efd01955 --- /dev/null +++ b/ui/src/components/EntryDetailed/EntrySections.tsx @@ -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 = ({label, value}) => { + return (label && value && + {label} + + + + ) || null; +} + +interface SectionCollapsibleTitleProps { + title: string; + isExpanded: boolean; +} + +const SectionCollapsibleTitle: React.FC = ({title, isExpanded}) => { + return
+ + {isExpanded ? '-' : '+'} + + {title} +
+} + +interface SectionContainerProps { + title: string; +} + +export const SectionContainer: React.FC = ({title, children}) => { + const [expanded, setExpanded] = useState(true); + return setExpanded(!expanded)} + title={} + > + {children} + +} + +interface BodySectionProps { + content: any; + encoding?: string; + contentType?: string; +} + +export const BodySection: React.FC = ({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 + {content && content.text?.length > 0 && + + + + + +
+ +
setIsWrapped(!isWrapped)}> +
+ {}}/> +
+ Wrap text +
+ + +
} +
+} + +interface TableSectionProps { + title: string, + arrayToIterate: any[], +} + +export const TableSection: React.FC = ({title, arrayToIterate}) => { + return + { + arrayToIterate && arrayToIterate.length > 0 ? + + + + {arrayToIterate.map(({name, value}, index) => )} + +
+
: + } +
+} + +interface HAREntryPolicySectionProps { + service: string, + title: string, + response: any, + latency?: number, + arrayToIterate: any[], +} + + +interface HAREntryPolicySectionCollapsibleTitleProps { + label: string; + matched: string; + isExpanded: boolean; +} + +const HAREntryPolicySectionCollapsibleTitle: React.FC = ({label, matched, isExpanded}) => { + return
+ + {isExpanded ? '-' : '+'} + + + + {label} + {matched} + + +
+} + +interface HAREntryPolicySectionContainerProps { + label: string; + matched: string; + children?: any; +} + +export const HAREntryPolicySectionContainer: React.FC = ({label, matched, children}) => { + const [expanded, setExpanded] = useState(false); + return setExpanded(!expanded)} + title={} + > + {children} + +} + +export const HAREntryTablePolicySection: React.FC = ({service, title, response, latency, arrayToIterate}) => { + return + {arrayToIterate && arrayToIterate.length > 0 ? <> + + + + {arrayToIterate.map(({rule, matched}, index) => { + return (= latency : true)? "Success" : "Failure"}> + <> + {rule.Key && } + {rule.Latency && } + {rule.Method && } + {rule.Path && } + {rule.Service && } + {rule.Type && } + {rule.Value && } + + )})} + +
Key:{rule.Key}
Latency: {rule.Latency}
Method: {rule.Method}
Path: {rule.Path}
Service: {service}
Type: {rule.Type}
Value: {rule.Value}
+
+ : No rules could be applied to this request.} +
+} \ No newline at end of file diff --git a/ui/src/components/EntryDetailed/Kafka/KafkaEntryDetailsContent.tsx b/ui/src/components/EntryDetailed/Kafka/KafkaEntryDetailsContent.tsx new file mode 100644 index 000000000..7fe97954c --- /dev/null +++ b/ui/src/components/EntryDetailed/Kafka/KafkaEntryDetailsContent.tsx @@ -0,0 +1,6 @@ +import React from "react"; + +export const KafkaEntryDetailsContent: React.FC = ({entryData}) => { + + return <>; +} diff --git a/ui/src/components/EntryDetailed/Kafka/KafkaEntryDetailsTitle.tsx b/ui/src/components/EntryDetailed/Kafka/KafkaEntryDetailsTitle.tsx new file mode 100644 index 000000000..4d1aeee2f --- /dev/null +++ b/ui/src/components/EntryDetailed/Kafka/KafkaEntryDetailsTitle.tsx @@ -0,0 +1,6 @@ +import React from "react"; + +export const KafkaEntryDetailsTitle: React.FC = ({entryData}) => { + + return <> +} \ No newline at end of file diff --git a/ui/src/components/EntryDetailed/Rest/RestEntryDetailsContent.tsx b/ui/src/components/EntryDetailed/Rest/RestEntryDetailsContent.tsx new file mode 100644 index 000000000..fe00f15a0 --- /dev/null +++ b/ui/src/components/EntryDetailed/Rest/RestEntryDetailsContent.tsx @@ -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 = ({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 <> +
+ + {request?.url && {request.url}} +
+ {currentTab === TABS[0].tab && <> + + + {request?.postData && } + + + } + {currentTab === TABS[1].tab && <> + + + + } + {currentTab === TABS[2].tab && <> + + } + ; +} diff --git a/ui/src/components/EntryDetailed/Rest/RestEntryDetailsTitle.tsx b/ui/src/components/EntryDetailed/Rest/RestEntryDetailsTitle.tsx new file mode 100644 index 000000000..e10aa1cf3 --- /dev/null +++ b/ui/src/components/EntryDetailed/Rest/RestEntryDetailsTitle.tsx @@ -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 = ({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 &&
+ +
} +
+ +
+
{formatSize(bodySize)}
+
{status} {statusText}
+
{Math.round(receive)}ms
+
{'rulesMatched' in entries[0] ? entries[0].rulesMatched?.length : '0'} Rules Applied
+ +} \ No newline at end of file diff --git a/ui/src/components/style/HarEntry.module.sass b/ui/src/components/EntryListItem/EntryListItem.module.sass similarity index 74% rename from ui/src/components/style/HarEntry.module.sass rename to ui/src/components/EntryListItem/EntryListItem.module.sass index 6a20447ff..23a5421df 100644 --- a/ui/src/components/style/HarEntry.module.sass +++ b/ui/src/components/EntryListItem/EntryListItem.module.sass @@ -1,4 +1,4 @@ -@import 'variables.module' +@import 'src/variables.module' .row display: flex @@ -43,20 +43,20 @@ .ruleNumberTextFailure color: #DB2156 - font-family: Source Sans Pro; - font-style: normal; - font-weight: 600; - font-size: 12px; - line-height: 15px; + font-family: Source Sans Pro + font-style: normal + font-weight: 600 + font-size: 12px + line-height: 15px padding-right: 12px .ruleNumberTextSuccess color: #219653 - font-family: Source Sans Pro; - font-style: normal; - font-weight: 600; - font-size: 12px; - line-height: 15px; + font-family: Source Sans Pro + font-style: normal + font-weight: 600 + font-size: 12px + line-height: 15px padding-right: 12px .service @@ -73,10 +73,11 @@ .timestamp font-size: 12px color: $secondary-font-color - padding-left: 12px flex-shrink: 0 width: 145px text-align: left + border-left: 1px solid $data-background-color + padding: 6px 0 6px 12px .endpointServiceContainer display: flex @@ -88,6 +89,12 @@ .directionContainer display: flex - border-right: 1px solid $data-background-color - padding: 4px - padding-right: 12px + padding: 4px 12px 4px 4px + +.icon + height: 14px + width: 50px + padding: 5px + background-color: white + border-radius: 15px + box-shadow: 1px 1px 9px -4px black \ No newline at end of file diff --git a/ui/src/components/EntryListItem/EntryListItem.tsx b/ui/src/components/EntryListItem/EntryListItem.tsx new file mode 100644 index 000000000..c66528e18 --- /dev/null +++ b/ui/src/components/EntryListItem/EntryListItem.tsx @@ -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 = ({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 = ; + icon = restIcon; + break; + case EntryType.Kafka: + content = ; + icon = kafkaIcon; + break; + default: + content = ; + icon = restIcon; + break; + } + + return <> +
setFocusedEntry(entry)}> + {icon &&
{icon}
} + {content} +
{new Date(+entry.timestamp)?.toLocaleString()}
+
+ +}; + diff --git a/ui/src/components/EntryListItem/KafkaEntryContent.tsx b/ui/src/components/EntryListItem/KafkaEntryContent.tsx new file mode 100644 index 000000000..b461aef35 --- /dev/null +++ b/ui/src/components/EntryListItem/KafkaEntryContent.tsx @@ -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 = ({entry}) => { + + return <> + +} \ No newline at end of file diff --git a/ui/src/components/EntryListItem/RestEntryContent.tsx b/ui/src/components/EntryListItem/RestEntryContent.tsx new file mode 100644 index 000000000..fb51bff87 --- /dev/null +++ b/ui/src/components/EntryListItem/RestEntryContent.tsx @@ -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 = ({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 &&
+ +
} +
+ +
+ {entry.service} +
+
+ {rule &&
+ {`Rules (${numberOfRules})`} +
} +
+ {entry.isOutgoing ? + outgoing traffic + : + ingoing traffic + } +
+ +} \ No newline at end of file diff --git a/ui/src/components/HarFilters.tsx b/ui/src/components/Filters.tsx similarity index 90% rename from ui/src/components/HarFilters.tsx rename to ui/src/components/Filters.tsx index 5dee2b564..39430a2cb 100644 --- a/ui/src/components/HarFilters.tsx +++ b/ui/src/components/Filters.tsx @@ -1,8 +1,8 @@ import React from "react"; -import styles from './style/HarFilters.module.sass'; -import {HARFilterSelect} from "./HARFilterSelect"; +import styles from './style/Filters.module.sass'; +import {FilterSelect} from "./UI/FilterSelect"; import {TextField} from "@material-ui/core"; -import {ALL_KEY} from "./Select"; +import {ALL_KEY} from "./UI/Select"; interface HarFiltersProps { methodsFilter: Array; @@ -13,7 +13,7 @@ interface HarFiltersProps { setPathFilter: (val: string) => void; } -export const HarFilters: React.FC = ({methodsFilter, setMethodsFilter, statusFilter, setStatusFilter, pathFilter, setPathFilter}) => { +export const Filters: React.FC = ({methodsFilter, setMethodsFilter, statusFilter, setStatusFilter, pathFilter, setPathFilter}) => { return
@@ -59,7 +59,7 @@ const MethodFilter: React.FC = ({methodsFilter, setMethodsFil } return - = ({statusFilter, setS } return - void; - isSelected?: boolean; -} - -export const HarEntry: React.FC = ({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 <> -
setFocusedEntryId(entry.id)}> - {entry.statusCode &&
- -
} -
- -
- {entry.service} -
-
- { - rule ? -
- {`Rules (${numberOfRules})`} -
- : "" - } -
- {entry.isOutgoing ? - outgoing traffic - : - ingoing traffic - } -
-
{new Date(+entry.timestamp)?.toLocaleString()}
-
- -}; diff --git a/ui/src/components/HarEntryDetailed.tsx b/ui/src/components/HarEntryDetailed.tsx deleted file mode 100644 index 82b6f029b..000000000 --- a/ui/src/components/HarEntryDetailed.tsx +++ /dev/null @@ -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 = ({har}) => { - const classes = useStyles(); - - const {log: {entries}} = har; - const {response, request, timings: {receive}} = entries[0].entry; - const {status, statusText, bodySize} = response; - - - return
- {status &&
- -
} -
- -
-
{formatSize(bodySize)}
-
{status} {statusText}
-
{Math.round(receive)}ms
-
; -}; - -export const HAREntryDetailed: React.FC = ({classes, harEntry}) => { - const har = singleEntryToHAR(harEntry); - - return <> - {har && } - <> - {har && } - - -}; diff --git a/ui/src/components/HarEntryViewer/HAREntrySections.tsx b/ui/src/components/HarEntryViewer/HAREntrySections.tsx deleted file mode 100644 index 3b5bd1d87..000000000 --- a/ui/src/components/HarEntryViewer/HAREntrySections.tsx +++ /dev/null @@ -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 = ({label, value}) => { - return (label && value && - {label} - - - - ) || null; -} - - -interface HAREntrySectionCollapsibleTitleProps { - title: string; - isExpanded: boolean; -} - -const HAREntrySectionCollapsibleTitle: React.FC = ({title, isExpanded}) => { - return
- - {isExpanded ? '-' : '+'} - - {title} -
-} - -interface HAREntrySectionContainerProps { - title: string; -} - -export const HAREntrySectionContainer: React.FC = ({title, children}) => { - const [expanded, setExpanded] = useState(true); - return setExpanded(!expanded)} - title={} - > - {children} - -} - -interface HAREntryBodySectionProps { - content: any; - encoding?: string; - contentType?: string; -} - -export const HAREntryBodySection: React.FC = ({ - 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 - {content && content.text?.length > 0 && - - - - - -
- -
setIsWrapped(!isWrapped)}> -
- {}}/> -
- Wrap text -
- - -
} -
-} - -interface HAREntrySectionProps { - title: string, - arrayToIterate: any[], -} - -export const HAREntryTableSection: React.FC = ({title, arrayToIterate}) => { - return - { - arrayToIterate && arrayToIterate.length > 0 ? - - - - {arrayToIterate.map(({name, value}, index) => )} - -
-
: - } -
-} - - - -interface HAREntryPolicySectionProps { - service: string, - title: string, - response: any, - latency?: number, - arrayToIterate: any[], -} - - -interface HAREntryPolicySectionCollapsibleTitleProps { - label: string; - matched: string; - isExpanded: boolean; -} - -const HAREntryPolicySectionCollapsibleTitle: React.FC = ({label, matched, isExpanded}) => { - return
- - {isExpanded ? '-' : '+'} - - - - {label} - {matched} - - -
-} - -interface HAREntryPolicySectionContainerProps { - label: string; - matched: string; - children?: any; -} - -export const HAREntryPolicySectionContainer: React.FC = ({label, matched, children}) => { - const [expanded, setExpanded] = useState(false); - return setExpanded(!expanded)} - title={} - > - {children} - -} - -export const HAREntryTablePolicySection: React.FC = ({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 - { - arrayToIterate && arrayToIterate.length > 0 ? - <> - - - - {arrayToIterate.map(({rule, matched}, index) => { - - - return ( - = latency : true)? "Success" : "Failure"}> - { - - <> - { - rule.Key != "" ? - - : null - } - { - rule.Latency != "" ? - - : null - } - { - rule.Method != "" ? - - : null - } - { - rule.Path != "" ? - - : null - } - { - rule.Service != "" ? - - : null - } - { - rule.Type != "" ? - - : null - } - { - rule.Value != "" ? - - : null - } - - } - - - - ) - } - ) - } - -
Key:{rule.Key}
Latency: {rule.Latency}
Method: {rule.Method}
Path: {rule.Path}
Service: {service}
Type: {rule.Type}
Value: {rule.Value}
-
- - : No rules could be applied to this request. - } -
-} \ No newline at end of file diff --git a/ui/src/components/HarEntryViewer/HAREntryViewer.module.sass b/ui/src/components/HarEntryViewer/HAREntryViewer.module.sass deleted file mode 100644 index 1dc344781..000000000 --- a/ui/src/components/HarEntryViewer/HAREntryViewer.module.sass +++ /dev/null @@ -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 \ No newline at end of file diff --git a/ui/src/components/HarEntryViewer/HAREntryViewer.tsx b/ui/src/components/HarEntryViewer/HAREntryViewer.tsx deleted file mode 100644 index e0450e1e7..000000000 --- a/ui/src/components/HarEntryViewer/HAREntryViewer.tsx +++ /dev/null @@ -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 = ({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 && MOCK} - }, - { - tab: 'Rules', - }, - ]; - - const [currentTab, setCurrentTab] = useState(TABS[0].tab); - - return
- - {!initialIsCollapsed &&
-
- - {request?.url && {request.url}} -
- { - currentTab === TABS[0].tab && - - - - - {request?.postData && } - - - - } - {currentTab === TABS[1].tab && - - - - - - } - {currentTab === TABS[2].tab && - - } -
} -
; -} - -interface Props { - harObject: any; - className?: string; - isResponseMocked?: boolean; - showTitle?: boolean; -} - -const HAREntryViewer: React.FC = ({harObject, className, isResponseMocked, showTitle=true}) => { - const {log: {entries}} = harObject; - const isCollapsed = entries.length > 1; - return
- {Object.keys(entries).map((entry: any, index) => )} -
-}; - -export default HAREntryViewer; diff --git a/ui/src/components/HarPaging.tsx b/ui/src/components/HarPaging.tsx deleted file mode 100644 index 7fb410bee..000000000 --- a/ui/src/components/HarPaging.tsx +++ /dev/null @@ -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 = ({showPageNumber=false}) => { - - return - { - // harStore.data.moveBack(); todo - }} alt="back"/> - {showPageNumber && - Page - {/*{numeral(harStore.data.currentPage).format(0, 0)}*/} //todo - - } - { - // harStore.data.moveNext(); todo - }} alt="next"/> - -}; \ No newline at end of file diff --git a/ui/src/components/HarPage.tsx b/ui/src/components/TrafficPage.tsx similarity index 71% rename from ui/src/components/HarPage.tsx rename to ui/src/components/TrafficPage.tsx index 3cf42d5ca..8ecfd46f0 100644 --- a/ui/src/components/HarPage.tsx +++ b/ui/src/components/TrafficPage.tsx @@ -1,14 +1,14 @@ import React, {useEffect, useRef, useState} from "react"; -import {HarFilters} from "./HarFilters"; -import {HarEntriesList} from "./HarEntriesList"; +import {Filters} from "./Filters"; +import {EntriesList} from "./EntriesList"; import {makeStyles} from "@material-ui/core"; -import "./style/HarPage.sass"; -import styles from './style/HarEntriesList.module.sass'; -import {HAREntryDetailed} from "./HarEntryDetailed"; +import "./style/TrafficPage.sass"; +import styles from './style/EntriesList.module.sass'; +import {EntryDetailed} from "./EntryDetailed/EntryDetailed"; import playIcon from './assets/run.svg'; import pauseIcon from './assets/pause.svg'; -import variables from './style/variables.module.scss'; -import {StatusBar} from "./StatusBar"; +import variables from '../variables.module.scss'; +import {StatusBar} from "./UI/StatusBar"; import Api, {MizuWebsocketURL} from "../helpers/api"; const useLayoutStyles = makeStyles(() => ({ @@ -43,13 +43,13 @@ interface HarPageProps { const api = new Api(); -export const HarPage: React.FC = ({setAnalyzeStatus, onTLSDetected}) => { +export const TrafficPage: React.FC = ({setAnalyzeStatus, onTLSDetected}) => { const classes = useLayoutStyles(); const [entries, setEntries] = useState([] as any); - const [focusedEntryId, setFocusedEntryId] = useState(null); - const [selectedHarEntry, setSelectedHarEntry] = useState(null); + const [focusedEntry, setFocusedEntry] = useState(null); + const [selectedEntryData, setSelectedEntryData] = useState(null); const [connection, setConnection] = useState(ConnectionStatus.Closed); const [noMoreDataTop, setNoMoreDataTop] = useState(false); const [noMoreDataBottom, setNoMoreDataBottom] = useState(false); @@ -83,7 +83,7 @@ export const HarPage: React.FC = ({setAnalyzeStatus, onTLSDetected setNoMoreDataBottom(false) return; } - if (!focusedEntryId) setFocusedEntryId(entry.id) + if (!focusedEntry) setFocusedEntry(entry) let newEntries = [...entries]; if (entries.length === 1000) { newEntries = newEntries.splice(1); @@ -128,17 +128,17 @@ export const HarPage: React.FC = ({setAnalyzeStatus, onTLSDetected useEffect(() => { - if (!focusedEntryId) return; - setSelectedHarEntry(null); + if (!focusedEntry) return; + setSelectedEntryData(null); (async () => { try { - const entryData = await api.getEntry(focusedEntryId); - setSelectedHarEntry(entryData); + const entryData = await api.getEntry(focusedEntry.id); + setSelectedEntryData(entryData); } catch (error) { console.error(error); } })() - }, [focusedEntryId]) + }, [focusedEntry]) const toggleConnection = () => { setConnection(connection === ConnectionStatus.Connected ? ConnectionStatus.Paused : ConnectionStatus.Connected); @@ -172,7 +172,7 @@ export const HarPage: React.FC = ({setAnalyzeStatus, onTLSDetected } const isScrollable = (element) => { - return element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight; + return element.scrollHeight > element.clientHeight; }; return ( @@ -189,35 +189,34 @@ export const HarPage: React.FC = ({setAnalyzeStatus, onTLSDetected
{entries.length > 0 &&
-
-
- {selectedHarEntry && - } + {selectedEntryData && }
} {tappingStatus?.pods != null && } diff --git a/ui/src/components/Checkbox.tsx b/ui/src/components/UI/Checkbox.tsx similarity index 100% rename from ui/src/components/Checkbox.tsx rename to ui/src/components/UI/Checkbox.tsx diff --git a/ui/src/components/CollapsibleContainer.tsx b/ui/src/components/UI/CollapsibleContainer.tsx similarity index 95% rename from ui/src/components/CollapsibleContainer.tsx rename to ui/src/components/UI/CollapsibleContainer.tsx index aad6b1552..4c0452623 100644 --- a/ui/src/components/CollapsibleContainer.tsx +++ b/ui/src/components/UI/CollapsibleContainer.tsx @@ -1,6 +1,6 @@ import React, {useState} from "react"; -import collapsedImg from "./assets/collapsed.svg"; -import expandedImg from "./assets/expanded.svg"; +import collapsedImg from "../assets/collapsed.svg"; +import expandedImg from "../assets/expanded.svg"; import "./style/CollapsibleContainer.sass"; interface Props { diff --git a/ui/src/components/EndpointPath.tsx b/ui/src/components/UI/EndpointPath.tsx similarity index 100% rename from ui/src/components/EndpointPath.tsx rename to ui/src/components/UI/EndpointPath.tsx diff --git a/ui/src/components/FancyTextDisplay.tsx b/ui/src/components/UI/FancyTextDisplay.tsx similarity index 97% rename from ui/src/components/FancyTextDisplay.tsx rename to ui/src/components/UI/FancyTextDisplay.tsx index c61a85bd5..91f10f4bf 100644 --- a/ui/src/components/FancyTextDisplay.tsx +++ b/ui/src/components/UI/FancyTextDisplay.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { CopyToClipboard } from 'react-copy-to-clipboard'; -import duplicateImg from "./assets/duplicate.svg"; +import duplicateImg from "../assets/duplicate.svg"; import './style/FancyTextDisplay.sass'; interface Props { diff --git a/ui/src/components/HARFilterSelect.tsx b/ui/src/components/UI/FilterSelect.tsx similarity index 79% rename from ui/src/components/HARFilterSelect.tsx rename to ui/src/components/UI/FilterSelect.tsx index c4bc51804..a2247b6d8 100644 --- a/ui/src/components/HARFilterSelect.tsx +++ b/ui/src/components/UI/FilterSelect.tsx @@ -1,6 +1,6 @@ import React from "react"; 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"; interface HARFilterSelectProps extends SelectProps { @@ -12,7 +12,7 @@ interface HARFilterSelectProps extends SelectProps { transformDisplay?: (string) => string; } -export const HARFilterSelect: React.FC = ({items, value, onChange, label, allowMultiple= false, transformDisplay}) => { +export const FilterSelect: React.FC = ({items, value, onChange, label, allowMultiple= false, transformDisplay}) => { return