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"
|
||||||
|
]
|
||||||
|
}
|