Traffic viewer

This commit is contained in:
Liraz Yehezkel 2021-04-27 14:48:22 +03:00
parent f0859b9277
commit 0e0b51b42c
55 changed files with 19814 additions and 0 deletions

29
ui/.gitignore vendored Normal file
View File

@ -0,0 +1,29 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
/.idea/
*.pyc
*.iml
/results/
.idea
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

17766
ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

48
ui/package.json Normal file
View File

@ -0,0 +1,48 @@
{
"name": "mizu-ui",
"version": "0.1.0",
"private": true,
"dependencies": {
"@material-ui/core": "^4.11.3",
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^12.8.3",
"@types/jest": "^26.0.22",
"@types/node": "^12.20.10",
"@types/react": "^17.0.3",
"@types/react-dom": "^17.0.3",
"node-sass": "^5.0.0",
"numeral": "^2.0.6",
"react": "^17.0.2",
"react-copy-to-clipboard": "^5.0.3",
"react-dom": "^17.0.2",
"react-scripts": "4.0.3",
"react-syntax-highlighter": "^15.4.3",
"typescript": "^4.2.4",
"web-vitals": "^1.1.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

BIN
ui/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

43
ui/public/index.html Normal file
View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>MIZU</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

BIN
ui/public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
ui/public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

25
ui/public/manifest.json Normal file
View File

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
ui/public/robots.txt Normal file
View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

16
ui/src/App.tsx Normal file
View File

@ -0,0 +1,16 @@
import React from 'react';
import {HarPage} from "./components/HarPage";
const App = () => {
return (
<div style={{backgroundColor: "#090b14", width: "100%"}}>
<div style={{height: 100, display: "flex", alignItems: "center", paddingLeft: 24}}>
<div style={{fontSize: 45, letterSpacing: 2}}>MIZU</div>
<div style={{marginLeft: 10, color: "rgba(255,255,255,0.5)", paddingTop: 15, fontSize: 16}}>Traffic viewer for Kubernetes</div>
</div>
<HarPage/>
</div>
);
}
export default App;

View File

@ -0,0 +1,17 @@
import React from "react";
export interface Props {
checked: boolean;
onToggle: (checked:boolean) => any;
}
const Checkbox: React.FC<Props> = ({checked, onToggle}) => {
return (
<div className="checkboxWrapper">
<input type="checkbox" className="checkbox" checked={checked} onChange={(event) => onToggle(event.target.checked)}/>
</div>
);
};
export default Checkbox;

View File

@ -0,0 +1,56 @@
import React, {useState} from "react";
import collapsedImg from "./assets/collapsed.svg";
import expandedImg from "./assets/expanded.svg";
import "./style/CollapsibleContainer.sass";
interface Props {
title: string | React.ReactNode,
onClick?: (e: React.SyntheticEvent) => void,
isExpanded?: boolean,
titleClassName?: string,
stickyHeader?: boolean,
className?: string,
initialExpanded?: boolean;
passiveOnClick?: boolean; //whether specifying onClick overrides internal _isExpanded state handling
}
const CollapsibleContainer: React.FC<Props> = ({title, children, isExpanded, onClick, titleClassName, stickyHeader = false, className, initialExpanded = true, passiveOnClick}) => {
const [_isExpanded, _setExpanded] = useState(initialExpanded);
let expanded = isExpanded !== undefined ? isExpanded : _isExpanded;
const classNames = `CollapsibleContainer ${expanded ? "CollapsibleContainer-Expanded" : "CollapsibleContainer-Collapsed"} ${className ? className : ''}`;
// This is needed to achieve the sticky header feature.
// It is needed an un-contained component for the css to work properly.
const content = <React.Fragment>
<div
className={`CollapsibleContainer-Header ${stickyHeader ? "CollapsibleContainer-Header-Sticky" : ""}
${expanded ? "CollapsibleContainer-Header-Expanded" : ""}`}
onClick={(e) => {
if (onClick) {
onClick(e)
if (passiveOnClick != true)
return;
}
_setExpanded(!_isExpanded)
}}
>
{
React.isValidElement(title)?
<React.Fragment>{title}</React.Fragment> :
<React.Fragment>
<div className={`CollapsibleContainer-Title ${titleClassName ? titleClassName : ''}`}>{title}</div>
<img
className="CollapsibleContainer-ExpandCollapseButton"
src={expanded ? expandedImg : collapsedImg}
alt="Expand/Collapse Button"
/>
</React.Fragment>
}
</div>
{expanded ? children : null}
</React.Fragment>;
return stickyHeader ? content : <div className={classNames}>{content}</div>;
};
export default CollapsibleContainer;

View File

@ -0,0 +1,15 @@
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<EndpointPathProps> = ({method, path}) => {
return <div className={styles.container}>
{method && <span className={`${miscStyles.protocol} ${miscStyles.method}`}>{method}</span>}
<div title={path} className={styles.path}>{path}</div>
</div>
};

View File

@ -0,0 +1,63 @@
import React, { useEffect, useState } from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import duplicateImg from "./assets/duplicate.svg";
import './style/FancyTextDisplay.sass';
interface Props {
text: string | number,
className?: string,
isPossibleToCopy?: boolean,
applyTextEllipsis?: boolean,
flipped?: boolean,
useTooltip?: boolean,
displayIconOnMouseOver?: boolean,
buttonOnly?: boolean,
}
const FancyTextDisplay: React.FC<Props> = ({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 || '';
const onCopy = () => {
setCopied(true)
};
useEffect(() => {
let timer;
if (showCopiedNotification) {
timer = setTimeout(() => {
setCopied(false);
}, 1000);
}
return () => clearTimeout(timer);
}, [showCopiedNotification]);
const textElement = <span className={'FancyTextDisplay-Text'}>{displayText}</span>;
const copyButton = isPossibleToCopy && displayText ? <CopyToClipboard text={displayText} onCopy={onCopy}>
<span
className={`FancyTextDisplay-Icon`}
title={`Copy "${displayText}" value to clipboard`}
>
<img src={duplicateImg} alt="Duplicate full value"/>
{showCopiedNotification && <span className={'FancyTextDisplay-CopyNotifier'}>Copied</span>}
</span>
</CopyToClipboard> : null;
return (
<p
className={`FancyTextDisplay-Container ${className ? className : ''} ${displayIconOnMouseOver ? 'displayIconOnMouseOver ' : ''} ${applyTextEllipsis ? ' FancyTextDisplay-ContainerEllipsis' : ''}`}
title={displayText.toString()}
onMouseOver={ e => setShowTooltip(true)}
onMouseLeave={ e => setShowTooltip(false)}
>
{!buttonOnly && flipped && textElement}
{copyButton}
{!buttonOnly && !flipped && textElement}
{useTooltip && showTooltip && <span className={'FancyTextDisplay-CopyNotifier'}>{displayText}</span>}
</p>
);
};
export default FancyTextDisplay;

View File

@ -0,0 +1,28 @@
import React from "react";
import { MenuItem } from '@material-ui/core';
import style from './style/HARFilterSelect.module.sass';
import { Select, SelectProps } from "./Select";
interface HARFilterSelectProps extends SelectProps {
items: string[];
value: string | string[];
onChange: (string) => void;
label?: string;
allowMultiple?: boolean;
transformDisplay?: (string) => string;
}
export const HARFilterSelect: React.FC<HARFilterSelectProps> = ({items, value, onChange, label, allowMultiple= false, transformDisplay}) => {
return <Select
value={value}
multiple={allowMultiple}
label={label}
onChange={onChange}
transformDisplay={transformDisplay}
labelOnTop={true}
labelClassName={style.HARSelectLabel}
trimItemsWhenMultiple={true}
>
{items?.map(item => <MenuItem key={item} value={item}><span className='uppercase'>{item}</span></MenuItem>)}
</Select>
};

View File

@ -0,0 +1,38 @@
import {HarEntry} from "./HarEntry";
import React, {useEffect, useRef} from "react";
import styles from './style/HarEntriesList.module.sass';
interface HarEntriesListProps {
entries: any[];
focusedEntryId: string;
setFocusedEntryId: (id: string) => void
}
export const HarEntriesList: React.FC<HarEntriesListProps> = ({entries, focusedEntryId, setFocusedEntryId}) => {
const entriesDiv = useRef(null);
const totalCount = null; //todo
// Scroll to bottom in case results do not fit in screen
useEffect(() => {
if (entriesDiv.current && totalCount > 0) {
entriesDiv.current.scrollTop = entriesDiv.current.scrollHeight;
}
}, [entriesDiv, totalCount])
// Reverse entries for displaying in ascending order
// const entries = harStore.data.currentPagedResults.value.slice();
return <>
{/*{!isKafka && harStore.data.latestErrorType === ErrorType.TIMEOUT && <div>Timed out - many entries. Try to remove filters and try again</div>}*/}
{/*{!isKafka && harStore.data.latestErrorType === ErrorType.GENERAL && <div>Error getting entries</div>}*/}
{/*{!isKafka && harStore.data.isInitialized && harStore.data.fetchedCount === 0 && <div>No entries found</div>}*/}
{/*{isKafka && selectedModelStore.kafka.sampleMessages.isLoading && <LoadingOverlay delay={0}/>}*/}
<div ref={entriesDiv} className={styles.list}>
{entries?.map(entry => <HarEntry key={entry.id}
entry={entry}
setFocusedEntryId={setFocusedEntryId}
isSelected={focusedEntryId === entry.id}
/>)}
</div>
</>;
};

View File

@ -0,0 +1,43 @@
import React from "react";
import styles from './style/HarEntry.module.sass';
import StatusCode from "./StatusCode";
import {EndpointPath} from "./EndpointPath";
interface HAREntry {
method?: string,
path: string,
service: string,
id: string,
statusCode?: number;
url?: string;
isCurrentRevision?: boolean;
timestamp: Date;
}
interface HAREntryProps {
entry: HAREntry;
setFocusedEntryId: (id: string) => void;
isSelected?: boolean;
}
export const HarEntry: React.FC<HAREntryProps> = ({entry, setFocusedEntryId, isSelected}) => {
return <>
<div
className={`${styles.row} ${isSelected ? styles.rowSelected : ''}`} onClick={() => setFocusedEntryId(entry.id)}
>
{entry.statusCode && <div>
<StatusCode statusCode={entry.statusCode}/>
</div>}
<div className={styles.endpointServiceContainer}>
<EndpointPath method={entry.method} path={entry.path}/>
<div className={styles.service}>
{entry.service}
</div>
</div>
<div className={styles.timestamp}>{new Date(+entry.timestamp*1000)?.toLocaleString()}</div>
</div>
</>
};

View File

@ -0,0 +1,66 @@
import React from "react";
import {singleEntryToHAR} from "./utils";
import styles from './style/HarEntryDetailed.module.sass';
import HAREntryViewer from "./HarEntryViewer/HAREntryViewer";
import {makeStyles} from "@material-ui/core";
import StatusCode from "./StatusCode";
import {EndpointPath} from "./EndpointPath";
const useStyles = makeStyles(() => ({
entryTitle: {
display: 'flex',
minHeight: 46,
maxHeight: 46,
alignItems: 'center',
marginBottom: 8
}
}));
interface HarEntryDetailedProps {
harEntry: any;
classes?: any;
}
export const formatSize = (n: number) => n > 1000 ? `${Math.round(n / 1000)}KB` : `${n} B`;
const HarEntryTitle: React.FC<any> = ({har}) => {
const classes = useStyles();
const {log: {entries}} = har;
const {response, request, timings: {receive}} = entries[0];
const {method, url, postData} = request;
const {status, statusText, bodySize} = response;
return <div className={classes.entryTitle}>
{status && <div style={{marginRight: 8}}>
<StatusCode statusCode={status}/>
</div>}
<div style={{flexGrow: 1, overflow: 'hidden'}}>
<EndpointPath method={method} path={url}/>
</div>
<div style={{margin: "0 24px", opacity: 0.5}}>{formatSize(bodySize)}</div>
<div style={{marginRight: 24}}>{status} {statusText}</div>
<div style={{opacity: 0.5}}>{Math.round(receive)}ms</div>
</div>;
};
export const HAREntryDetailed: React.FC<HarEntryDetailedProps> = ({classes, harEntry}) => {
const har = singleEntryToHAR(harEntry);
// const contractVirtualizationStore = useContractVirtualizationStore();
// const contractVirtualizationDemoEnabled = useFeature('contractVirtualizationDemo');
return <>
{har && <HarEntryTitle har={har}/>}
<>
{har && <HAREntryViewer
harObject={har}
className={classes?.root ?? styles.har}
// isResponseMocked={contractVirtualizationDemoEnabled && contractVirtualizationStore.virtualServices.value.indexOf(har?.log?.entries[0]?.service) > -1}
// showTitle={!extendedTitle}
/>}
{/*{(harEntry?.isLoading) && <LoadingIndicator className={styles.loader}/>}*/}
{/*{(harEntry?.isError) && <div>Error loading HAR entry</div>}*/}
</>
</>
};

View File

@ -0,0 +1,72 @@
.title
display: flex
align-items: center
font-weight: 800
color: white
.button
display: flex
align-content: center
justify-content: space-around
width: .75rem
height: .75rem
border-radius: .25rem
border: solid 1px #344073
font-size: .75rem
line-height: 0.92
margin-right: .5rem
font-weight: 800
color: #627ef7
&.expanded
@extend .button
line-height: .75rem
background: #344073
color: white
.dataLine
font-weight: 600
font-size: .75rem
line-height: 1.2
margin: .3rem 0
.dataKey
text-transform: capitalize
color: white
margin: 0 0.5rem 0 0
text-align: right
overflow: hidden
text-overflow: ellipsis
width: 1%
max-width: 15rem
.dataValue
color: rgba(255, 255, 255, 0.5)
margin: 0
> span:first-child
word-break: break-all
max-width: calc(100% - 1.5rem)
> span:nth-child(2)
border-radius: .2rem
background-color: #344073
display: block
margin-left: .5rem
margin-right: 0
transition: all .3s
width: 1rem
height: 1rem
&:hover
background-color: #42518f
img
position: relative
width: 100%
.collapsibleContainer
border-top: 1px solid #171C30
padding: 0 1rem 1rem 1rem
background: none
table
width: 100%
tr td:first-child
white-space: nowrap
padding-right: .5rem

View File

@ -0,0 +1,140 @@
import styles from "./HAREntrySections.module.sass";
import React, {useState} from "react";
import {SyntaxHighlighter} from "../SyntaxHighlighter/index";
import CollapsibleContainer from "../CollapsibleContainer";
import FancyTextDisplay from "../FancyTextDisplay";
import Checkbox from "../Checkbox";
interface HAREntryViewLineProps {
label: string;
value: number | string;
}
const HAREntryViewLine: React.FC<HAREntryViewLineProps> = ({label, value}) => {
return label && value && <tr className={styles.dataLine}>
<td className={styles.dataKey}>{label}</td>
<td>
<FancyTextDisplay
className={styles.dataValue}
text={value}
applyTextEllipsis={false}
flipped={true}
displayIconOnMouseOver={true}
/>
</td>
</tr> || null;
}
interface HAREntrySectionCollapsibleTitleProps {
title: string;
isExpanded: boolean;
}
const HAREntrySectionCollapsibleTitle: React.FC<HAREntrySectionCollapsibleTitleProps> = ({title, isExpanded}) => {
return <div className={styles.title}>
<span className={`${styles.button} ${isExpanded ? styles.expanded : ''}`}>
{isExpanded ? '-' : '+'}
</span>
<span>{title}</span>
</div>
}
interface HAREntrySectionContainerProps {
title: string;
}
export const HAREntrySectionContainer: React.FC<HAREntrySectionContainerProps> = ({title, children}) => {
const [expanded, setExpanded] = useState(true);
return <CollapsibleContainer
className={styles.collapsibleContainer}
isExpanded={expanded}
onClick={() => setExpanded(!expanded)}
title={<HAREntrySectionCollapsibleTitle title={title} isExpanded={expanded}/>}
>
{children}
</CollapsibleContainer>
}
interface HAREntryBodySectionProps {
content: any;
encoding?: string;
contentType?: string;
}
export const HAREntryBodySection: React.FC<HAREntryBodySectionProps> = ({
content,
encoding,
contentType,
}) => {
const MAXIMUM_BYTES_TO_HIGHLIGHT = 10000; // The maximum of chars to highlight in body, in case the response can be megabytes
const supportedLanguages = [['html', 'html'], ['json', 'json'], ['application/grpc', 'json']]; // [[indicator, languageToUse],...]
const jsonLikeFormats = ['json', '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;
if (jsonLikeFormats.some(format => content?.mimeType?.indexOf(format) > -1)) {
try {
return JSON.stringify(JSON.parse(bodyBuf), null, 2);
} catch (error) {
console.error(error);
}
}
return bodyBuf;
}
const getLanguage = (mimetype) => {
const chunk = content.text?.slice(0, 100);
if (chunk.indexOf('html') > 0 || chunk.indexOf('HTML') > 0) return supportedLanguages[0][1];
const language = supportedLanguages.find(el => (mimetype + contentType).indexOf(el[0]) > -1);
return language ? language[1] : 'default';
}
return <React.Fragment>
{content && content.text?.length > 0 && <HAREntrySectionContainer title='Body'>
<table>
<tbody>
<HAREntryViewLine label={'Mime type'} value={content?.mimeType}/>
<HAREntryViewLine label={'Encoding'} value={encoding}/>
</tbody>
</table>
<div style={{display: 'flex', alignItems: 'center', alignContent: 'center', margin: "5px 0"}} onClick={() => setIsWrapped(!isWrapped)}>
<div style={{paddingTop: 3}}>
<Checkbox checked={isWrapped} onToggle={() => {}}/>
</div>
<span style={{marginLeft: '.5rem', color: "white"}}>Wrap text</span>
</div>
<SyntaxHighlighter
isWrapped={isWrapped}
code={formatTextBody(content.text)}
language={content?.mimeType ? getLanguage(content.mimeType) : 'default'}
/>
</HAREntrySectionContainer>}
</React.Fragment>
}
interface HAREntrySectionProps {
title: string,
arrayToIterate: any[],
}
export const HAREntryTableSection: React.FC<HAREntrySectionProps> = ({title, arrayToIterate}) => {
return <React.Fragment>
{
arrayToIterate && arrayToIterate.length > 0 ?
<HAREntrySectionContainer title={title}>
<table>
<tbody>
{arrayToIterate.map(({name, value}, index) => <HAREntryViewLine key={index} label={name}
value={value}/>)}
</tbody>
</table>
</HAREntrySectionContainer> : <span/>
}
</React.Fragment>
}

View File

@ -0,0 +1,56 @@
.harEntry
font-family: "Source Sans Pro", Lucida Grande, Tahoma, sans-serif
height: 100%
width: 100%
h3,
h4
font-family: "Source Sans Pro", Lucida Grande, Tahoma, sans-serif
.header
background-color: rgb(55, 65, 111)
padding: 0.5rem .75rem .65rem .75rem
border-top-left-radius: 0.25rem
border-top-right-radius: 0.25rem
display: flex
font-size: .75rem
align-items: center
.description
min-width: 25rem
display: flex
align-items: center
justify-content: space-between
.method
padding: 0 .25rem
font-size: 0.75rem
font-weight: bold
border-radius: 0.25rem
border: 0.0625rem solid rgba(255, 255, 255, 0.16)
margin-right: .5rem
> span
margin-left: .5rem
.timing
border-left: 1px solid #627ef7
margin-left: .3rem
padding-left: .3rem
color: #627ef7
.headerClickable
cursor: pointer
&:hover
background: lighten(rgb(55, 65, 111), 10%)
border-top-left-radius: 0
border-top-right-radius: 0
.body
background: #090b14
color: rgb(18, 19, 27)
.bodyHeader
padding: 0 1rem
.endpointURL
font-size: .75rem
display: block
color: #627ef7
text-decoration: none
font-weight: 600
margin-bottom: .5rem

View File

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

View File

@ -0,0 +1,161 @@
import React, {useEffect} from "react";
import styles from './style/HarFilters.module.sass';
import {HARFilterSelect} from "./HARFilterSelect";
import {TextField} from "@material-ui/core";
export const HarFilters: React.FC = () => {
return <div className={styles.container}>
<ServiceFilter/>
<MethodFilter/>
<StatusTypesFilter/>
<SourcesFilter/>
<FetchModeFilter/>
<PathFilter/>
</div>;
};
const _toUpperCase = v => v.toUpperCase();
const FilterContainer: React.FC = ({children}) => {
return <div className={styles.filterContainer}>
{children}
</div>;
};
const ServiceFilter: React.FC = () => {
const providerIds = []; //todo
const selectedServices = []; //todo
return <FilterContainer>
<HARFilterSelect
items={providerIds}
value={selectedServices}
onChange={(val) => {
//todo: harStore.updateFilter({toggleService: val})
}}
allowMultiple={true}
label={"Services"}
transformDisplay={_toUpperCase}
/>
</FilterContainer>
};
const BROWSER_SOURCE = "_BROWSER_";
const SourcesFilter: React.FC = () => {
const sources = []; //todo
const selectedSource = null; //todo
useEffect(() => {
//todo: fetch sources
}, []);
return <FilterContainer>
<HARFilterSelect
items={sources}
value={selectedSource}
onChange={(val) => {
//todo: harStore.updateFilter({toggleSource: val});
}}
allowMultiple={true}
label={"Sources"}
transformDisplay={item => item === BROWSER_SOURCE ? "BROWSER" : item.toUpperCase()}
/>
</FilterContainer>
};
enum HARFetchMode {
UP_TO_REVISION = "Up to revision",
ALL = "All",
QUEUED = "Unprocessed"
}
const FetchModeFilter: React.FC = () => {
const selectedHarFetchMode = null;
return <FilterContainer>
<HARFilterSelect
items={Object.values(HARFetchMode)}
value={selectedHarFetchMode}
onChange={(val) => {
// selectedModelStore.har.setHarFetchMode(val);
// selectedModelStore.har.data.reset();
// selectedModelStore.har.data.fetch();
//todo
}}
label={"Processed"}
/>
</FilterContainer>
};
enum HTTPMethod {
GET = "get",
PUT = "put",
POST = "post",
DELETE = "delete",
OPTIONS="options",
PATCH = "patch"
}
const MethodFilter: React.FC = () => {
const selectedMethods = [];
return <FilterContainer>
<HARFilterSelect
items={Object.values(HTTPMethod)}
allowMultiple={true}
value={selectedMethods}
onChange={(val) => {
// harStore.updateFilter({toggleMethod: val}) todo
}}
transformDisplay={_toUpperCase}
label={"Methods"}
/>
</FilterContainer>;
};
enum StatusType {
SUCCESS = "success",
ERROR = "error"
}
const StatusTypesFilter: React.FC = () => {
const selectedStatusTypes = [];
return <FilterContainer>
<HARFilterSelect
items={Object.values(StatusType)}
allowMultiple={true}
value={selectedStatusTypes}
onChange={(val) => {
// harStore.updateFilter({toggleStatusType: val}) todo
}}
transformDisplay={_toUpperCase}
label="Status"
/>
</FilterContainer>;
};
// TODO path search is inclusive of the qs -> we want to avoid this - TRA-1681
const PathFilter: React.FC = () => {
const onFilterChange = (value) => {
// harStore.updateFilter({setPathSearch: value}); todo
}
return <FilterContainer>
<div className={styles.filterLabel}>Path</div>
<div>
<TextField variant="outlined" className={styles.filterText} style={{minWidth: '150px'}} onKeyDown={(e: any) => e.key === "Enter" && onFilterChange(e.target.value)}/>
</div>
</FilterContainer>;
};

View File

@ -0,0 +1,66 @@
import React, {useEffect, useState} from "react";
import {HarFilters} from "./HarFilters";
import {HarEntriesList} from "./HarEntriesList";
import {Box, makeStyles} from "@material-ui/core";
import "./style/HarPage.sass";
import styles from './style/HarEntriesList.module.sass';
import {HAREntryDetailed} from "./HarEntryDetailed";
import {HarPaging} from "./HarPaging";
const useLayoutStyles = makeStyles(() => ({
details: {
flex: "0 0 50%",
width: "45vw",
backgroundColor: "#171c30",
padding: "12px 24px",
},
harViewer: {
display: 'flex',
overflowY: 'auto',
height: "calc(100% - 58px)",
}
}));
export const HarPage: React.FC = () => {
const classes = useLayoutStyles();
const [entries, setEntries] = useState([] as any);
const [focusedEntryId, setFocusedEntryId] = useState(null);
const [selectedHarEntry, setSelectedHarEntry] = useState(null);
useEffect(() => {
fetch("http://localhost:8899/api/entries")
.then(response => response.json())
.then(data => {setEntries(data); setFocusedEntryId(data[0]?.id)});
}, []);
useEffect(() => {
if(!focusedEntryId) return;
fetch(`http://localhost:8899/api/entries/${focusedEntryId}`)
.then(response => response.json())
.then(data => setSelectedHarEntry(data));
},[focusedEntryId])
return (
<div className="HarPage">
<div className="HarPage-Container">
<div className="HarPage-ListContainer">
{/*<HarFilters />*/}
<div className={styles.container}>
<HarEntriesList entries={entries} focusedEntryId={focusedEntryId} setFocusedEntryId={setFocusedEntryId}/>
{/*<Box flexGrow={0} flexShrink={0}>*/}
{/* {!harStore.data.isFirstLoading &&*/}
{/* <HarPaging showPageNumber />*/}
{/* }*/}
{/*</Box>*/}
</div>
</div>
<div className={classes.details}>
<HAREntryDetailed harEntry={selectedHarEntry} classes={{root: classes.harViewer}}/>
</div>
</div>
</div>
)
};

View File

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

View File

@ -0,0 +1,88 @@
import {ReactComponent as DefaultIconDown} from '../../../assets/default_icon_down.svg';
import {MenuItem, Select as MUISelect} from '@material-ui/core';
import React from 'react';
import {SelectProps as MUISelectProps} from '@material-ui/core/Select/Select';
import styles from './style/Select.module.sass';
const ALL_KEY= 'All';
const menuProps: any = {
anchorOrigin: {
vertical: "bottom",
horizontal: "left"
},
transformOrigin: {
vertical: "top",
horizontal: "left"
},
getContentAnchorEl: null
};
// icons styles are not overwritten from the Props, only as a separate object
const classes = {icon: styles.icon, selectMenu: styles.list};
const defaultProps = {
MenuProps: menuProps,
IconComponent: DefaultIconDown
};
export interface SelectProps extends MUISelectProps {
onChange: (string) => void;
value: string | string[];
ellipsis?: boolean;
labelOnTop?: boolean;
className?: string;
labelClassName?: string;
trimItemsWhenMultiple?: boolean;
transformDisplay?: (string) => string;
}
export const Select: React.FC<SelectProps> = ({
label,
value,
onChange,
transformDisplay,
ellipsis = true,
multiple,
labelOnTop = false,
children,
className,
labelClassName,
trimItemsWhenMultiple,
...props
}) => {
let _value = value;
const _onChange = (_, item) => {
const value = item.props.value;
value === ALL_KEY ? onChange(ALL_KEY) : onChange(value);
}
if (multiple && (!_value || _value.length === 0)) _value = [ALL_KEY];
const transformItem: (i: string) => string = transformDisplay ? transformDisplay : i => i;
const renderValue = multiple
? (item: any[]) => <span className={ellipsis ? 'ellipsis' : ''}>{
trimItemsWhenMultiple && item.length > 1 ?
transformItem(`${item[item.length-1]} (+${item.length - 1})`):
item?.map(transformItem).join(",")
}</span>
: null;
return <div className={`select ${labelOnTop ? 'labelOnTop' : ''} ${className ? className : ''}`}>
{label && <div className={`selectLabel ${labelClassName ? labelClassName : ''}`}>{label}</div>}
<MUISelect
{...Object.assign({}, defaultProps, props, {
classes,
value: _value,
renderValue,
multiple,
onChange: _onChange,
})}
>
{multiple && <MenuItem key={ALL_KEY} value={ALL_KEY}>{transformItem(ALL_KEY)}</MenuItem>}
{children}
</MUISelect>
</div>
}

View File

@ -0,0 +1,28 @@
import React from "react";
import styles from './style/StatusCode.module.sass';
enum StatusCodeClassification {
SUCCESS = "success",
FAILURE = "failure",
NEUTRAL = "neutral"
}
interface HAREntryProps {
statusCode: number
}
const StatusCode: React.FC<HAREntryProps> = ({statusCode}) => {
let classification = StatusCodeClassification.NEUTRAL;
if (statusCode >= 200 && statusCode <= 399) {
classification = StatusCodeClassification.SUCCESS;
} else if (statusCode >= 400) {
classification = StatusCodeClassification.FAILURE;
}
return <span className={`${styles[classification]} ${styles.base}`}>{statusCode}</span>
};
export default StatusCode;

View File

@ -0,0 +1,154 @@
export const up9Style = {
"code[class*=\"language-\"]": {
"color": "#fff",
"textShadow": "0 1px rgba(0, 0, 0, 0.3)",
"fontFamily": "Inconsolata, Monaco, Consolas, 'Courier New', Courier, monospace",
"direction": "ltr",
"textAlign": "left",
"whiteSpace": "pre",
"wordSpacing": "normal",
"wordBreak": "normal",
"lineHeight": "1.5",
"MozTabSize": "4",
"OTabSize": "4",
"tabSize": "4",
"padding": "1rem",
"WebkitHyphetokenns": "none",
"MozHyphens": "none",
"msHyphens": "none",
"hyphens": "none"
},
"pre[class*=\"language-\"]": {
"color": "#fff",
"textShadow": "0 1px rgba(0, 0, 0, 0.3)",
"fontFamily": "Inconsolata, Monaco, Consolas, 'Courier New', Courier, monospace",
"direction": "ltr",
"textAlign": "left",
"whiteSpace": "pre",
"wordSpacing": "normal",
"wordBreak": "normal",
"lineHeight": "1.2",
"MozTabSize": "4",
"OTabSize": "4",
"tabSize": "4",
"WebkitHyphens": "none",
"MozHyphens": "none",
"msHyphens": "none",
"hyphens": "none",
"padding": "0",
"margin": ".5em 0",
"overflow": "auto",
"borderRadius": "0.3em",
"background": "rgb(38, 46, 77)"
},
":not(pre) > code[class*=\"language-\"]": {
"background": "rgb(38, 46, 77)",
"padding": ".1em",
"borderRadius": ".3em"
},
"comment": {
"color": "#5d6aa0"
},
"prolog": {
"color": "#fff"
},
"doctype": {
"color": "#fff"
},
"cdata": {
"color": "#fff"
},
"punctuation": {
"color": "#fff"
},
".namespace": {
"Opacity": ".7"
},
"property": {
"color": "#627ef7"
},
"keyword": {
"color": "#627ef7"
},
"tag": {
"color": "#627ef7"
},
"class-name": {
"color": "#3eb545",
"textDecoration": "underline"
},
"boolean": {
"color": "#3eb545"
},
"constant": {
"color": "#3eb545"
},
"symbol": {
"color": "#ff3a30"
},
"deleted": {
"color": "#ff3a30"
},
"number": {
"color": "#ff16f7"
},
"selector": {
"color": "rgb(9,224,19)"
},
"attr-name": {
"color": "rgb(9,224,19)"
},
"string": {
"color": "rgb(9,224,19)"
},
"char": {
"color": "rgb(9,224,19)"
},
"builtin": {
"color": "rgb(9,224,19)"
},
"inserted": {
"color": "rgb(9,224,19)"
},
"variable": {
"color": "#C6C5FE"
},
"operator": {
"color": "#EDEDED"
},
"entity": {
"color": "#fdab2b",
"cursor": "help"
},
"url": {
"color": "#96CBFE"
},
".language-css .token.string": {
"color": "#87C38A"
},
".style .token.string": {
"color": "#87C38A"
},
"atrule": {
"color": "#fdab2b"
},
"attr-value": {
"color": "#f8c575"
},
"function": {
"color": "#fdab2b"
},
"regex": {
"color": "#fab248"
},
"important": {
"color": "#fd971f",
"fontWeight": "bold"
},
"bold": {
"fontWeight": "bold"
},
"italic": {
"fontStyle": "italic"
}
};

View File

@ -0,0 +1,37 @@
.highlighterContainer {
&.fitScreen {
pre {
max-height: 90vh;
overflow: auto;
}
}
pre {
code {
font-size: 0.75rem;
&:first-child {
margin-right: 0.75rem;
background: rgb(41, 48, 83);
.react-syntax-highlighter-line-number {
color: rgb(98, 126, 247);
}
}
&:last-child {
display: block;
}
}
}
}
.wrapped{
pre {
code {
&:last-child {
white-space: pre-wrap!important
}
}
}
}

View File

@ -0,0 +1,30 @@
import React from 'react';
import {Prism as SyntaxHighlighterContainer} from 'react-syntax-highlighter';
import {up9Style} from './highlighterStyle'
import './index.scss';
interface Props {
code: string;
style?: any;
showLineNumbers?: boolean;
className?: string;
language?: string;
isWrapped?: boolean;
}
export const SyntaxHighlighter: React.FC<Props> = ({
code,
style = up9Style,
showLineNumbers = true,
className,
language = 'python',
isWrapped = false,
}) => {
return <div className={`highlighterContainer ${className ? className : ''} ${isWrapped ? 'wrapped' : ''}`}>
<SyntaxHighlighterContainer language={language} style={style} showLineNumbers={showLineNumbers}>
{code ?? ""}
</SyntaxHighlighterContainer>
</div>;
};
export default SyntaxHighlighter;

View File

@ -0,0 +1,96 @@
import Tooltip from "./Tooltip";
import React from "react";
import {makeStyles} from '@material-ui/core/styles';
interface Tab {
tab: string,
hidden?: boolean,
disabled?: boolean,
disabledMessage?: string,
highlight?: boolean,
badge?: any,
}
interface Props {
classes?: any,
tabs: Tab[],
currentTab: string,
onChange: (string) => void,
leftAligned?: boolean,
dark?: boolean,
}
const useTabsStyles = makeStyles((theme) => ({
root: {
height: 40,
paddingTop: 15
},
tab: {
display: 'inline-block',
textTransform: 'uppercase',
color: theme.palette.primary.main,
cursor: 'pointer',
},
tabsAlignLeft: {
textAlign: 'left'
},
active: {
fontWeight: theme.typography.fontWeightBold,
color: theme.palette.common.white,
cursor: 'unset',
borderBottom: "2px solid " + theme.palette.common.white,
paddingBottom: 6,
"&.dark": {
color: theme.palette.common.black,
borderBottom: "2px solid " + theme.palette.common.black,
}
},
disabled: {
color: theme.palette.primary.dark,
cursor: 'unset'
},
highlight: {
color: theme.palette.primary.light,
},
separator: {
borderRight: "1px solid " + theme.palette.primary.dark,
height: 20,
verticalAlign: 'middle',
margin: '0 20px'
}
}));
const Tabs: React.FC<Props> = ({classes={}, tabs, currentTab, onChange, leftAligned, dark}) => {
const _classes = {...useTabsStyles(), ...classes};
return <div className={`${_classes.root} ${leftAligned ? _classes.tabsAlignLeft : ''}`}>
{tabs.filter((tab) => !tab.hidden).map(({tab, disabled, disabledMessage, highlight, badge}, index) => {
const active = currentTab === tab;
const tabLink = <a
key={tab}
className={`${_classes.tab} ${active ? _classes.active : ''} ${disabled ? _classes.disabled : ''} ${highlight ? _classes.highlight : ''} ${dark ? 'dark' : ''}`}
onClick={() => !disabled && onChange(tab)}
>
{tab}
{React.isValidElement(badge) && badge}
</a>;
return <span key={tab}>
{disabled && disabledMessage ? <Tooltip title={disabledMessage} isSimple>{tabLink}</Tooltip> : tabLink}
{index < tabs.length - 1 && <span className={_classes.tab + ' ' + _classes.separator} key={tab + '_sepparator'}></span>}
</span>;
})}
</div>;
}
export default Tabs;

View File

@ -0,0 +1,48 @@
import {Tooltip as MUITooltip, Fade, TooltipProps as MUITooltipProps, makeStyles} from "@material-ui/core";
import React from "react";
export interface TooltipProps extends MUITooltipProps {
variant?: 'default' | 'wide' | 'fit';
isSimple?: boolean;
}
export type TooltipPlacement = 'bottom-end' | 'bottom-start' | 'bottom' | 'left-end' | 'left-start' | 'left' | 'right-end' | 'right-start' | 'right' | 'top-end' | 'top-start' | 'top';
const styles = {
default: {
maxWidth: 300
},
wide: {
maxWidth: 700
},
fit: {
maxWidth: '100%'
}
};
const useStyles = makeStyles((theme) => styles);
const Tooltip: React.FC<TooltipProps> = (props) => {
const {isSimple, ..._props} = props;
const classes = useStyles(props.variant);
const variant = props.variant ?? 'default';
const backgroundClass = isSimple ? "" : "noBackground"
return (
<MUITooltip
classes={{tooltip: `${backgroundClass} ` + classes[variant]}}
interactive={true}
enterDelay={200}
TransitionComponent={Fade}
{..._props}
>
{props.children ?? <div/>}
</MUITooltip>
);
};
export default Tooltip;

View File

@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
<defs>
<style>
.cls-1{fill:#fff;stroke:#707070;opacity:0}.cls-2{fill:#627ef7}.cls-3{stroke:none}.cls-4{fill:none}
</style>
</defs>
<g id="icon_down_diagonal" transform="translate(-1304 -166)">
<g id="Rectangle_924" class="cls-1" data-name="Rectangle 924">
<path d="M0 0h22v22H0z" class="cls-3" transform="translate(1304 166)"/>
<path d="M.5.5h21v21H.5z" class="cls-4" transform="translate(1304 166)"/>
</g>
<path id="icon_down" d="M5.117 0H3.07v3.07H0v2.047h5.117V0z" class="cls-2" transform="translate(1312.46 174.491)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 711 B

View File

@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
<defs>
<style>
.cls-1,.cls-2,.cls-5{fill:#fff}.cls-1{opacity:0}.cls-3,.cls-6{fill:none}.cls-3{stroke:#8f9bb2}.cls-4,.cls-5{stroke:none}
</style>
</defs>
<g id="duplicate" transform="rotate(180 11 11)">
<path id="Rectangle_1845" d="M0 0h22v22H0z" class="cls-1" data-name="Rectangle 1845"/>
<g id="Group_4275" data-name="Group 4275" transform="translate(4 4)">
<g id="Subtraction_10" class="cls-2" data-name="Subtraction 10">
<path d="M5 8.498H2c-.827 0-1.5-.673-1.5-1.5v-5c0-.827.673-1.5 1.5-1.5h.611v2.409c0 1.378 1.122 2.5 2.5 2.5h1.39v1.591c0 .827-.674 1.5-1.5 1.5z" class="cls-4" transform="translate(1 5.202)"/>
<path d="M5 7.998a1 1 0 0 0 1-1V5.907h-.889c-1.654 0-3-1.346-3-3V.998H2a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h3m0 1H2c-1.103 0-2-.897-2-2v-5c0-1.103.897-2 2-2H3.11v2.909c0 1.103.897 2 2 2h1.89v2.091c0 1.103-.898 2-2 2z" class="cls-5" transform="translate(1 5.202)"/>
</g>
<g id="Rectangle_680" class="cls-3" data-name="Rectangle 680" transform="translate(4)">
<rect width="9" height="11" class="cls-4" rx="2"/>
<rect width="8" height="10" x=".5" y=".5" class="cls-6" rx="1.5"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,3 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.2427 11.8285L14.0711 9.00003L15.4853 10.4142L11.2427 14.6569L11.2426 14.6569L9.82846 13.2427L7 10.4142L8.41421 9L11.2427 11.8285Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 302 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
<g id="prefix__icon_expand" fill="#627ef7" transform="rotate(0 11 11)">
<path id="prefix__icon_down" d="M5.117 0H3.07v3.07H0v2.047h5.117V0z" transform="rotate(-45 16.54 -2.201)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 292 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
<g id="prefix__icon_expand" fill="#627ef7" transform="rotate(180 11 11)">
<path id="prefix__icon_down" d="M5.117 0H3.07v3.07H0v2.047h5.117V0z" transform="rotate(-45 16.54 -2.201)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 294 B

View File

@ -0,0 +1,34 @@
.CollapsibleContainer-Header
display: flex
align-items: center
cursor: pointer
min-height: 40px
.CollapsibleContainer-Header-Sticky
background: #1f253f
position: sticky
top: 0
z-index: 1
.CollapsibleContainer-Bigger
padding: 10px
font-size: 14px !important
.CollapsibleContainer-ExpandCollapseButton
margin-left: auto
padding-right: 2%
.CollapsibleContainer-Expanded
//background-color: rgba(98, 126, 247, 0.12)
min-height: 40px
.CollapsibleContainer-Title
font-weight: 600
font-family: 'Source Sans Pro', sans-serif
font-size: 12px
.CollapsibleContainer-Expanded .CollapsibleContainer-Title
color: rgba(186, 199, 255, 1)
.CollapsibleContainer-Collapsed .CollapsibleContainer-Title
color: rgba(186, 199, 255, 0.75)

View File

@ -0,0 +1,8 @@
.container
display: flex
align-items: center
.path
text-overflow: ellipsis
overflow: hidden
white-space: nowrap

View File

@ -0,0 +1,41 @@
.FancyTextDisplay-Container
display: flex
align-items: center
&.displayIconOnMouseOver
.FancyTextDisplay-Icon
opacity: 0
pointer-events: none
&:hover
.FancyTextDisplay-Icon
opacity: 1
pointer-events: all
.FancyTextDisplay-Icon
height: 22px
width: 22px
cursor: pointer
margin-right: 3px
&:hover
background-color: rgba(255, 255, 255, 0.06)
border-radius: 4px
&.FancyTextDisplay-ContainerEllipsis
.FancyTextDisplay-Text
text-align: left
text-overflow: ellipsis
overflow: hidden
white-space: nowrap
width: calc(100% - 30px)
.FancyTextDisplay-CopyNotifier
background-color: #4252a5
padding: 2px 5px
border-radius: 4px
position: absolute
transform: translate(0, -80%)
color: white
z-index: 1000

View File

@ -0,0 +1,12 @@
.list
overflow: scroll
display: flex
flex-grow: 1
flex-direction: column
.container
position: relative
display: flex
flex-direction: column
overflow: hidden
flex-grow: 1

View File

@ -0,0 +1,45 @@
.row
display: flex
background: #1c2133
min-height: 46px
max-height: 46px
align-items: center
padding: 0 8px
border-radius: 4px
cursor: pointer
border: solid 1px transparent
&:not(:first-child)
margin-top: 10px
&:hover
border: solid 1px lighten(#4253a5, 20%)
.rowSelected
border: solid 1px #4253a5
.service
text-overflow: ellipsis
overflow: hidden
white-space: nowrap
color: rgba(255, 255, 255, 0.5)
padding-left: 4px
padding-top: 3px
padding-right: 10px
display: flex
font-size: 12px
.timestamp
font-size: 12px
color: rgba(255, 255, 255, 0.5)
padding-left: 8px
padding-right: 8px
flex-shrink: 0
.endpointServiceContainer
color: white
display: flex
flex-direction: column
overflow: hidden
padding-right: 10px
padding-left: 10px
flex-grow: 1

View File

@ -0,0 +1,9 @@
.loader
margin: 30px auto 0
.har
display: flex
overflow: scroll
//padding: .75rem .75rem 0 .75rem
height: calc(100% - 1.75rem)
//width: calc(100% - 2rem)

View File

@ -0,0 +1,31 @@
.container
display: flex
flex-direction: row
align-items: center
min-height: 3rem
overflow-y: hidden
overflow-x: auto
padding: .5rem 0
.filterLabel
color: #8f9bb2
margin-right: 6px
font-size: 11px
margin-bottom: 4px
.icon
fill: #627ef7
.filterContainer
padding-right: 14px
display: flex
flex-direction: column
.filterText
input
padding: 4px 12px
background: #171922
border-radius: 12px
font-size: 12px
fieldset
border: none

View File

@ -0,0 +1,71 @@
.HarPage
width: 100%
display: flex
flex-direction: column
border-radius: 8px
overflow: hidden
flex-grow: 1
height: calc(100vh - 100px)
.HarPage-Header
display: flex
height: 2.5%
justify-content: space-between
align-items: center
padding: 18px 15px
.HarPage-Header-Image
width: 22px
height: 22px
.HarPage-Header-Text
margin-left: 10px
font-family: 'Source Sans Pro', serif
font-size: 14px
font-weight: bold
color: #f7f9fc
.HarPage-Header-Actions
margin-left: auto
.HarPage-Header-Actions-Image
width: 22px
height: 22px
cursor: pointer
padding-right: 1.2vw
margin-left: auto
transform: translate(0 ,25%)
.HarPage-Viewer
height: 96.5%
overflow: auto
> iframe
width: 100%
height: 96.5%
display: block
overflow-y: auto
.harContent
box-sizing: border-box
height: calc(100% - 60px)
overflow: scroll
.HarPage-Container
display: flex
flex-grow: 1
overflow: hidden
.HarPage-ListContainer
display: flex
flex-grow: 1
overflow: hidden
padding: 0 24px
flex-direction: column
.HarPage-DetailContainer
//flex-grow: 1
width: 45vw
background-color: #171c30
flex: 0 0 50%
padding: 12px 24px

View File

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

View File

@ -0,0 +1,5 @@
.icon
fill: #627ef7
.list
margin-top: 8px

View File

@ -0,0 +1,21 @@
.base
border-radius: 4px
width: 42px
height: 22px
font-size: 13px
display: inline-block
text-align: center
line-height: 22px
font-weight: 600
.neutral
background: gray
color: black
.success
background: #358645
color: white
.failure
background: #ff3a30
color: white

View File

@ -0,0 +1,27 @@
.protocol
border-radius: 4px
border: solid 1px #bcc6dd60
margin-left: 4px
padding: 1px 3px
color: #ffffff88
text-transform: uppercase
font-family: "Source Sans Pro", sans-serif
font-size: 11px
font-weight: bold
&.method
color: white
margin-right: 10px
&.filterPlate
border-color: #bcc6dd20
color: #a0b2ff
font-size: 10px
.noSelect
-webkit-touch-callout: none
-webkit-user-select: none
-khtml-user-select: none
-moz-user-select: none
-ms-user-select: none
user-select: none

View File

@ -0,0 +1,35 @@
export const singleEntryToHAR = (entry) => {
if (!entry) return null;
const modifiedEntry = {
...entry,
"startedDateTime": "2019-10-08T11:49:51.090+03:00",
"cache": {},
"timings": {
"blocked": -1,
"dns": -1,
"connect": -1,
"ssl": -1,
"send": -1,
"wait": -1,
"receive": -1
},
"time": -1
};
const har = {
log: {
entries: [modifiedEntry],
version: "1.2",
creator: {
"name": "Firefox",
"version": "69.0.1"
}
}
}
return har;
};
export const formatSize = (n: number) => n > 1000 ? `${Math.round(n / 1000)}KB` : `${n} B`;

View File

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

32
ui/src/index.sass Normal file
View File

@ -0,0 +1,32 @@
html,
body
height: 100%
body
font-family: 'Source Sans Pro', sans-serif
font-weight: 400
font-size: 90%
color: #fff
background-color: #313346
-webkit-font-smoothing: antialiased
-moz-osx-font-smoothing: grayscale
margin: 0
padding: 0
code
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace
// scroll-bar css
::-webkit-scrollbar
width: 8px
::-webkit-scrollbar-thumb
background-color: rgba(0,0,0,0.5)
border-radius: 16px
::-webkit-scrollbar-button
display: none
::-webkit-scrollbar-corner
display: none

11
ui/src/index.tsx Normal file
View File

@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.sass';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);

1
ui/src/react-app-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="react-scripts" />

27
ui/tsconfig.json Normal file
View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"downlevelIteration": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"src"
]
}