+
+
+ {label}
+
+
= ({title, color, isExpanded}) => {
return
-
+
{isExpanded ? '-' : '+'}
-
+
{title}
}
@@ -62,15 +87,19 @@ export const EntrySectionContainer: React.FC = ({tit
interface EntryBodySectionProps {
content: any,
color: string,
+ updateQuery: any,
encoding?: string,
contentType?: string,
+ selector?: string,
}
export const EntryBodySection: React.FC = ({
color,
+ updateQuery,
content,
encoding,
contentType,
+ selector,
}) => {
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],...]
@@ -107,8 +136,8 @@ export const EntryBodySection: React.FC = ({
{content && content?.length > 0 &&
@@ -132,17 +161,23 @@ interface EntrySectionProps {
title: string,
color: string,
arrayToIterate: any[],
+ updateQuery: any,
}
-export const EntryTableSection: React.FC = ({title, color, arrayToIterate}) => {
+export const EntryTableSection: React.FC = ({title, color, arrayToIterate, updateQuery}) => {
return
{
arrayToIterate && arrayToIterate.length > 0 ?
- {arrayToIterate.map(({name, value}, index) => )}
+ {arrayToIterate.map(({name, value, selector}, index) => )}
:
diff --git a/ui/src/components/EntryDetailed/EntryViewer.module.sass b/ui/src/components/EntryDetailed/EntryViewer.module.sass
index 740a4c417..fdc1e7195 100644
--- a/ui/src/components/EntryDetailed/EntryViewer.module.sass
+++ b/ui/src/components/EntryDetailed/EntryViewer.module.sass
@@ -4,6 +4,7 @@
font-family: "Source Sans Pro", Lucida Grande, Tahoma, sans-serif
height: calc(100% - 70px)
width: 100%
+ margin-top: 10px
h3,
h4
diff --git a/ui/src/components/EntryDetailed/EntryViewer.tsx b/ui/src/components/EntryDetailed/EntryViewer.tsx
index dc8b8f4c7..dabf3d5b7 100644
--- a/ui/src/components/EntryDetailed/EntryViewer.tsx
+++ b/ui/src/components/EntryDetailed/EntryViewer.tsx
@@ -8,7 +8,7 @@ enum SectionTypes {
SectionBody = "body",
}
-const SectionsRepresentation: React.FC = ({data, color}) => {
+const SectionsRepresentation: React.FC = ({data, color, updateQuery}) => {
const sections = []
if (data) {
@@ -16,12 +16,12 @@ const SectionsRepresentation: React.FC = ({data, color}) => {
switch (row.type) {
case SectionTypes.SectionTable:
sections.push(
-
+
)
break;
case SectionTypes.SectionBody:
sections.push(
-
+
)
break;
default:
@@ -33,7 +33,7 @@ const SectionsRepresentation: React.FC = ({data, color}) => {
return <>{sections}>;
}
-const AutoRepresentation: React.FC = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color}) => {
+const AutoRepresentation: React.FC = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color, updateQuery}) => {
var TABS = [
{
tab: 'Request'
@@ -85,10 +85,10 @@ const AutoRepresentation: React.FC = ({representation, isRulesEnabled, rule
{currentTab === TABS[0].tab &&
-
+
}
{response && currentTab === TABS[responseTabIndex].tab &&
-
+
}
{isRulesEnabled && currentTab === TABS[rulesTabIndex].tab &&
@@ -110,9 +110,10 @@ interface Props {
contractContent: string;
color: string;
elapsedTime: number;
+ updateQuery: any;
}
-const EntryViewer: React.FC = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color}) => {
+const EntryViewer: React.FC = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color, updateQuery}) => {
return = ({representation, isRulesEnabled, rulesMatc
contractContent={contractContent}
elapsedTime={elapsedTime}
color={color}
+ updateQuery={updateQuery}
/>
};
diff --git a/ui/src/components/EntryListItem/EntryListItem.module.sass b/ui/src/components/EntryListItem/EntryListItem.module.sass
index d8e5295d5..7e396d136 100644
--- a/ui/src/components/EntryListItem/EntryListItem.module.sass
+++ b/ui/src/components/EntryListItem/EntryListItem.module.sass
@@ -19,7 +19,6 @@
.rowSelected
border: 1px $blue-color solid
- margin-right: 3px
.ruleSuccessRow
background: #E8FFF1
@@ -46,13 +45,12 @@
.ruleNumberTextSuccess
color: #219653
-.service
+.resolvedName
text-overflow: ellipsis
overflow: hidden
white-space: nowrap
color: $secondary-font-color
padding-left: 4px
- padding-top: 3px
padding-right: 10px
display: flex
font-size: 12px
@@ -62,7 +60,7 @@
color: $secondary-font-color
padding-left: 12px
flex-shrink: 0
- width: 145px
+ width: 185px
text-align: left
.endpointServiceContainer
@@ -70,7 +68,6 @@
flex-direction: column
overflow: hidden
padding-right: 10px
- padding-left: 10px
flex-grow: 1
.separatorRight
@@ -84,7 +81,14 @@
padding: 4px
padding-left: 12px
-.port
+.tcpInfo
font-size: 12px
color: $secondary-font-color
- margin: 5px
+ margin-top: 5px
+ margin-bottom: 5px
+
+.port
+ margin-right: 5px
+
+.ip
+ margin-left: 5px
diff --git a/ui/src/components/EntryListItem/EntryListItem.tsx b/ui/src/components/EntryListItem/EntryListItem.tsx
index fc418631f..d88575250 100644
--- a/ui/src/components/EntryListItem/EntryListItem.tsx
+++ b/ui/src/components/EntryListItem/EntryListItem.tsx
@@ -1,8 +1,11 @@
import React from "react";
+import Moment from 'moment';
+import SwapHorizIcon from '@material-ui/icons/SwapHoriz';
import styles from './EntryListItem.module.sass';
import StatusCode, {getClassification, StatusCodeClassification} from "../UI/StatusCode";
import Protocol, {ProtocolInterface} from "../UI/Protocol"
-import {EndpointPath} from "../UI/EndpointPath";
+import {Summary} from "../UI/Summary";
+import Queryable from "../UI/Queryable";
import ingoingIconSuccess from "../assets/ingoing-traffic-success.svg"
import ingoingIconFailure from "../assets/ingoing-traffic-failure.svg"
import ingoingIconNeutral from "../assets/ingoing-traffic-neutral.svg"
@@ -10,19 +13,21 @@ 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 TCPInterface {
+ ip: string
+ port: string
+ name: string
+}
+
interface Entry {
protocol: ProtocolInterface,
method?: string,
summary: string,
- service: string,
- id: string,
+ id: number,
statusCode?: number;
- url?: string;
timestamp: Date;
- sourceIp: string,
- sourcePort: string,
- destinationIp: string,
- destinationPort: string,
+ src: TCPInterface,
+ dst: TCPInterface,
isOutgoing?: boolean;
latency: number;
rules: Rules;
@@ -37,12 +42,17 @@ interface Rules {
interface EntryProps {
entry: Entry;
+ focusedEntryId: string;
setFocusedEntryId: (id: string) => void;
- isSelected?: boolean;
style: object;
+ updateQuery: any;
+ headingMode: boolean;
}
-export const EntryItem: React.FC = ({entry, setFocusedEntryId, isSelected, style}) => {
+export const EntryItem: React.FC = ({entry, focusedEntryId, setFocusedEntryId, style, updateQuery, headingMode}) => {
+
+ const isSelected = focusedEntryId === entry.id.toString();
+
const classification = getClassification(entry.statusCode)
const numberOfRules = entry.rules.numberOfRules
let ingoingIcon;
@@ -113,28 +123,66 @@ export const EntryItem: React.FC = ({entry, setFocusedEntryId, isSel
break;
}
+ const isStatusCodeEnabled = ((entry.protocol.name === "http" && "statusCode" in entry) || entry.statusCode !== 0);
+ var endpointServiceContainer = "10px";
+ if (!isStatusCodeEnabled) endpointServiceContainer = "20px";
+
return <>
setFocusedEntryId(entry.id)}
+ onClick={() => {
+ if (!setFocusedEntryId) return;
+ setFocusedEntryId(entry.id.toString());
+ }}
style={{
border: isSelected ? `1px ${entry.protocol.backgroundColor} solid` : "1px transparent solid",
- position: "absolute",
+ position: !headingMode ? "absolute" : "unset",
top: style['top'],
- marginTop: style['marginTop'],
- width: "calc(100% - 25px)",
+ marginTop: !headingMode ? style['marginTop'] : "10px",
+ width: !headingMode ? "calc(100% - 25px)" : "calc(100% - 18px)",
}}
>
-
- {((entry.protocol.name === "http" && "statusCode" in entry) || entry.statusCode !== 0) &&
-
+ {!headingMode ? : null}
+ {isStatusCodeEnabled &&
+
}
-
-
-
-
{entry.service}
+
+
+
+
+
+ {entry.src.name ? entry.src.name : "[Unresolved]"}
+
+
+
+
+
+ {entry.dst.name ? entry.dst.name : "[Unresolved]"}
+
+
{
@@ -152,18 +200,109 @@ export const EntryItem: React.FC
= ({entry, setFocusedEntryId, isSel
: ""
}
-
{entry.sourcePort}
+
+
+ {entry.src.ip}
+
+
+
:
+
+
+ {entry.src.port}
+
+
{entry.isOutgoing ?
-
+
+
+
:
-
+
+ {
+ updateQuery(`outgoing == false`)
+ }}
+ />
+
}
-
{entry.destinationPort}
+
+
+ {entry.dst.ip}
+
+
+
:
+
+
+ {entry.dst.port}
+
+
-
- {new Date(+entry.timestamp)?.toLocaleString()}
-
+ = datetime("${Moment(+entry.timestamp)?.utc().format('MM/DD/YYYY, h:mm:ss.SSS A')}")`}
+ updateQuery={updateQuery}
+ displayIconOnMouseOver={true}
+ flipped={false}
+ >
+
+ {Moment(+entry.timestamp)?.utc().format('MM/DD/YYYY, h:mm:ss.SSS A')}
+
+
>
diff --git a/ui/src/components/Filters.tsx b/ui/src/components/Filters.tsx
index c1795a67a..0afc7325d 100644
--- a/ui/src/components/Filters.tsx
+++ b/ui/src/components/Filters.tsx
@@ -1,137 +1,330 @@
-import React from "react";
+import React, {useRef, useState} from "react";
import styles from './style/Filters.module.sass';
-import {FilterSelect} from "./UI/FilterSelect";
-import {TextField} from "@material-ui/core";
-import {ALL_KEY} from "./UI/Select";
+import {Button, Grid, Modal, Box, Typography, Backdrop, Fade, Divider} from "@material-ui/core";
+import CodeEditor from '@uiw/react-textarea-code-editor';
+import MenuBookIcon from '@material-ui/icons/MenuBook';
+import {SyntaxHighlighter} from "./UI/SyntaxHighlighter/index";
+import filterUIExample1 from "./assets/filter-ui-example-1.png"
+import filterUIExample2 from "./assets/filter-ui-example-2.png"
+import variables from '../variables.module.scss';
interface FiltersProps {
- methodsFilter: Array;
- setMethodsFilter: (methods: Array) => void;
- statusFilter: Array;
- setStatusFilter: (methods: Array) => void;
- pathFilter: string
- setPathFilter: (val: string) => void;
- serviceFilter: string
- setServiceFilter: (val: string) => void;
+ query: string
+ setQuery: any
+ backgroundColor: string
+ ws: any
+ openWebSocket: (query: string, resetEntries: boolean) => void;
}
-export const Filters: React.FC = ({methodsFilter, setMethodsFilter, statusFilter, setStatusFilter, pathFilter, setPathFilter, serviceFilter, setServiceFilter}) => {
-
+export const Filters: React.FC = ({query, setQuery, backgroundColor, ws, openWebSocket}) => {
return ;
};
-const _toUpperCase = v => v.toUpperCase();
+interface QueryFormProps {
+ query: string
+ setQuery: any
+ backgroundColor: string
+ ws: any
+ openWebSocket: (query: string, resetEntries: boolean) => void;
+}
-const FilterContainer: React.FC = ({children}) => {
- return
- {children}
-
;
+const style = {
+ position: 'absolute',
+ top: '10%',
+ left: '50%',
+ transform: 'translate(-50%, 0%)',
+ width: '80vw',
+ bgcolor: 'background.paper',
+ borderRadius: '5px',
+ boxShadow: 24,
+ p: 4,
+ color: '#000',
};
-enum HTTPMethod {
- GET = "get",
- PUT = "put",
- POST = "post",
- DELETE = "delete",
- OPTIONS="options",
- PATCH = "patch"
-}
+export const QueryForm: React.FC = ({query, setQuery, backgroundColor, ws, openWebSocket}) => {
-interface MethodFilterProps {
- methodsFilter: Array;
- setMethodsFilter: (methods: Array) => void;
-}
+ const formRef = useRef(null);
-const MethodFilter: React.FC = ({methodsFilter, setMethodsFilter}) => {
+ const [openModal, setOpenModal] = useState(false);
- const methodClicked = (val) => {
- if(val === ALL_KEY) {
- setMethodsFilter([]);
- return;
- }
- if(methodsFilter.includes(val)) {
- setMethodsFilter(methodsFilter.filter(method => method !== val))
+ const handleOpenModal = () => setOpenModal(true);
+ const handleCloseModal = () => setOpenModal(false);
+
+ const handleChange = async (e) => {
+ setQuery(e.target.value);
+ }
+
+ const handleSubmit = (e) => {
+ ws.close();
+ if (query) {
+ openWebSocket(`(${query}) and leftOff(-1)`, true);
} else {
- setMethodsFilter([...methodsFilter, val]);
+ openWebSocket(`leftOff(-1)`, true);
}
+ e.preventDefault();
}
- return
- methodClicked(val)}
- transformDisplay={_toUpperCase}
- label={"Methods"}
- />
- ;
-};
+ return <>
+
-export enum StatusType {
- SUCCESS = "success",
- ERROR = "error"
+
+
+
+
+ Filtering Guide (Cheatsheet)
+
+
+ Mizu has a rich filtering syntax that let's you query the results both flexibly and efficiently.
+ Here are some examples that you can try;
+
+
+
+
+ This is a simple query that matches to HTTP packets with request path "/catalogue":
+
+
+
+ The same query can be negated for HTTP path and written like this:
+
+
+
+ The syntax supports regular expressions. Here is a query that matches the HTTP requests that send JSON to a server:
+
+
+
+ Here is another query that matches HTTP responses with status code 4xx:
+
+
+
+ The same exact query can be as integer comparison:
+
+ = 400`}
+ language="python"
+ />
+
+ The results can be queried based on their timestamps:
+
+
+
+
+
+
+ Since Mizu supports various protocols like gRPC, AMQP, Kafka and Redis. It's possible to write complex queries that match multiple protocols like this:
+
+
+
+ By clicking the plus icon that appears beside the queryable UI elements on hovering in both left-pane and right-pane, you can automatically select a field and update the query:
+
+
+
+ Such that; clicking this icon in left-pane, would append the query below:
+
+
+
+ Another queriable UI element example, this time from the right-pane:
+
+
+
+ A query that compares one selector to another is also a valid query:
+
+
+
+
+
+
+ There are a few helper methods included the in the filter language* to help building queries more easily.
+
+
+
+ true if the given selector's value starts with the string:
+
+
+
+ true if the given selector's value ends with the string:
+
+
+
+ true if the given selector's value contains the string:
+
+
+
+ returns the UNIX timestamp which is the equivalent of the time that's provided by the string. Invalid input evaluates to false:
+
+ = datetime("10/19/2021, 6:29:02.593 PM")`}
+ language="python"
+ />
+
+ limits the number of records that are streamed back as a result of a query. Always evaluates to true:
+
+
+
+
+
+
+ *The filtering functionality is provided through Basenine database server. Please refer to BFL Syntax Reference for more information.
+
+
+
+
+ >
}
-
-interface StatusTypesFilterProps {
- statusFilter: Array;
- setStatusFilter: (methods: Array) => void;
-}
-
-const StatusTypesFilter: React.FC = ({statusFilter, setStatusFilter}) => {
-
- const statusClicked = (val) => {
- if(val === ALL_KEY) {
- setStatusFilter([]);
- return;
- }
- setStatusFilter([val]);
- }
-
- return
- statusClicked(val)}
- transformDisplay={_toUpperCase}
- label="Status"
- />
- ;
-};
-
-interface PathFilterProps {
- pathFilter: string;
- setPathFilter: (val: string) => void;
-}
-
-const PathFilter: React.FC = ({pathFilter, setPathFilter}) => {
-
- return
- Path
-
- setPathFilter(e.target.value)}/>
-
- ;
-};
-
-interface ServiceFilterProps {
- serviceFilter: string;
- setServiceFilter: (val: string) => void;
-}
-
-const ServiceFilter: React.FC = ({serviceFilter, setServiceFilter}) => {
-
- return
- Service
-
- setServiceFilter(e.target.value)}/>
-
- ;
-};
-
diff --git a/ui/src/components/TrafficPage.tsx b/ui/src/components/TrafficPage.tsx
index f1e3be2b1..0faf7a816 100644
--- a/ui/src/components/TrafficPage.tsx
+++ b/ui/src/components/TrafficPage.tsx
@@ -1,4 +1,4 @@
-import React, {useEffect, useRef, useState} from "react";
+import React, {useEffect, useMemo, useRef, useState} from "react";
import {Filters} from "./Filters";
import {EntriesList} from "./EntriesList";
import {makeStyles} from "@material-ui/core";
@@ -10,6 +10,9 @@ import pauseIcon from './assets/pause.svg';
import variables from '../variables.module.scss';
import {StatusBar} from "./UI/StatusBar";
import Api, {MizuWebsocketURL} from "../helpers/api";
+import { ToastContainer, toast } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css';
+import debounce from 'lodash/debounce';
const useLayoutStyles = makeStyles(() => ({
details: {
@@ -18,7 +21,7 @@ const useLayoutStyles = makeStyles(() => ({
padding: "12px 24px",
borderRadius: 4,
marginTop: 15,
- background: variables.headerBackgoundColor,
+ background: variables.headerBackgroundColor,
},
viewer: {
@@ -34,7 +37,6 @@ const useLayoutStyles = makeStyles(() => ({
enum ConnectionStatus {
Closed,
Connected,
- Paused
}
interface TrafficPageProps {
@@ -52,26 +54,82 @@ export const TrafficPage: React.FC = ({setAnalyzeStatus, onTLS
const [focusedEntryId, setFocusedEntryId] = useState(null);
const [selectedEntryData, setSelectedEntryData] = useState(null);
const [connection, setConnection] = useState(ConnectionStatus.Closed);
- const [noMoreDataTop, setNoMoreDataTop] = useState(false);
- const [noMoreDataBottom, setNoMoreDataBottom] = useState(false);
- const [methodsFilter, setMethodsFilter] = useState([]);
- const [statusFilter, setStatusFilter] = useState([]);
- const [pathFilter, setPathFilter] = useState("");
- const [serviceFilter, setServiceFilter] = useState("");
+ const [noMoreDataTop, setNoMoreDataTop] = useState(false);
const [tappingStatus, setTappingStatus] = useState(null);
- const [disableScrollList, setDisableScrollList] = useState(false);
+ const [isSnappedToBottom, setIsSnappedToBottom] = useState(true);
+
+ const [query, setQuery] = useState("");
+ const [queryBackgroundColor, setQueryBackgroundColor] = useState("#f5f5f5");
+ const [addition, updateQuery] = useState("");
+
+ const [queriedCurrent, setQueriedCurrent] = useState(0);
+ const [queriedTotal, setQueriedTotal] = useState(0);
+ const [leftOffBottom, setLeftOffBottom] = useState(0);
+ const [leftOffTop, setLeftOffTop] = useState(null);
+ const [truncatedTimestamp, setTruncatedTimestamp] = useState(0);
+
+ const [startTime, setStartTime] = useState(0);
+
+ const handleQueryChange = useMemo(() => debounce(async (query: string) => {
+ if (!query) {
+ setQueryBackgroundColor("#f5f5f5")
+ } else {
+ const data = await api.validateQuery(query);
+ if (!data) {
+ return;
+ }
+ if (data.valid) {
+ setQueryBackgroundColor("#d2fad2");
+ } else {
+ setQueryBackgroundColor("#fad6dc");
+ }
+ }
+ }, 500), []) as (query: string) => void;
+
+ useEffect(() => {
+ handleQueryChange(query);
+ }, [query]);
+
+ useEffect(() => {
+ if (query) {
+ setQuery(`${query} and ${addition}`);
+ } else {
+ setQuery(addition);
+ }
+ // eslint-disable-next-line
+ }, [addition]);
const ws = useRef(null);
const listEntry = useRef(null);
- const openWebSocket = () => {
+ const openWebSocket = (query: string, resetEntries: boolean) => {
+ if (resetEntries) {
+ setFocusedEntryId(null);
+ setEntries([]);
+ setQueriedCurrent(0);
+ setLeftOffTop(null);
+ setNoMoreDataTop(false);
+ }
ws.current = new WebSocket(MizuWebsocketURL);
- ws.current.onopen = () => setConnection(ConnectionStatus.Connected);
- ws.current.onclose = () => setConnection(ConnectionStatus.Closed);
+ ws.current.onopen = () => {
+ setConnection(ConnectionStatus.Connected);
+ ws.current.send(query);
+ }
+ ws.current.onclose = () => {
+ setConnection(ConnectionStatus.Closed);
+ }
+ ws.current.onerror = (event) => {
+ console.error("WebSocket error:", event);
+ if (query) {
+ openWebSocket(`(${query}) and leftOff(${leftOffBottom})`, false);
+ } else {
+ openWebSocket(`leftOff(${leftOffBottom})`, false);
+ }
+ }
}
if (ws.current) {
@@ -80,19 +138,15 @@ export const TrafficPage: React.FC = ({setAnalyzeStatus, onTLS
const message = JSON.parse(e.data);
switch (message.messageType) {
case "entry":
- const entry = message.data
- if (connection === ConnectionStatus.Paused) {
- setNoMoreDataBottom(false)
- return;
- }
- if (!focusedEntryId) setFocusedEntryId(entry.id)
- let newEntries = [...entries];
- setEntries([...newEntries, entry])
- if(listEntry.current) {
- if(isScrollable(listEntry.current.firstChild)) {
- setDisableScrollList(true)
- }
+ const entry = message.data;
+ if (!focusedEntryId) setFocusedEntryId(entry.id.toString())
+ const newEntries = [...entries, entry];
+ if (newEntries.length === 10001) {
+ setLeftOffTop(newEntries[0].entry.id);
+ newEntries.shift();
+ setNoMoreDataTop(false);
}
+ setEntries(newEntries);
break
case "status":
setTappingStatus(message.tappingStatus);
@@ -103,6 +157,30 @@ export const TrafficPage: React.FC = ({setAnalyzeStatus, onTLS
case "outboundLink":
onTLSDetected(message.Data.DstIP);
break;
+ case "toast":
+ toast[message.data.type](message.data.text, {
+ position: "bottom-right",
+ theme: "colored",
+ autoClose: message.data.autoClose,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ });
+ break;
+ case "queryMetadata":
+ setQueriedCurrent(queriedCurrent + message.data.current);
+ setQueriedTotal(message.data.total);
+ setLeftOffBottom(message.data.leftOff);
+ setTruncatedTimestamp(message.data.truncatedTimestamp);
+ if (leftOffTop === null) {
+ setLeftOffTop(message.data.leftOff - 1);
+ }
+ break;
+ case "startTime":
+ setStartTime(message.data);
+ break;
default:
console.error(`unsupported websocket message type, Got: ${message.messageType}`)
}
@@ -111,7 +189,7 @@ export const TrafficPage: React.FC = ({setAnalyzeStatus, onTLS
useEffect(() => {
(async () => {
- openWebSocket();
+ openWebSocket("leftOff(-1)", true);
try{
const tapStatusResponse = await api.tapStatus();
setTappingStatus(tapStatusResponse);
@@ -133,20 +211,38 @@ export const TrafficPage: React.FC = ({setAnalyzeStatus, onTLS
const entryData = await api.getEntry(focusedEntryId);
setSelectedEntryData(entryData);
} catch (error) {
+ if (error.response) {
+ toast[error.response.data.type](`Entry[${focusedEntryId}]: ${error.response.data.msg}`, {
+ position: "bottom-right",
+ theme: "colored",
+ autoClose: error.response.data.autoClose,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ });
+ }
console.error(error);
}
- })()
- }, [focusedEntryId])
+ })();
+ // eslint-disable-next-line
+ }, [focusedEntryId]);
const toggleConnection = () => {
- setConnection(connection === ConnectionStatus.Connected ? ConnectionStatus.Paused : ConnectionStatus.Connected);
+ ws.current.close();
+ if (connection !== ConnectionStatus.Connected) {
+ if (query) {
+ openWebSocket(`(${query}) and leftOff(-1)`, true);
+ } else {
+ openWebSocket(`leftOff(-1)`, true);
+ }
+ }
}
const getConnectionStatusClass = (isContainer) => {
const container = isContainer ? "Container" : "";
switch (connection) {
- case ConnectionStatus.Paused:
- return "orangeIndicator" + container;
case ConnectionStatus.Connected:
return "greenIndicator" + container;
default:
@@ -156,28 +252,27 @@ export const TrafficPage: React.FC = ({setAnalyzeStatus, onTLS
const getConnectionTitle = () => {
switch (connection) {
- case ConnectionStatus.Paused:
- return "traffic paused";
case ConnectionStatus.Connected:
- return "connected, waiting for traffic"
+ return "streaming live traffic"
default:
- return "not connected";
+ return "streaming paused";
}
}
- const onScrollEvent = (isAtBottom) => {
- isAtBottom ? setDisableScrollList(false) : setDisableScrollList(true)
+ const onSnapBrokenEvent = () => {
+ setIsSnappedToBottom(false);
+ if (connection === ConnectionStatus.Connected) {
+ ws.current.close();
+ }
}
- const isScrollable = (element) => {
- return element.scrollHeight > element.clientHeight;
- };
-
return (
- {connection !== ConnectionStatus.Closed &&
}
+
+
{getConnectionTitle()}
@@ -185,42 +280,61 @@ export const TrafficPage: React.FC = ({setAnalyzeStatus, onTLS
- {entries.length > 0 &&
+ {
- {selectedEntryData && }
+ {selectedEntryData && }
}
- {tappingStatus?.pods != null &&
}
+ {tappingStatus && }
+
)
};
diff --git a/ui/src/components/UI/EndpointPath.tsx b/ui/src/components/UI/EndpointPath.tsx
deleted file mode 100644
index 2561aab44..000000000
--- a/ui/src/components/UI/EndpointPath.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import miscStyles from "./style/misc.module.sass";
-import React from "react";
-import styles from './style/EndpointPath.module.sass';
-
-interface EndpointPathProps {
- method: string,
- path: string
-}
-
-export const EndpointPath: React.FC
= ({method, path}) => {
- return
- {method &&
{method} }
- {path &&
{path}
}
-
-};
diff --git a/ui/src/components/UI/FancyTextDisplay.tsx b/ui/src/components/UI/FancyTextDisplay.tsx
index 91f10f4bf..10dc49f1c 100644
--- a/ui/src/components/UI/FancyTextDisplay.tsx
+++ b/ui/src/components/UI/FancyTextDisplay.tsx
@@ -17,7 +17,7 @@ interface Props {
const FancyTextDisplay: React.FC = ({text, className, isPossibleToCopy = true, applyTextEllipsis = true, flipped = false, useTooltip= false, displayIconOnMouseOver = false, buttonOnly = false}) => {
const [showCopiedNotification, setCopied] = useState(false);
const [showTooltip, setShowTooltip] = useState(false);
- const displayText = text || '';
+ text = String(text);
const onCopy = () => {
setCopied(true)
@@ -33,12 +33,12 @@ const FancyTextDisplay: React.FC = ({text, className, isPossibleToCopy =
return () => clearTimeout(timer);
}, [showCopiedNotification]);
- const textElement = {displayText} ;
+ const textElement = {text} ;
- const copyButton = isPossibleToCopy && displayText ?
+ const copyButton = isPossibleToCopy && text ?
{showCopiedNotification && Copied }
@@ -48,14 +48,14 @@ const FancyTextDisplay: React.FC = ({text, className, isPossibleToCopy =
return (
setShowTooltip(true)}
onMouseLeave={ e => setShowTooltip(false)}
>
{!buttonOnly && flipped && textElement}
{copyButton}
{!buttonOnly && !flipped && textElement}
- {useTooltip && showTooltip && {displayText} }
+ {useTooltip && showTooltip && {text} }
);
};
diff --git a/ui/src/components/UI/FilterSelect.tsx b/ui/src/components/UI/FilterSelect.tsx
deleted file mode 100644
index bf6764ad0..000000000
--- a/ui/src/components/UI/FilterSelect.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import React from "react";
-import { MenuItem } from '@material-ui/core';
-import style from './style/FilterSelect.module.sass';
-import { Select, SelectProps } from "./Select";
-
-interface FilterSelectProps extends SelectProps {
- items: string[];
- value: string | string[];
- onChange: (string) => void;
- label?: string;
- allowMultiple?: boolean;
- transformDisplay?: (string) => string;
-}
-
-export const FilterSelect: React.FC = ({items, value, onChange, label, allowMultiple= false, transformDisplay}) => {
- return
- {items?.map(item => {item} )}
-
-};
diff --git a/ui/src/components/UI/Protocol.tsx b/ui/src/components/UI/Protocol.tsx
index 0b031bf18..3db0c2eb1 100644
--- a/ui/src/components/UI/Protocol.tsx
+++ b/ui/src/components/UI/Protocol.tsx
@@ -1,10 +1,12 @@
import React from "react";
import styles from './style/Protocol.module.sass';
+import Queryable from "./Queryable";
export interface ProtocolInterface {
name: string
longName: string
- abbreviation: string
+ abbr: string
+ macro: string
backgroundColor: string
foregroundColor: string
fontSize: number
@@ -16,37 +18,51 @@ export interface ProtocolInterface {
interface ProtocolProps {
protocol: ProtocolInterface
horizontal: boolean
+ updateQuery: any
}
-const Protocol: React.FC = ({protocol, horizontal}) => {
+const Protocol: React.FC = ({protocol, horizontal, updateQuery}) => {
if (horizontal) {
- return
-
- {protocol.longName}
-
-
+ return
+
+
+ {protocol.longName}
+
+
+
} else {
- return
+ return
- {protocol.abbreviation}
+ {protocol.abbr}
-
+
}
};
diff --git a/ui/src/components/UI/Queryable.tsx b/ui/src/components/UI/Queryable.tsx
new file mode 100644
index 000000000..e08d36cd9
--- /dev/null
+++ b/ui/src/components/UI/Queryable.tsx
@@ -0,0 +1,62 @@
+import React, { useEffect, useState } from 'react';
+import { CopyToClipboard } from 'react-copy-to-clipboard';
+import AddCircleIcon from '@material-ui/icons/AddCircle';
+import './style/Queryable.sass';
+
+interface Props {
+ query: string,
+ updateQuery: any,
+ style?: object,
+ iconStyle?: object,
+ className?: string,
+ useTooltip?: boolean,
+ displayIconOnMouseOver?: boolean,
+ flipped?: boolean,
+}
+
+const Queryable: React.FC = ({query, updateQuery, style, iconStyle, className, useTooltip= true, displayIconOnMouseOver = false, flipped = false, children}) => {
+ const [showAddedNotification, setAdded] = useState(false);
+ const [showTooltip, setShowTooltip] = useState(false);
+
+ const onCopy = () => {
+ setAdded(true)
+ };
+
+ useEffect(() => {
+ let timer;
+ if (showAddedNotification) {
+ updateQuery(query);
+ timer = setTimeout(() => {
+ setAdded(false);
+ }, 1000);
+ }
+ return () => clearTimeout(timer);
+ }, [showAddedNotification, query, updateQuery]);
+
+ const addButton = query ?
+
+
+ {showAddedNotification && Added }
+
+ : null;
+
+ return (
+ setShowTooltip(true)}
+ onMouseLeave={ e => setShowTooltip(false)}
+ >
+ {flipped && addButton}
+ {children}
+ {!flipped && addButton}
+ {useTooltip && showTooltip && {query} }
+
+ );
+};
+
+export default Queryable;
diff --git a/ui/src/components/UI/StatusBar.tsx b/ui/src/components/UI/StatusBar.tsx
index 5fe6d2738..d8f71b48b 100644
--- a/ui/src/components/UI/StatusBar.tsx
+++ b/ui/src/components/UI/StatusBar.tsx
@@ -1,9 +1,13 @@
import './style/StatusBar.sass';
import React, {useState} from "react";
+import warningIcon from '../assets/warning_icon.svg';
+import failIcon from '../assets/failed.svg';
+import successIcon from '../assets/success.svg';
export interface TappingStatusPod {
name: string;
namespace: string;
+ isTapped: boolean;
}
export interface TappingStatus {
@@ -11,7 +15,7 @@ export interface TappingStatus {
}
export interface Props {
- tappingStatus: TappingStatus
+ tappingStatus: TappingStatusPod[]
}
const pluralize = (noun: string, amount: number) => {
@@ -22,23 +26,29 @@ export const StatusBar: React.FC = ({tappingStatus}) => {
const [expandedBar, setExpandedBar] = useState(false);
- const uniqueNamespaces = Array.from(new Set(tappingStatus.pods.map(pod => pod.namespace)));
- const amountOfPods = tappingStatus.pods.length;
+ const uniqueNamespaces = Array.from(new Set(tappingStatus.map(pod => pod.namespace)));
+ const amountOfPods = tappingStatus.length;
+ const amountOfTappedPods = tappingStatus.filter(pod => pod.isTapped).length;
+ const amountOfUntappedPods = amountOfPods - amountOfTappedPods;
return setExpandedBar(true)} onMouseLeave={() => setExpandedBar(false)}>
-
{`Tapping ${amountOfPods} ${pluralize('pod', amountOfPods)} in ${pluralize('namespace', uniqueNamespaces.length)} ${uniqueNamespaces.join(", ")}`}
+
+ {tappingStatus.some(pod => !pod.isTapped) &&
}
+ {`Tapping ${amountOfUntappedPods > 0 ? amountOfTappedPods + " / " + amountOfPods : amountOfPods} ${pluralize('pod', amountOfPods)} in ${pluralize('namespace', uniqueNamespaces.length)} ${uniqueNamespaces.join(", ")}`}
{expandedBar &&
Pod name
Namespace
+ Tapping
- {tappingStatus.pods.map(pod =>
+ {tappingStatus.map(pod =>
{pod.name}
{pod.namespace}
+
)}
diff --git a/ui/src/components/UI/StatusCode.tsx b/ui/src/components/UI/StatusCode.tsx
index b35788ac1..054be378a 100644
--- a/ui/src/components/UI/StatusCode.tsx
+++ b/ui/src/components/UI/StatusCode.tsx
@@ -1,5 +1,6 @@
import React from "react";
import styles from './style/StatusCode.module.sass';
+import Queryable from "./Queryable";
export enum StatusCodeClassification {
SUCCESS = "success",
@@ -9,17 +10,27 @@ export enum StatusCodeClassification {
interface EntryProps {
statusCode: number
+ updateQuery: any
}
-const StatusCode: React.FC
= ({statusCode}) => {
+const StatusCode: React.FC = ({statusCode, updateQuery}) => {
const classification = getClassification(statusCode)
- return
+ return
+
{statusCode}
-
+
+
};
export function getClassification(statusCode: number): string {
diff --git a/ui/src/components/UI/Summary.tsx b/ui/src/components/UI/Summary.tsx
new file mode 100644
index 000000000..760f0f5f3
--- /dev/null
+++ b/ui/src/components/UI/Summary.tsx
@@ -0,0 +1,38 @@
+import miscStyles from "./style/misc.module.sass";
+import React from "react";
+import styles from './style/Summary.module.sass';
+import Queryable from "./Queryable";
+
+interface SummaryProps {
+ method: string
+ summary: string
+ updateQuery: any
+}
+
+export const Summary: React.FC = ({method, summary, updateQuery}) => {
+
+ return
+ {method &&
+
+ {method}
+
+ }
+ {summary &&
+
+ {summary}
+
+ }
+
+};
diff --git a/ui/src/components/UI/SyntaxHighlighter/highlighterStyle.ts b/ui/src/components/UI/SyntaxHighlighter/highlighterStyle.ts
index a5be67b25..1766e309a 100644
--- a/ui/src/components/UI/SyntaxHighlighter/highlighterStyle.ts
+++ b/ui/src/components/UI/SyntaxHighlighter/highlighterStyle.ts
@@ -112,7 +112,7 @@ export const highlighterStyle = {
"color": "#C6C5FE"
},
"operator": {
- "color": "#EDEDED"
+ "color": "#A1A1A1"
},
"entity": {
"color": "#fdab2b",
diff --git a/ui/src/components/UI/style/EndpointPath.module.sass b/ui/src/components/UI/style/EndpointPath.module.sass
deleted file mode 100644
index 2fc54c0f3..000000000
--- a/ui/src/components/UI/style/EndpointPath.module.sass
+++ /dev/null
@@ -1,8 +0,0 @@
-.container
- display: flex
- align-items: center
-
-.path
- text-overflow: ellipsis
- overflow: hidden
- white-space: nowrap
\ No newline at end of file
diff --git a/ui/src/components/UI/style/Protocol.module.sass b/ui/src/components/UI/style/Protocol.module.sass
index e702f5ae2..9ffeaab14 100644
--- a/ui/src/components/UI/style/Protocol.module.sass
+++ b/ui/src/components/UI/style/Protocol.module.sass
@@ -6,7 +6,6 @@
background-color: #000
color: #fff
margin-left: -8px
- margin-bottom: -4px
.vertical
line-height: 22px
diff --git a/ui/src/components/UI/style/Queryable.sass b/ui/src/components/UI/style/Queryable.sass
new file mode 100644
index 000000000..f045f2183
--- /dev/null
+++ b/ui/src/components/UI/style/Queryable.sass
@@ -0,0 +1,48 @@
+.Queryable-Container
+ display: flex
+ align-items: center
+
+ &.displayIconOnMouseOver
+ .Queryable-Icon
+ opacity: 0
+ width: 0px
+ pointer-events: none
+ &:hover
+ .Queryable-Icon
+ opacity: 1
+ pointer-events: all
+
+
+ .Queryable-Icon
+ height: 22px
+ width: 22px
+ cursor: pointer
+ color: #27AE60
+
+ &:hover
+ background-color: rgba(255, 255, 255, 0.06)
+ border-radius: 4px
+ color: #1E884B
+
+ .Queryable-AddNotifier
+ background-color: #1E884B
+ font-weight: normal
+ padding: 2px 5px
+ border-radius: 4px
+ position: absolute
+ transform: translate(0, 10%)
+ color: white
+ z-index: 1000
+ font-size: 11px
+
+ .Queryable-Tooltip
+ background-color: #1E884B
+ font-weight: normal
+ padding: 2px 5px
+ border-radius: 4px
+ position: absolute
+ transform: translate(0, -80%)
+ color: white
+ z-index: 1000
+ font-size: 11px
+
diff --git a/ui/src/components/UI/style/StatusBar.sass b/ui/src/components/UI/style/StatusBar.sass
index 6a55dbc36..f6e66624c 100644
--- a/ui/src/components/UI/style/StatusBar.sass
+++ b/ui/src/components/UI/style/StatusBar.sass
@@ -24,8 +24,13 @@
padding: 8px
font-weight: 600
+ img
+ margin-right: 10px
+ height: 22px
+
th
text-align: left
+ padding-right: 15px
td
padding-right: 15px
padding-top: 5px
diff --git a/ui/src/components/UI/style/Summary.module.sass b/ui/src/components/UI/style/Summary.module.sass
new file mode 100644
index 000000000..2bce9af67
--- /dev/null
+++ b/ui/src/components/UI/style/Summary.module.sass
@@ -0,0 +1,6 @@
+.container
+ display: flex
+ align-items: center
+
+.summary
+ white-space: nowrap
diff --git a/ui/src/components/UI/style/misc.module.sass b/ui/src/components/UI/style/misc.module.sass
index 8f57e3382..bef317cd3 100644
--- a/ui/src/components/UI/style/misc.module.sass
+++ b/ui/src/components/UI/style/misc.module.sass
@@ -5,20 +5,20 @@
border: solid 1px $secondary-font-color
margin-left: 4px
padding: 2px 5px
- text-transform: uppercase
font-family: "Source Sans Pro", sans-serif
font-size: 11px
font-weight: bold
&.method
margin-right: 10px
+ height: 12px
&.filterPlate
border-color: #bcc6dd20
color: #a0b2ff
font-size: 10px
-.noSelect
+.noSelect
-webkit-touch-callout: none
-webkit-user-select: none
-khtml-user-select: none
diff --git a/ui/src/components/assets/failed.svg b/ui/src/components/assets/failed.svg
new file mode 100644
index 000000000..bab53af12
--- /dev/null
+++ b/ui/src/components/assets/failed.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/components/assets/filter-ui-example-1.png b/ui/src/components/assets/filter-ui-example-1.png
new file mode 100644
index 000000000..6f4d88430
Binary files /dev/null and b/ui/src/components/assets/filter-ui-example-1.png differ
diff --git a/ui/src/components/assets/filter-ui-example-2.png b/ui/src/components/assets/filter-ui-example-2.png
new file mode 100644
index 000000000..e8ae1a5ec
Binary files /dev/null and b/ui/src/components/assets/filter-ui-example-2.png differ
diff --git a/ui/src/components/assets/success.svg b/ui/src/components/assets/success.svg
new file mode 100644
index 000000000..f8fe3aa64
--- /dev/null
+++ b/ui/src/components/assets/success.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/components/assets/warning_icon.svg b/ui/src/components/assets/warning_icon.svg
new file mode 100644
index 000000000..7ef6ba5a2
--- /dev/null
+++ b/ui/src/components/assets/warning_icon.svg
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/components/style/EntriesList.module.sass b/ui/src/components/style/EntriesList.module.sass
index 453f46081..5295dd656 100644
--- a/ui/src/components/style/EntriesList.module.sass
+++ b/ui/src/components/style/EntriesList.module.sass
@@ -47,13 +47,21 @@
text-align: center
font-weight: 600
color: $secondary-font-color
-.fetchButtonContainer
- width: 100%
- display: flex
- justify-content: center
- margin-top: 12px
- font-weight: 600
- color: rgba(255,255,255,0.75)
+
+.btnOld
+ position: absolute
+ top: 20px
+ right: 10px
+ background: #205CF5
+ border-radius: 50%
+ height: 35px
+ width: 35px
+ border: none
+ cursor: pointer
+ z-index: 1
+ img
+ height: 10px
+ transform: scaleY(-1)
.btnLive
position: absolute
diff --git a/ui/src/components/style/Filters.module.sass b/ui/src/components/style/Filters.module.sass
index ae8af8224..a815d7a9d 100644
--- a/ui/src/components/style/Filters.module.sass
+++ b/ui/src/components/style/Filters.module.sass
@@ -4,9 +4,6 @@
display: flex
flex-direction: row
align-items: center
- min-height: 3rem
- overflow-y: hidden
- overflow-x: auto
padding: .5rem 0
border-bottom: 1px solid #BCC6DD
margin-right: 20px
@@ -29,8 +26,24 @@
input
padding: 4px 12px
background: $main-background-color
- border-radius: 12px
- font-size: 12px
+ border-radius: 4px
+ font-size: 14px
border: 1px solid #BCC6DD
fieldset
border: none
+
+$divider-breakpoint-1: 1474px
+$divider-breakpoint-2: 1366px
+$divider-breakpoint-3: 1980px
+
+@media (max-width: $divider-breakpoint-1)
+ .divider1
+ display: none
+
+@media (max-width: $divider-breakpoint-2)
+ .divider2
+ display: none
+
+@media (min-width: $divider-breakpoint-1) and (max-width: $divider-breakpoint-3)
+ .divider2
+ display: none
diff --git a/ui/src/components/style/TrafficPage.sass b/ui/src/components/style/TrafficPage.sass
index ceeefe128..68d4c3d08 100644
--- a/ui/src/components/style/TrafficPage.sass
+++ b/ui/src/components/style/TrafficPage.sass
@@ -110,3 +110,8 @@
align-items: center
height: 17px
font-size: 16px
+
+.playPauseIcon
+ cursor: pointer
+ margin-right: 15px
+ height: 30px
diff --git a/ui/src/helpers/api.js b/ui/src/helpers/api.js
index 216b5b0c4..ef7404996 100644
--- a/ui/src/helpers/api.js
+++ b/ui/src/helpers/api.js
@@ -1,16 +1,16 @@
import * as axios from "axios";
-const mizuAPIPathPrefix = "/mizu";
-
// When working locally cp `cp .env.example .env`
-export const MizuWebsocketURL = process.env.REACT_APP_OVERRIDE_WS_URL ? process.env.REACT_APP_OVERRIDE_WS_URL : `ws://${window.location.host}${mizuAPIPathPrefix}/ws`;
+export const MizuWebsocketURL = process.env.REACT_APP_OVERRIDE_WS_URL ? process.env.REACT_APP_OVERRIDE_WS_URL : `ws://${window.location.host}/ws`;
+
+const CancelToken = axios.CancelToken;
export default class Api {
constructor() {
// When working locally cp `cp .env.example .env`
- const apiURL = process.env.REACT_APP_OVERRIDE_API_URL ? process.env.REACT_APP_OVERRIDE_API_URL : `${window.location.origin}${mizuAPIPathPrefix}/`;
+ const apiURL = process.env.REACT_APP_OVERRIDE_API_URL ? process.env.REACT_APP_OVERRIDE_API_URL : `${window.location.origin}/`;
this.client = axios.create({
baseURL: apiURL,
@@ -19,6 +19,8 @@ export default class Api {
Accept: "application/json",
}
});
+
+ this.source = null;
}
tapStatus = async () => {
@@ -31,13 +33,16 @@ export default class Api {
return response.data;
}
- getEntry = async (entryId) => {
- const response = await this.client.get(`/entries/${entryId}`);
+ getEntry = async (id) => {
+ const response = await this.client.get(`/entries/${id}`);
return response.data;
}
- fetchEntries = async (operator, timestamp) => {
- const response = await this.client.get(`/entries?limit=50&operator=${operator}×tamp=${timestamp}`);
+ fetchEntries = async (leftOff, direction, query, limit, timeoutMs) => {
+ const response = await this.client.get(`/entries/?leftOff=${leftOff}&direction=${direction}&query=${query}&limit=${limit}&timeoutMs=${timeoutMs}`).catch(function (thrown) {
+ console.error(thrown.message);
+ return {};
+ });
return response.data;
}
@@ -50,4 +55,27 @@ export default class Api {
const response = await this.client.get("/status/auth");
return response.data;
}
+
+ validateQuery = async (query) => {
+ if (this.source) {
+ this.source.cancel();
+ }
+ this.source = CancelToken.source();
+
+ const form = new FormData();
+ form.append('query', query)
+ const response = await this.client.post(`/query/validate`, form, {
+ cancelToken: this.source.token
+ }).catch(function (thrown) {
+ if (!axios.isCancel(thrown)) {
+ console.error('Validate error', thrown.message);
+ }
+ });
+
+ if (!response) {
+ return null;
+ }
+
+ return response.data;
+ }
}
diff --git a/ui/src/variables.module.scss b/ui/src/variables.module.scss
index d2f3d89bd..9e25bb3c3 100644
--- a/ui/src/variables.module.scss
+++ b/ui/src/variables.module.scss
@@ -11,7 +11,7 @@ $blue-gray: #494677;
:export {
mainBackgroundColor: $main-background-color;
- headerBackgoundColor: $header-background-color;
+ headerBackgroundColor: $header-background-color;
fontColor: $font-color;
secondaryFontColor: $secondary-font-color;
blueColor: $blue-color;