TRA-4159 Mizu state management (#631)

* initiate recoil state management with entPage and tappingStatus

* first recoil selector

* insert entries and focusedEntryId into recoil

* ws connection, entry data

* manage query by recoil

* identifier for cypress

* conflicts fix

* css fix

* cr fixes

Co-authored-by: gadotroee <55343099+gadotroee@users.noreply.github.com>
This commit is contained in:
lirazyehezkel 2022-01-16 15:27:09 +02:00 committed by GitHub
parent 5c59cd643a
commit 5fed5808d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 317 additions and 262 deletions

13
ui/package-lock.json generated
View File

@ -7856,6 +7856,11 @@
"pify": "^4.0.1"
}
},
"hamt_plus": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz",
"integrity": "sha1-4hwlKWjH4zsg9qGwlM2FeHomVgE="
},
"handle-thing": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
@ -14229,6 +14234,14 @@
"picomatch": "^2.2.1"
}
},
"recoil": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/recoil/-/recoil-0.5.2.tgz",
"integrity": "sha512-Edibzpu3dbUMLy6QRg73WL8dvMl9Xqhp+kU+f2sJtXxsaXvAlxU/GcnDE8HXPkprXrhHF2e6SZozptNvjNF5fw==",
"requires": {
"hamt_plus": "1.0.2"
}
},
"recursive-readdir": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz",

View File

@ -31,6 +31,7 @@
"react-scrollable-feed-virtualized": "^1.4.9",
"react-syntax-highlighter": "^15.4.3",
"react-toastify": "^8.0.3",
"recoil": "^0.5.2",
"typescript": "^4.2.4",
"web-vitals": "^1.1.1",
"xml-formatter": "^2.6.0"

View File

