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

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" />