diff --git a/README.md b/README.md index 4e84d10a3..7d74038a0 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,6 @@ 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 @@ -160,15 +159,3 @@ 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 8f0ffd4d6..f87cba902 100644 --- a/agent/main.go +++ b/agent/main.go @@ -26,17 +26,14 @@ 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 && !*harsReaderMode{ - panic("One of the flags --tap, --api or --standalone or --hars-read must be provided") + if !*tapperMode && !*apiServerMode && !*standaloneMode { + panic("One of the flags --tap, --api or --standalone must be provided") } if *standaloneMode { @@ -80,13 +77,6 @@ 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 0b409a236..629011eac 100644 --- a/ui/src/App.sass +++ b/ui/src/App.sass @@ -1,4 +1,4 @@ -@import 'src/variables.module' +@import 'components/style/variables.module' .mizuApp background-color: $main-background-color diff --git a/ui/src/App.tsx b/ui/src/App.tsx index fa5b8153c..c30d09083 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 {TrafficPage} from "./components/TrafficPage"; -import Tooltip from "./components/UI/Tooltip"; +import {HarPage} from "./components/HarPage"; +import Tooltip from "./components/Tooltip"; import {makeStyles} from "@material-ui/core/styles"; import MuiAlert from '@material-ui/lab/Alert'; import Api from "./helpers/api"; @@ -38,7 +38,6 @@ const App = () => { } })(); - // eslint-disable-next-line }, []); const onTLSDetected = (destAddress: string) => { @@ -117,7 +116,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/UI/Checkbox.tsx b/ui/src/components/Checkbox.tsx similarity index 100% rename from ui/src/components/UI/Checkbox.tsx rename to ui/src/components/Checkbox.tsx diff --git a/ui/src/components/UI/CollapsibleContainer.tsx b/ui/src/components/CollapsibleContainer.tsx similarity index 95% rename from ui/src/components/UI/CollapsibleContainer.tsx rename to ui/src/components/CollapsibleContainer.tsx index 4c0452623..aad6b1552 100644 --- a/ui/src/components/UI/CollapsibleContainer.tsx +++ b/ui/src/components/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/UI/EndpointPath.tsx b/ui/src/components/EndpointPath.tsx similarity index 100% rename from ui/src/components/UI/EndpointPath.tsx rename to ui/src/components/EndpointPath.tsx diff --git a/ui/src/components/EntryDetailed/EntryDetailed.module.sass b/ui/src/components/EntryDetailed/EntryDetailed.module.sass deleted file mode 100644 index 2af3d6a54..000000000 --- a/ui/src/components/EntryDetailed/EntryDetailed.module.sass +++ /dev/null @@ -1,23 +0,0 @@ -@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 deleted file mode 100644 index 0db1d1a6a..000000000 --- a/ui/src/components/EntryDetailed/EntryDetailed.tsx +++ /dev/null @@ -1,56 +0,0 @@ -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/EntryDetailed/EntrySections.tsx b/ui/src/components/EntryDetailed/EntrySections.tsx deleted file mode 100644 index c6546f0a4..000000000 --- a/ui/src/components/EntryDetailed/EntrySections.tsx +++ /dev/null @@ -1,213 +0,0 @@ -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 > 0 ? : ''} - {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 deleted file mode 100644 index 7fe97954c..000000000 --- a/ui/src/components/EntryDetailed/Kafka/KafkaEntryDetailsContent.tsx +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index 4d1aeee2f..000000000 --- a/ui/src/components/EntryDetailed/Kafka/KafkaEntryDetailsTitle.tsx +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index fe00f15a0..000000000 --- a/ui/src/components/EntryDetailed/Rest/RestEntryDetailsContent.tsx +++ /dev/null @@ -1,43 +0,0 @@ -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 deleted file mode 100644 index 3d9925505..000000000 --- a/ui/src/components/EntryDetailed/Rest/RestEntryDetailsTitle.tsx +++ /dev/null @@ -1,26 +0,0 @@ -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
- -} \ No newline at end of file diff --git a/ui/src/components/EntryListItem/EntryListItem.tsx b/ui/src/components/EntryListItem/EntryListItem.tsx deleted file mode 100644 index 4a8a8a017..000000000 --- a/ui/src/components/EntryListItem/EntryListItem.tsx +++ /dev/null @@ -1,85 +0,0 @@ -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 deleted file mode 100644 index b461aef35..000000000 --- a/ui/src/components/EntryListItem/KafkaEntryContent.tsx +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index fb51bff87..000000000 --- a/ui/src/components/EntryListItem/RestEntryContent.tsx +++ /dev/null @@ -1,82 +0,0 @@ -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/UI/FancyTextDisplay.tsx b/ui/src/components/FancyTextDisplay.tsx similarity index 97% rename from ui/src/components/UI/FancyTextDisplay.tsx rename to ui/src/components/FancyTextDisplay.tsx index 91f10f4bf..c61a85bd5 100644 --- a/ui/src/components/UI/FancyTextDisplay.tsx +++ b/ui/src/components/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/UI/FilterSelect.tsx b/ui/src/components/HARFilterSelect.tsx similarity index 79% rename from ui/src/components/UI/FilterSelect.tsx rename to ui/src/components/HARFilterSelect.tsx index a2247b6d8..c4bc51804 100644 --- a/ui/src/components/UI/FilterSelect.tsx +++ b/ui/src/components/HARFilterSelect.tsx @@ -1,6 +1,6 @@ import React from "react"; import { MenuItem } from '@material-ui/core'; -import style from './style/FilterSelect.module.sass'; +import style from './style/HARFilterSelect.module.sass'; import { Select, SelectProps } from "./Select"; interface HARFilterSelectProps extends SelectProps { @@ -12,7 +12,7 @@ interface HARFilterSelectProps extends SelectProps { transformDisplay?: (string) => string; } -export const FilterSelect: React.FC = ({items, value, onChange, label, allowMultiple= false, transformDisplay}) => { +export const HARFilterSelect: React.FC = ({items, value, onChange, label, allowMultiple= false, transformDisplay}) => { return