Traffic viewer
29
ui/.gitignore
vendored
Normal 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
48
ui/package.json
Normal 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
After Width: | Height: | Size: 3.8 KiB |
43
ui/public/index.html
Normal 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
After Width: | Height: | Size: 5.2 KiB |
BIN
ui/public/logo512.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
25
ui/public/manifest.json
Normal 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
@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
16
ui/src/App.tsx
Normal 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;
|
17
ui/src/components/Checkbox.tsx
Normal 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;
|
56
ui/src/components/CollapsibleContainer.tsx
Normal 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;
|
15
ui/src/components/EndpointPath.tsx
Normal 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>
|
||||
};
|
63
ui/src/components/FancyTextDisplay.tsx
Normal 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;
|
28
ui/src/components/HARFilterSelect.tsx
Normal 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>
|
||||
};
|
38
ui/src/components/HarEntriesList.tsx
Normal 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>
|
||||
</>;
|
||||
};
|
43
ui/src/components/HarEntry.tsx
Normal 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>
|
||||
</>
|
||||
};
|
66
ui/src/components/HarEntryDetailed.tsx
Normal 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>}*/}
|
||||
</>
|
||||
</>
|
||||
};
|
@ -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
|
140
ui/src/components/HarEntryViewer/HAREntrySections.tsx
Normal 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>
|
||||
}
|
56
ui/src/components/HarEntryViewer/HAREntryViewer.module.sass
Normal 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
|
72
ui/src/components/HarEntryViewer/HAREntryViewer.tsx
Normal 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;
|
161
ui/src/components/HarFilters.tsx
Normal 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>;
|
||||
};
|
||||
|
66
ui/src/components/HarPage.tsx
Normal 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>
|
||||
)
|
||||
};
|
27
ui/src/components/HarPaging.tsx
Normal 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>
|
||||
};
|
88
ui/src/components/Select.tsx
Normal 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>
|
||||
}
|
28
ui/src/components/StatusCode.tsx
Normal 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;
|
154
ui/src/components/SyntaxHighlighter/highlighterStyle.ts
Normal 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"
|
||||
}
|
||||
};
|
37
ui/src/components/SyntaxHighlighter/index.scss
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
30
ui/src/components/SyntaxHighlighter/index.tsx
Normal 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;
|
96
ui/src/components/Tabs.tsx
Normal 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;
|
48
ui/src/components/Tooltip.tsx
Normal 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;
|
14
ui/src/components/assets/collapsed.svg
Normal 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 |
20
ui/src/components/assets/duplicate.svg
Normal 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 |
3
ui/src/components/assets/expanded.svg
Normal 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 |
5
ui/src/components/assets/icon-next.svg
Normal 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 |
5
ui/src/components/assets/icon-prev.svg
Normal 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 |
34
ui/src/components/style/CollapsibleContainer.sass
Normal 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)
|
8
ui/src/components/style/EndpointPath.module.sass
Normal file
@ -0,0 +1,8 @@
|
||||
.container
|
||||
display: flex
|
||||
align-items: center
|
||||
|
||||
.path
|
||||
text-overflow: ellipsis
|
||||
overflow: hidden
|
||||
white-space: nowrap
|
41
ui/src/components/style/FancyTextDisplay.sass
Normal 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
|
||||
|
12
ui/src/components/style/HarEntriesList.module.sass
Normal 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
|
45
ui/src/components/style/HarEntry.module.sass
Normal 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
|
9
ui/src/components/style/HarEntryDetailed.module.sass
Normal 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)
|
31
ui/src/components/style/HarFilters.module.sass
Normal 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
|
71
ui/src/components/style/HarPage.sass
Normal 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
|
16
ui/src/components/style/HarPaging.module.sass
Normal 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
|
5
ui/src/components/style/Select.module.sass
Normal file
@ -0,0 +1,5 @@
|
||||
.icon
|
||||
fill: #627ef7
|
||||
|
||||
.list
|
||||
margin-top: 8px
|
21
ui/src/components/style/StatusCode.module.sass
Normal 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
|
27
ui/src/components/style/misc.module.sass
Normal 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
|
35
ui/src/components/utils.ts
Normal 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`;
|
10
ui/src/hooks/use-toggle.ts
Normal 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
@ -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
@ -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
@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
27
ui/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|