@ -1,4 +1,4 @@
import React, {useEffect, useState} from 'react';
import React, {useCallback, useEffect, useState} from 'react';
import './App.sass';
import {TrafficPage} from "./components/TrafficPage";
import {TLSWarning} from "./components/TLSWarning/TLSWarning";
@ -9,43 +9,29 @@ import InstallPage from "./components/InstallPage";
import LoginPage from "./components/LoginPage";
import LoadingOverlay from "./components/LoadingOverlay";
import AuthPageBase from './components/AuthPageBase';
import entPageAtom, {Page} from "./recoil/entPage";
import {useRecoilState} from "recoil";
const api = Api.getInstance();
// TODO: move to state management
export enum Page {
Traffic,
Setup,
Login
}
// TODO: move to state management
export interface MizuContextModel {
page: Page;
setPage: (page: Page) => void;
}
// TODO: move to state management
export const MizuContext = React.createContext<MizuContextModel>(null);
const EntApp = () => {
const [isLoading, setIsLoading] = useState(true);
const [showTLSWarning, setShowTLSWarning] = useState(false);
const [userDismissedTLSWarning, setUserDismissedTLSWarning] = useState(false);
const [addressesWithTLS, setAddressesWithTLS] = useState(new Set<string>());
const [page, setPage] = useState(Page.Traffic); // TODO: move to state management
const [entPage, setEntPage] = useRecoilState(entPageAtom);
const [isFirstLogin, setIsFirstLogin] = useState(false);
const determinePage = async () => { // TODO: move to state management
const determinePage = useCallback(async () => { // TODO: move to state management
try {
const isInstallNeeded = await api.isInstallNeeded();
if (isInstallNeeded) {
setPage(Page.Setup);
setEntPage(Page.Setup);
} else {
const isAuthNeeded = await api.isAuthenticationNeeded();
if(isAuthNeeded) {
setPage(Page.Login);
setEntPage(Page.Login);
}
}
} catch (e) {
@ -54,11 +40,11 @@ const EntApp = () => {
} finally {
setIsLoading(false);
}
}
},[setEntPage]);
useEffect(() => {
determinePage();
}, []);
}, [determinePage]);
const onTLSDetected = (destAddress: string) => {
addressesWithTLS.add(destAddress);
@ -71,7 +57,7 @@ const EntApp = () => {
let pageComponent: any;
switch (page) { // TODO: move to state management / proper routing
switch (entPage) { // TODO: move to state management / proper routing
case Page.Traffic:
pageComponent = <TrafficPage onTLSDetected={onTLSDetected}/>;
break;
@ -91,16 +77,14 @@ const EntApp = () => {
return (
<div className="mizuApp">
<MizuContext.Provider value={{page, setPage}}>
{page === Page.Traffic && <EntHeader isFirstLogin={isFirstLogin} setIsFirstLogin={setIsFirstLogin}/>}
{pageComponent}
{page === Page.Traffic && <TLSWarning showTLSWarning={showTLSWarning}
setShowTLSWarning={setShowTLSWarning}
addressesWithTLS={addressesWithTLS}
setAddressesWithTLS={setAddressesWithTLS}
userDismissedTLSWarning={userDismissedTLSWarning}
setUserDismissedTLSWarning={setUserDismissedTLSWarning}/>}
</MizuContext.Provider>
{entPage === Page.Traffic && <EntHeader isFirstLogin={isFirstLogin} setIsFirstLogin={setIsFirstLogin}/>}
{pageComponent}
{entPage === Page.Traffic && <TLSWarning showTLSWarning={showTLSWarning}
setShowTLSWarning={setShowTLSWarning}
addressesWithTLS={addressesWithTLS}
setAddressesWithTLS={setAddressesWithTLS}
userDismissedTLSWarning={userDismissedTLSWarning}
setUserDismissedTLSWarning={setUserDismissedTLSWarning}/>}
</div>
);
}

View File

@ -6,11 +6,12 @@ import {EntryItem} from "./EntryListItem/EntryListItem";
import down from "./assets/downImg.svg";
import spinner from './assets/spinner.svg';
import Api from "../helpers/api";
import {useRecoilState, useRecoilValue} from "recoil";
import entriesAtom from "../recoil/entries";
import wsConnectionAtom, {WsConnectionStatus} from "../recoil/wsConnection";
import queryAtom from "../recoil/query";
interface EntriesListProps {
entries: any[];
setEntries: any;
query: string;
listEntryREF: any;
onSnapBrokenEvent: () => void;
isSnappedToBottom: boolean;
@ -22,12 +23,8 @@ interface EntriesListProps {
startTime: number;
noMoreDataTop: boolean;
setNoMoreDataTop: (flag: boolean) => void;
focusedEntryId: string;
setFocusedEntryId: (id: string) => void;
updateQuery: any;
leftOffTop: number;
setLeftOffTop: (leftOffTop: number) => void;
isWebSocketConnectionClosed: boolean;
ws: any;
openWebSocket: (query: string, resetEntries: boolean) => void;
leftOffBottom: number;
@ -38,7 +35,13 @@ interface EntriesListProps {
const api = Api.getInstance();
export const EntriesList: React.FC<EntriesListProps> = ({entries, setEntries, query, listEntryREF, onSnapBrokenEvent, isSnappedToBottom, setIsSnappedToBottom, queriedCurrent, setQueriedCurrent, queriedTotal, setQueriedTotal, startTime, noMoreDataTop, setNoMoreDataTop, focusedEntryId, setFocusedEntryId, updateQuery, leftOffTop, setLeftOffTop, isWebSocketConnectionClosed, ws, openWebSocket, leftOffBottom, truncatedTimestamp, setTruncatedTimestamp, scrollableRef}) => {
export const EntriesList: React.FC<EntriesListProps> = ({listEntryREF, onSnapBrokenEvent, isSnappedToBottom, setIsSnappedToBottom, queriedCurrent, setQueriedCurrent, queriedTotal, setQueriedTotal, startTime, noMoreDataTop, setNoMoreDataTop, leftOffTop, setLeftOffTop, ws, openWebSocket, leftOffBottom, truncatedTimestamp, setTruncatedTimestamp, scrollableRef}) => {
const [entries, setEntries] = useRecoilState(entriesAtom);
const wsConnection = useRecoilValue(wsConnectionAtom);
const query = useRecoilValue(queryAtom);
const isWsConnectionClosed = wsConnection === WsConnectionStatus.Closed;
const [loadMoreTop, setLoadMoreTop] = useState(false);
const [isLoadingTop, setIsLoadingTop] = useState(false);
@ -95,9 +98,9 @@ export const EntriesList: React.FC<EntriesListProps> = ({entries, setEntries, qu
},[setLoadMoreTop, setIsLoadingTop, entries, setEntries, query, setNoMoreDataTop, leftOffTop, setLeftOffTop, queriedCurrent, setQueriedCurrent, setQueriedTotal, setTruncatedTimestamp, scrollableRef]);
useEffect(() => {
if(!isWebSocketConnectionClosed || !loadMoreTop || noMoreDataTop) return;
if(!isWsConnectionClosed || !loadMoreTop || noMoreDataTop) return;
getOldEntries();
}, [loadMoreTop, noMoreDataTop, getOldEntries, isWebSocketConnectionClosed]);
}, [loadMoreTop, noMoreDataTop, getOldEntries, isWsConnectionClosed]);
const scrollbarVisible = scrollableRef.current?.childWrapperRef.current.clientHeight > scrollableRef.current?.wrapperRef.current.clientHeight;
@ -113,10 +116,7 @@ export const EntriesList: React.FC<EntriesListProps> = ({entries, setEntries, qu
{memoizedEntries.map(entry => <EntryItem
key={`entry-${entry.id}`}
entry={entry}
focusedEntryId={focusedEntryId}
setFocusedEntryId={setFocusedEntryId}
style={{}}
updateQuery={updateQuery}
headingMode={false}
/>)}
</ScrollableFeedVirtualized>
@ -131,9 +131,9 @@ export const EntriesList: React.FC<EntriesListProps> = ({entries, setEntries, qu
</button>
<button type="button"
title="Snap to bottom"
className={`${styles.btnLive} ${isSnappedToBottom && !isWebSocketConnectionClosed ? styles.hideButton : styles.showButton}`}
className={`${styles.btnLive} ${isSnappedToBottom && !isWsConnectionClosed ? styles.hideButton : styles.showButton}`}
onClick={(_) => {
if (isWebSocketConnectionClosed) {
if (isWsConnectionClosed) {
if (query) {
openWebSocket(`(${query}) and leftOff(${leftOffBottom})`, false);
} else {

View File

@ -1,9 +1,13 @@
import React from "react";
import React, {useEffect, useState} from "react";
import EntryViewer from "./EntryDetailed/EntryViewer";
import {EntryItem} from "./EntryListItem/EntryListItem";
import {makeStyles} from "@material-ui/core";
import Protocol from "./UI/Protocol"
import Queryable from "./UI/Queryable";
import {toast} from "react-toastify";
import {useRecoilValue} from "recoil";
import focusedEntryIdAtom from "../recoil/focusedEntryId";
import Api from "../helpers/api";
const useStyles = makeStyles(() => ({
entryTitle: {
@ -27,23 +31,17 @@ const useStyles = makeStyles(() => ({
}
}));
interface EntryDetailedProps {
entryData: any
updateQuery: any
}
export const formatSize = (n: number) => n > 1000 ? `${Math.round(n / 1000)}KB` : `${n} B`;
const EntryTitle: React.FC<any> = ({protocol, data, bodySize, elapsedTime, updateQuery}) => {
const EntryTitle: React.FC<any> = ({protocol, data, bodySize, elapsedTime}) => {
const classes = useStyles();
const response = data.response;
return <div className={classes.entryTitle}>
<Protocol protocol={protocol} horizontal={true} updateQuery={updateQuery}/>
<Protocol protocol={protocol} horizontal={true}/>
<div style={{right: "30px", position: "absolute", display: "flex"}}>
{response && <Queryable
query={`response.bodySize == ${bodySize}`}
updateQuery={updateQuery}
style={{margin: "0 18px"}}
displayIconOnMouseOver={true}
>
@ -55,7 +53,6 @@ const EntryTitle: React.FC<any> = ({protocol, data, bodySize, elapsedTime, updat
</Queryable>}
{response && <Queryable
query={`elapsedTime >= ${elapsedTime}`}
updateQuery={updateQuery}
style={{marginRight: 18}}
displayIconOnMouseOver={true}
>
@ -69,30 +66,58 @@ const EntryTitle: React.FC<any> = ({protocol, data, bodySize, elapsedTime, updat
</div>;
};
const EntrySummary: React.FC<any> = ({entry, updateQuery}) => {
const EntrySummary: React.FC<any> = ({entry}) => {
return <EntryItem
key={`entry-${entry.id}`}
entry={entry}
focusedEntryId={null}
setFocusedEntryId={null}
style={{}}
updateQuery={updateQuery}
headingMode={true}
/>;
};
export const EntryDetailed: React.FC<EntryDetailedProps> = ({entryData, updateQuery}) => {
const api = Api.getInstance();
export const EntryDetailed = () => {
const focusedEntryId = useRecoilValue(focusedEntryIdAtom);
const [entryData, setEntryData] = useState(null);
useEffect(() => {
if (!focusedEntryId) return;
setEntryData(null);
(async () => {
try {
const entryData = await api.getEntry(focusedEntryId);
setEntryData(entryData);
} catch (error) {
if (error.response?.data?.type) {
toast[error.response.data.type](`Entry[${focusedEntryId}]: ${error.response.data.msg}`, {
position: "bottom-right",
theme: "colored",
autoClose: error.response.data.autoClose,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
}
console.error(error);
}
})();
// eslint-disable-next-line
}, [focusedEntryId]);
return <>
<EntryTitle
{entryData && <EntryTitle
protocol={entryData.protocol}
data={entryData.data}
bodySize={entryData.bodySize}
elapsedTime={entryData.data.elapsedTime}
updateQuery={updateQuery}
/>
{entryData.data && <EntrySummary entry={entryData.data} updateQuery={updateQuery}/>}
/>}
{entryData && <EntrySummary entry={entryData.data}/>}
<>
{entryData.data && <EntryViewer
{entryData && <EntryViewer
representation={entryData.representation}
isRulesEnabled={entryData.isRulesEnabled}
rulesMatched={entryData.rulesMatched}
@ -102,7 +127,6 @@ export const EntryDetailed: React.FC<EntryDetailedProps> = ({entryData, updateQu
contractContent={entryData.data.contractContent}
elapsedTime={entryData.data.elapsedTime}
color={entryData.protocol.backgroundColor}
updateQuery={updateQuery}
/>}
</>
</>

View File

@ -12,14 +12,13 @@ import {default as xmlBeautify} from "xml-formatter";
interface EntryViewLineProps {
label: string;
value: number | string;
updateQuery?: any;
selector?: string;
overrideQueryValue?: string;
displayIconOnMouseOver?: boolean;
useTooltip?: boolean;
}
const EntryViewLine: React.FC<EntryViewLineProps> = ({label, value, updateQuery = null, selector = "", overrideQueryValue = "", displayIconOnMouseOver = true, useTooltip = true}) => {
const EntryViewLine: React.FC<EntryViewLineProps> = ({label, value, selector = "", overrideQueryValue = "", displayIconOnMouseOver = true, useTooltip = true}) => {
let query: string;
if (!selector) {
query = "";
@ -34,7 +33,6 @@ const EntryViewLine: React.FC<EntryViewLineProps> = ({label, value, updateQuery
<td className={`${styles.dataKey}`}>
<Queryable
query={query}
updateQuery={updateQuery}
style={{float: "right", height: "18px"}}
iconStyle={{marginRight: "20px"}}
flipped={true}
@ -63,10 +61,9 @@ interface EntrySectionCollapsibleTitleProps {
expanded: boolean,
setExpanded: any,
query?: string,
updateQuery?: any,
}
const EntrySectionCollapsibleTitle: React.FC<EntrySectionCollapsibleTitleProps> = ({title, color, expanded, setExpanded, query = "", updateQuery = null}) => {
const EntrySectionCollapsibleTitle: React.FC<EntrySectionCollapsibleTitleProps> = ({title, color, expanded, setExpanded, query = ""}) => {
return <div className={styles.title}>
<div
className={`${styles.button} ${expanded ? styles.expanded : ''}`}
@ -79,9 +76,8 @@ const EntrySectionCollapsibleTitle: React.FC<EntrySectionCollapsibleTitleProps>
</div>
<Queryable
query={query}
updateQuery={updateQuery}
useTooltip={updateQuery ? true : false}
displayIconOnMouseOver={updateQuery ? true : false}
useTooltip={!!query}
displayIconOnMouseOver={!!query}
>
<span>{title}</span>
</Queryable>
@ -92,15 +88,14 @@ interface EntrySectionContainerProps {
title: string,
color: string,
query?: string,
updateQuery?: any,
}
export const EntrySectionContainer: React.FC<EntrySectionContainerProps> = ({title, color, children, query = "", updateQuery = null}) => {
export const EntrySectionContainer: React.FC<EntrySectionContainerProps> = ({title, color, children, query = ""}) => {
const [expanded, setExpanded] = useState(true);
return <CollapsibleContainer
className={styles.collapsibleContainer}
expanded={expanded}
title={<EntrySectionCollapsibleTitle title={title} color={color} expanded={expanded} setExpanded={setExpanded} query={query} updateQuery={updateQuery}/>}
title={<EntrySectionCollapsibleTitle title={title} color={color} expanded={expanded} setExpanded={setExpanded} query={query}/>}
>
{children}
</CollapsibleContainer>
@ -110,7 +105,6 @@ interface EntryBodySectionProps {
title: string,
content: any,
color: string,
updateQuery: any,
encoding?: string,
contentType?: string,
selector?: string,
@ -119,7 +113,6 @@ interface EntryBodySectionProps {
export const EntryBodySection: React.FC<EntryBodySectionProps> = ({
title,
color,
updateQuery,
content,
encoding,
contentType,
@ -172,7 +165,6 @@ export const EntryBodySection: React.FC<EntryBodySectionProps> = ({
title={title}
color={color}
query={`${selector} == r".*"`}
updateQuery={updateQuery}
>
<div style={{display: 'flex', alignItems: 'center', alignContent: 'center', margin: "5px 0"}}>
{supportsPrettying && <div style={{paddingTop: 3}}>
@ -203,10 +195,9 @@ interface EntrySectionProps {
title: string,
color: string,
arrayToIterate: any[],
updateQuery: any,
}
export const EntryTableSection: React.FC<EntrySectionProps> = ({title, color, arrayToIterate, updateQuery}) => {
export const EntryTableSection: React.FC<EntrySectionProps> = ({title, color, arrayToIterate}) => {
let arrayToIterateSorted: any[];
if (arrayToIterate) {
arrayToIterateSorted = arrayToIterate.sort((a, b) => {
@ -231,7 +222,6 @@ export const EntryTableSection: React.FC<EntrySectionProps> = ({title, color, ar
key={index}
label={name}
value={value}
updateQuery={updateQuery}
selector={selector}
/>)}
</tbody>

View File

@ -8,7 +8,7 @@ enum SectionTypes {
SectionBody = "body",
}
const SectionsRepresentation: React.FC<any> = ({data, color, updateQuery}) => {
const SectionsRepresentation: React.FC<any> = ({data, color}) => {
const sections = []
if (data) {
@ -16,12 +16,12 @@ const SectionsRepresentation: React.FC<any> = ({data, color, updateQuery}) => {
switch (row.type) {
case SectionTypes.SectionTable:
sections.push(
<EntryTableSection key={i} title={row.title} color={color} arrayToIterate={JSON.parse(row.data)} updateQuery={updateQuery}/>
<EntryTableSection key={i} title={row.title} color={color} arrayToIterate={JSON.parse(row.data)}/>
)
break;
case SectionTypes.SectionBody:
sections.push(
<EntryBodySection key={i} title={row.title} color={color} content={row.data} updateQuery={updateQuery} encoding={row.encoding} contentType={row.mimeType} selector={row.selector}/>
<EntryBodySection key={i} title={row.title} color={color} content={row.data} encoding={row.encoding} contentType={row.mimeType} selector={row.selector}/>
)
break;
default:
@ -33,7 +33,7 @@ const SectionsRepresentation: React.FC<any> = ({data, color, updateQuery}) => {
return <>{sections}</>;
}
const AutoRepresentation: React.FC<any> = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color, updateQuery}) => {
const AutoRepresentation: React.FC<any> = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color}) => {
var TABS = [
{
tab: 'Request'
@ -48,9 +48,9 @@ const AutoRepresentation: React.FC<any> = ({representation, isRulesEnabled, rule
const {request, response} = JSON.parse(representation);
var responseTabIndex = 0;
var rulesTabIndex = 0;
var contractTabIndex = 0;
let responseTabIndex = 0;
let rulesTabIndex = 0;
let contractTabIndex = 0;
if (response) {
TABS.push(
@ -85,10 +85,10 @@ const AutoRepresentation: React.FC<any> = ({representation, isRulesEnabled, rule
<Tabs tabs={TABS} currentTab={currentTab} color={color} onChange={setCurrentTab} leftAligned/>
</div>
{currentTab === TABS[0].tab && <React.Fragment>
<SectionsRepresentation data={request} color={color} updateQuery={updateQuery}/>
<SectionsRepresentation data={request} color={color}/>
</React.Fragment>}
{response && currentTab === TABS[responseTabIndex].tab && <React.Fragment>
<SectionsRepresentation data={response} color={color} updateQuery={updateQuery}/>
<SectionsRepresentation data={response} color={color}/>
</React.Fragment>}
{isRulesEnabled && currentTab === TABS[rulesTabIndex].tab && <React.Fragment>
<EntryTablePolicySection title={'Rule'} color={color} latency={elapsedTime} arrayToIterate={rulesMatched ? rulesMatched : []}/>
@ -110,10 +110,9 @@ interface Props {
contractContent: string;
color: string;
elapsedTime: number;
updateQuery: any;
}
const EntryViewer: React.FC<Props> = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color, updateQuery}) => {
const EntryViewer: React.FC<Props> = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color}) => {
return <AutoRepresentation
representation={representation}
isRulesEnabled={isRulesEnabled}
@ -124,7 +123,6 @@ const EntryViewer: React.FC<Props> = ({representation, isRulesEnabled, rulesMatc
contractContent={contractContent}
elapsedTime={elapsedTime}
color={color}
updateQuery={updateQuery}
/>
};

View File

@ -74,7 +74,6 @@
.separatorRight
display: flex
border-right: 1px solid $data-background-color
padding: 4px
padding-right: 12px
.separatorLeft

View File

@ -12,6 +12,9 @@ import ingoingIconNeutral from "../assets/ingoing-traffic-neutral.svg"
import outgoingIconSuccess from "../assets/outgoing-traffic-success.svg"
import outgoingIconFailure from "../assets/outgoing-traffic-failure.svg"
import outgoingIconNeutral from "../assets/outgoing-traffic-neutral.svg"
import {useRecoilState} from "recoil";
import focusedEntryIdAtom from "../../recoil/focusedEntryId";
import queryAtom from "../../recoil/query";
interface TCPInterface {
ip: string
@ -42,15 +45,14 @@ interface Rules {
interface EntryProps {
entry: Entry;
focusedEntryId: string;
setFocusedEntryId: (id: string) => void;
style: object;
updateQuery: any;
headingMode: boolean;
}
export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocusedEntryId, style, updateQuery, headingMode}) => {
export const EntryItem: React.FC<EntryProps> = ({entry, style, headingMode}) => {
const [focusedEntryId, setFocusedEntryId] = useRecoilState(focusedEntryIdAtom);
const [queryState, setQuery] = useRecoilState(queryAtom);
const isSelected = focusedEntryId === entry.id.toString();
const classification = getClassification(entry.status)
@ -103,8 +105,8 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
}
}
var contractEnabled = true;
var contractText = "";
let contractEnabled = true;
let contractText = "";
switch (entry.contractStatus) {
case 0:
contractEnabled = false;
@ -123,8 +125,9 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
break;
}
const isStatusCodeEnabled = ((entry.proto.name === "http" && "status" in entry) || entry.status !== 0);
var endpointServiceContainer = "10px";
let endpointServiceContainer = "10px";
if (!isStatusCodeEnabled) endpointServiceContainer = "20px";
return <>
@ -147,17 +150,15 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
{!headingMode ? <Protocol
protocol={entry.proto}
horizontal={false}
updateQuery={updateQuery}
/> : null}
{isStatusCodeEnabled && <div>
<StatusCode statusCode={entry.status} updateQuery={updateQuery}/>
<StatusCode statusCode={entry.status}/>
</div>}
<div className={styles.endpointServiceContainer} style={{paddingLeft: endpointServiceContainer}}>
<Summary method={entry.method} summary={entry.summary} updateQuery={updateQuery}/>
<Summary method={entry.method} summary={entry.summary}/>
<div className={styles.resolvedName}>
<Queryable
query={`src.name == "${entry.src.name}"`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={true}
style={{marginTop: "-4px", overflow: "visible"}}
@ -174,7 +175,6 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
<SwapHorizIcon style={{color: entry.proto.backgroundColor, marginTop: "-2px"}}></SwapHorizIcon>
<Queryable
query={`dst.name == "${entry.dst.name}"`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
style={{marginTop: "-4px"}}
iconStyle={{marginTop: "4px", marginLeft: "-2px"}}
@ -204,7 +204,6 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
<div className={styles.separatorRight}>
<Queryable
query={`src.ip == "${entry.src.ip}"`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={true}
iconStyle={{marginRight: "16px"}}
@ -219,7 +218,6 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
<span className={`${styles.tcpInfo}`} style={{marginTop: "18px"}}>{entry.src.port ? ":" : ""}</span>
<Queryable
query={`src.port == "${entry.src.port}"`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={true}
iconStyle={{marginTop: "28px"}}
@ -234,7 +232,6 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
{entry.isOutgoing ?
<Queryable
query={`outgoing == true`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={true}
iconStyle={{marginTop: "28px"}}
@ -248,7 +245,6 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
:
<Queryable
query={`outgoing == true`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={true}
iconStyle={{marginTop: "28px"}}
@ -258,14 +254,14 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
alt="Outgoing traffic"
title="Outgoing"
onClick={() => {
updateQuery(`outgoing == false`)
const query = `outgoing == false`;
setQuery(queryState ? `${queryState} and ${query}` : query);
}}
/>
</Queryable>
}
<Queryable
query={`dst.ip == "${entry.dst.ip}"`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={false}
iconStyle={{marginTop: "28px"}}
@ -280,7 +276,6 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
<span className={`${styles.tcpInfo}`} style={{marginTop: "18px"}}>:</span>
<Queryable
query={`dst.port == "${entry.dst.port}"`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={false}
>
@ -295,7 +290,6 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
<div className={styles.timestamp}>
<Queryable
query={`timestamp >= datetime("${Moment(+entry.timestamp)?.utc().format('MM/DD/YYYY, h:mm:ss.SSS A')}")`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={false}
>

View File

@ -7,20 +7,18 @@ import {SyntaxHighlighter} from "./UI/SyntaxHighlighter/index";
import filterUIExample1 from "./assets/filter-ui-example-1.png"
import filterUIExample2 from "./assets/filter-ui-example-2.png"
import variables from '../variables.module.scss';
import {useRecoilState} from "recoil";
import queryAtom from "../recoil/query";
interface FiltersProps {
query: string
setQuery: any
backgroundColor: string
ws: any
openWebSocket: (query: string, resetEntries: boolean) => void;
}
export const Filters: React.FC<FiltersProps> = ({query, setQuery, backgroundColor, ws, openWebSocket}) => {
export const Filters: React.FC<FiltersProps> = ({backgroundColor, ws, openWebSocket}) => {
return <div className={styles.container}>
<QueryForm
query={query}
setQuery={setQuery}
backgroundColor={backgroundColor}
ws={ws}
openWebSocket={openWebSocket}
@ -29,8 +27,6 @@ export const Filters: React.FC<FiltersProps> = ({query, setQuery, backgroundColo
};
interface QueryFormProps {
query: string
setQuery: any
backgroundColor: string
ws: any
openWebSocket: (query: string, resetEntries: boolean) => void;
@ -50,9 +46,10 @@ export const modalStyle = {
color: '#000',
};
export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, backgroundColor, ws, openWebSocket}) => {
export const QueryForm: React.FC<QueryFormProps> = ({backgroundColor, ws, openWebSocket}) => {
const formRef = useRef<HTMLFormElement>(null);
const [query, setQuery] = useRecoilState(queryAtom);
const [openModal, setOpenModal] = useState(false);

View File

@ -1,4 +1,4 @@
import React, {useContext, useEffect, useState} from "react";
import React, {useEffect, useState} from "react";
import logo from '../assets/MizuEntLogo.svg';
import './Header.sass';
import userImg from '../assets/user-circle.svg';
@ -9,7 +9,8 @@ import logoutIcon from '../assets/logout.png';
import {SettingsModal} from "../SettingsModal/SettingModal";
import Api from "../../helpers/api";
import {toast} from "react-toastify";
import {MizuContext, Page} from "../../EntApp";
import {useSetRecoilState} from "recoil";
import entPageAtom, {Page} from "../../recoil/entPage";
const api = Api.getInstance();
@ -49,12 +50,12 @@ export const EntHeader: React.FC<EntHeaderProps> = ({isFirstLogin, setIsFirstLog
const ProfileButton = () => {
const {setPage} = useContext(MizuContext);
const setEntPage = useSetRecoilState(entPageAtom);
const logout = async (popupState) => {
try {
await api.logout();
setPage(Page.Login);
setEntPage(Page.Login);
} catch (e) {
toast.error("Something went wrong, please check the console");
console.error(e);

View File

@ -1,11 +1,12 @@
import { Button } from "@material-ui/core";
import React, { useContext, useState } from "react";
import { MizuContext, Page } from "../EntApp";
import React, { useState } from "react";
import { adminUsername } from "../consts";
import Api, { FormValidationErrorType } from "../helpers/api";
import { toast } from 'react-toastify';
import LoadingOverlay from "./LoadingOverlay";
import { useCommonStyles } from "../helpers/commonStyle";
import {useSetRecoilState} from "recoil";
import entPageAtom, {Page} from "../recoil/entPage";
const api = Api.getInstance();
@ -20,7 +21,7 @@ export const InstallPage: React.FC<InstallPageProps> = ({onFirstLogin}) => {
const [password, setPassword] = useState("");
const [passwordConfirm, setPasswordConfirm] = useState("");
const {setPage} = useContext(MizuContext);
const setEntPage = useSetRecoilState(entPageAtom);
const onFormSubmit = async () => {
if (password.length < 4) {
@ -35,7 +36,7 @@ export const InstallPage: React.FC<InstallPageProps> = ({onFirstLogin}) => {
setIsLoading(true);
await api.register(adminUsername, password);
if (!await api.isAuthenticationNeeded()) {
setPage(Page.Traffic);
setEntPage(Page.Traffic);
onFirstLogin();
}
} catch (e) {

View File

@ -1,10 +1,11 @@
import { Button } from "@material-ui/core";
import React, { useContext, useState } from "react";
import React, { useState } from "react";
import { toast } from "react-toastify";
import { MizuContext, Page } from "../EntApp";
import Api from "../helpers/api";
import { useCommonStyles } from "../helpers/commonStyle";
import LoadingOverlay from "./LoadingOverlay";
import entPageAtom, {Page} from "../recoil/entPage";
import {useSetRecoilState} from "recoil";
const api = Api.getInstance();
@ -15,7 +16,7 @@ const LoginPage: React.FC = () => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const {setPage} = useContext(MizuContext);
const setEntPage = useSetRecoilState(entPageAtom);
const onFormSubmit = async () => {
setIsLoading(true);
@ -23,7 +24,7 @@ const LoginPage: React.FC = () => {
try {
await api.login(username, password);
if (!await api.isAuthenticationNeeded()) {
setPage(Page.Traffic);
setEntPage(Page.Traffic);
} else {
toast.error("Invalid credentials");
}

View File

@ -12,6 +12,12 @@ import {StatusBar} from "./UI/StatusBar";
import Api, {MizuWebsocketURL} from "../helpers/api";
import { toast } from 'react-toastify';
import debounce from 'lodash/debounce';
import {useRecoilState, useRecoilValue} from "recoil";
import tappingStatusAtom from "../recoil/tappingStatus";
import entriesAtom from "../recoil/entries";
import focusedEntryIdAtom from "../recoil/focusedEntryId";
import websocketConnectionAtom, {WsConnectionStatus} from "../recoil/wsConnection";
import queryAtom from "../recoil/query";
const useLayoutStyles = makeStyles(() => ({
details: {
@ -33,11 +39,6 @@ const useLayoutStyles = makeStyles(() => ({
}
}));
enum ConnectionStatus {
Closed,
Connected,
}
interface TrafficPageProps {
onTLSDetected: (destAddress: string) => void;
setAnalyzeStatus?: (status: any) => void;
@ -48,21 +49,16 @@ const api = Api.getInstance();
export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnalyzeStatus}) => {
const classes = useLayoutStyles();
const [entries, setEntries] = useState([] as any);
const [focusedEntryId, setFocusedEntryId] = useState(null);
const [selectedEntryData, setSelectedEntryData] = useState(null);
const [connection, setConnection] = useState(ConnectionStatus.Closed);
const [tappingStatus, setTappingStatus] = useRecoilState(tappingStatusAtom);
const [entries, setEntries] = useRecoilState(entriesAtom);
const [focusedEntryId, setFocusedEntryId] = useRecoilState(focusedEntryIdAtom);
const [wsConnection, setWsConnection] = useRecoilState(websocketConnectionAtom);
const query = useRecoilValue(queryAtom);
const [noMoreDataTop, setNoMoreDataTop] = useState(false);
const [tappingStatus, setTappingStatus] = useState(null);
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);
@ -94,15 +90,6 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
handleQueryChange(query);
}, [query, handleQueryChange]);
useEffect(() => {
if (query) {
setQuery(`${query} and ${addition}`);
} else {
setQuery(addition);
}
// eslint-disable-next-line
}, [addition]);
const ws = useRef(null);
const listEntry = useRef(null);
@ -117,11 +104,11 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
}
ws.current = new WebSocket(MizuWebsocketURL);
ws.current.onopen = () => {
setConnection(ConnectionStatus.Connected);
setWsConnection(WsConnectionStatus.Connected);
ws.current.send(query);
}
ws.current.onclose = () => {
setConnection(ConnectionStatus.Closed);
setWsConnection(WsConnectionStatus.Closed);
}
ws.current.onerror = (event) => {
console.error("WebSocket error:", event);
@ -206,36 +193,9 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
// eslint-disable-next-line
}, []);
useEffect(() => {
if (!focusedEntryId) return;
setSelectedEntryData(null);
(async () => {
try {
const entryData = await api.getEntry(focusedEntryId, query);
setSelectedEntryData(entryData);
} catch (error) {
if (error.response?.data?.type) {
toast[error.response.data.type](`Entry[${focusedEntryId}]: ${error.response.data.msg}`, {
position: "bottom-right",
theme: "colored",
autoClose: error.response.data.autoClose,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
}
console.error(error);
}
})();
// eslint-disable-next-line
}, [focusedEntryId]);
const toggleConnection = () => {
ws.current.close();
if (connection !== ConnectionStatus.Connected) {
if (wsConnection !== WsConnectionStatus.Connected) {
if (query) {
openWebSocket(`(${query}) and leftOff(-1)`, true);
} else {
@ -248,8 +208,8 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
const getConnectionStatusClass = (isContainer) => {
const container = isContainer ? "Container" : "";
switch (connection) {
case ConnectionStatus.Connected:
switch (wsConnection) {
case WsConnectionStatus.Connected:
return "greenIndicator" + container;
default:
return "redIndicator" + container;
@ -257,8 +217,8 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
}
const getConnectionTitle = () => {
switch (connection) {
case ConnectionStatus.Connected:
switch (wsConnection) {
case WsConnectionStatus.Connected:
return "streaming live traffic"
default:
return "streaming paused";
@ -267,7 +227,7 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
const onSnapBrokenEvent = () => {
setIsSnappedToBottom(false);
if (connection === ConnectionStatus.Connected) {
if (wsConnection === WsConnectionStatus.Connected) {
ws.current.close();
}
}
@ -275,9 +235,9 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
return (
<div className="TrafficPage">
<div className="TrafficPageHeader">
<img className="playPauseIcon" style={{visibility: connection === ConnectionStatus.Connected ? "visible" : "hidden"}} alt="pause"
<img className="playPauseIcon" style={{visibility: wsConnection === WsConnectionStatus.Connected ? "visible" : "hidden"}} alt="pause"
src={pauseIcon} onClick={toggleConnection}/>
<img className="playPauseIcon" style={{position: "absolute", visibility: connection === ConnectionStatus.Connected ? "hidden" : "visible"}} alt="play"
<img className="playPauseIcon" style={{position: "absolute", visibility: wsConnection === WsConnectionStatus.Connected ? "hidden" : "visible"}} alt="play"
src={playIcon} onClick={toggleConnection}/>
<div className="connectionText">
{getConnectionTitle()}
@ -289,17 +249,12 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
{<div className="TrafficPage-Container">
<div className="TrafficPage-ListContainer">
<Filters
query={query}
setQuery={setQuery}
backgroundColor={queryBackgroundColor}
ws={ws.current}
openWebSocket={openWebSocket}
/>
<div className={styles.container}>
<EntriesList
entries={entries}
setEntries={setEntries}
query={query}
listEntryREF={listEntry}
onSnapBrokenEvent={onSnapBrokenEvent}
isSnappedToBottom={isSnappedToBottom}
@ -311,12 +266,8 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
startTime={startTime}
noMoreDataTop={noMoreDataTop}
setNoMoreDataTop={setNoMoreDataTop}
focusedEntryId={focusedEntryId}
setFocusedEntryId={setFocusedEntryId}
updateQuery={updateQuery}
leftOffTop={leftOffTop}
setLeftOffTop={setLeftOffTop}
isWebSocketConnectionClosed={connection === ConnectionStatus.Closed}
ws={ws.current}
openWebSocket={openWebSocket}
leftOffBottom={leftOffBottom}
@ -327,10 +278,10 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
</div>
</div>
<div className={classes.details}>
{selectedEntryData && <EntryDetailed entryData={selectedEntryData} updateQuery={updateQuery}/>}
{focusedEntryId && <EntryDetailed/>}
</div>
</div>}
{tappingStatus && <StatusBar tappingStatus={tappingStatus}/>}
{tappingStatus && <StatusBar/>}
</div>
)
};

View File

@ -18,14 +18,12 @@ export interface ProtocolInterface {
interface ProtocolProps {
protocol: ProtocolInterface
horizontal: boolean
updateQuery: any
}
const Protocol: React.FC<ProtocolProps> = ({protocol, horizontal, updateQuery}) => {
const Protocol: React.FC<ProtocolProps> = ({protocol, horizontal}) => {
if (horizontal) {
return <Queryable
query={protocol.macro}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
>
<a target="_blank" rel="noopener noreferrer" href={protocol.referenceLink}>
@ -45,7 +43,6 @@ const Protocol: React.FC<ProtocolProps> = ({protocol, horizontal, updateQuery})
} else {
return <Queryable
query={protocol.macro}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={false}
iconStyle={{marginTop: "52px", marginRight: "10px", zIndex: 1000}}

View File

@ -2,10 +2,11 @@ import React, { useEffect, useState } from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import AddCircleIcon from '@material-ui/icons/AddCircle';
import './style/Queryable.sass';
import {useRecoilState} from "recoil";
import queryAtom from "../../recoil/query";
interface Props {
query: string,
updateQuery: any,
style?: object,
iconStyle?: object,
className?: string,
@ -14,9 +15,10 @@ interface Props {
flipped?: boolean,
}
const Queryable: React.FC<Props> = ({query, updateQuery, style, iconStyle, className, useTooltip= true, displayIconOnMouseOver = false, flipped = false, children}) => {
const Queryable: React.FC<Props> = ({query, style, iconStyle, className, useTooltip= true, displayIconOnMouseOver = false, flipped = false, children}) => {
const [showAddedNotification, setAdded] = useState(false);
const [showTooltip, setShowTooltip] = useState(false);
const [queryState, setQuery] = useRecoilState(queryAtom);
const onCopy = () => {
setAdded(true)
@ -25,13 +27,15 @@ const Queryable: React.FC<Props> = ({query, updateQuery, style, iconStyle, class
useEffect(() => {
let timer;
if (showAddedNotification) {
updateQuery(query);
setQuery(queryState ? `${queryState} and ${query}` : query);
timer = setTimeout(() => {
setAdded(false);
}, 1000);
}
return () => clearTimeout(timer);
}, [showAddedNotification, query, updateQuery]);
// eslint-disable-next-line
}, [showAddedNotification, query, setQuery]);
const addButton = query ? <CopyToClipboard text={query} onCopy={onCopy}>
<span
@ -39,7 +43,7 @@ const Queryable: React.FC<Props> = ({query, updateQuery, style, iconStyle, class
title={`Add "${query}" to the filter`}
style={iconStyle}
>
<AddCircleIcon fontSize="small" color="inherit"></AddCircleIcon>
<AddCircleIcon fontSize="small" color="inherit"/>
{showAddedNotification && <span className={'Queryable-AddNotifier'}>Added</span>}
</span>
</CopyToClipboard> : null;

View File

@ -3,33 +3,18 @@ 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 {
pods: TappingStatusPod[];
}
export interface Props {
tappingStatus: TappingStatusPod[]
}
import {useRecoilValue} from "recoil";
import tappingStatusAtom, {tappingStatusDetails} from "../../recoil/tappingStatus";
const pluralize = (noun: string, amount: number) => {
return `${noun}${amount !== 1 ? 's' : ''}`
}
export const StatusBar: React.FC<Props> = ({tappingStatus}) => {
export const StatusBar = () => {
const tappingStatus = useRecoilValue(tappingStatusAtom);
const [expandedBar, setExpandedBar] = useState(false);
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;
const {uniqueNamespaces, amountOfPods, amountOfTappedPods, amountOfUntappedPods} = useRecoilValue(tappingStatusDetails);
return <div className={'statusBar' + (expandedBar ? ' expandedStatusBar' : "")} onMouseOver={() => setExpandedBar(true)} onMouseLeave={() => setExpandedBar(false)}>
<div className="podsCount">

View File

@ -10,16 +10,14 @@ export enum StatusCodeClassification {
interface EntryProps {
statusCode: number
updateQuery: any
}
const StatusCode: React.FC<EntryProps> = ({statusCode, updateQuery}) => {
const StatusCode: React.FC<EntryProps> = ({statusCode}) => {
const classification = getClassification(statusCode)
return <Queryable
query={`response.status == ${statusCode}`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={true}
iconStyle={{marginTop: "40px", paddingLeft: "10px"}}

View File

@ -6,16 +6,14 @@ import Queryable from "./Queryable";
interface SummaryProps {
method: string
summary: string
updateQuery: any
}
export const Summary: React.FC<SummaryProps> = ({method, summary, updateQuery}) => {
export const Summary: React.FC<SummaryProps> = ({method, summary}) => {
return <div className={styles.container}>
{method && <Queryable
query={`method == "${method}"`}
className={`${miscStyles.protocol} ${miscStyles.method}`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
style={{whiteSpace: "nowrap"}}
>
@ -25,7 +23,6 @@ export const Summary: React.FC<SummaryProps> = ({method, summary, updateQuery})
</Queryable>}
{summary && <Queryable
query={`summary == "${summary}"`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
>
<div

View File

@ -5,23 +5,26 @@ import App from './App';
import EntApp from "./EntApp";
import {ToastContainer} from "react-toastify";
import 'react-toastify/dist/ReactToastify.css';
import {RecoilRoot} from "recoil";
ReactDOM.render(
<React.StrictMode>
<>
{window["isEnt"] ? <EntApp/> : <App/>}
<ToastContainer
position="bottom-right"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
/>
</>
<RecoilRoot>
<>
{window["isEnt"] ? <EntApp/> : <App/>}
<ToastContainer
position="bottom-right"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
/>
</>
</RecoilRoot>
</React.StrictMode>,
document.getElementById('root')
);

View File

@ -0,0 +1,8 @@
import { atom } from "recoil"
const entPageAtom = atom({
key: "entPageAtom",
default: 0
})
export default entPageAtom

View File

@ -0,0 +1,11 @@
import atom from "./atom";
enum Page {
Traffic,
Setup,
Login
}
export { Page };
export default atom;

View File

@ -0,0 +1,8 @@
import { atom } from "recoil";
const entriesAtom = atom({
key: "entriesAtom",
default: []
});
export default entriesAtom;

View File

@ -0,0 +1,3 @@
import atom from "./atom";
export default atom

View File

@ -0,0 +1,8 @@
import { atom } from "recoil";
const focusedEntryIdAtom = atom({
key: "focusedEntryIdAtom",
default: null
});
export default focusedEntryIdAtom;

View File

@ -0,0 +1,3 @@
import atom from "./atom";
export default atom

View File

@ -0,0 +1,8 @@
import { atom } from "recoil";
const queryAtom = atom({
key: "queryAtom",
default: ""
});
export default queryAtom;

View File

@ -0,0 +1,3 @@
import atom from "./atom";
export default atom;

View File

@ -0,0 +1,9 @@
import { atom } from "recoil";
import {TappingStatusPod} from "./index";
const tappingStatusAtom = atom({
key: "tappingStatusAtom",
default: null as TappingStatusPod[]
});
export default tappingStatusAtom;

View File

@ -0,0 +1,22 @@
import {selector} from "recoil";
import tappingStatusAtom from "./atom";
const tappingStatusDetails = selector({
key: 'tappingStatusDetails',
get: ({get}) => {
const tappingStatus = get(tappingStatusAtom);
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 {
uniqueNamespaces,
amountOfPods,
amountOfTappedPods,
amountOfUntappedPods,
};
},
});
export default tappingStatusDetails;

View File

@ -0,0 +1,16 @@
import atom from "./atom";
import tappingStatusDetails from './details';
interface TappingStatusPod {
name: string;
namespace: string;
isTapped: boolean;
}
interface TappingStatus {
pods: TappingStatusPod[];
}
export type {TappingStatus, TappingStatusPod, tappingStatusDetails};
export default atom;

View File

@ -0,0 +1,8 @@
import { atom } from "recoil";
const wsConnectionAtom = atom({
key: "wsConnectionAtom",
default: 0
});
export default wsConnectionAtom;

View File

@ -0,0 +1,10 @@
import atom from "./atom";
enum WsConnectionStatus {
Closed,
Connected,
}
export {WsConnectionStatus};
export default atom