Feature/UI/filters (#32)

* UI filters

* refactor

* Revert "refactor"

This reverts commit 70e7d4b6ac.

* remove recursive func
This commit is contained in:
lirazyehezkel 2021-05-06 17:40:35 +03:00 committed by GitHub
parent b9d0e0ee87
commit c7a20ed9c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 309 additions and 163 deletions

View File

@ -1,8 +1,9 @@
import {HarEntry} from "./HarEntry";
import React, {useEffect, useState} from "react";
import React, {useCallback, useEffect, useMemo, useState} from "react";
import styles from './style/HarEntriesList.module.sass';
import spinner from './assets/spinner.svg';
import ScrollableFeed from "react-scrollable-feed";
import {StatusType} from "./HarFilters";
interface HarEntriesListProps {
entries: any[];
@ -14,6 +15,9 @@ interface HarEntriesListProps {
setNoMoreDataTop: (flag: boolean) => void;
noMoreDataBottom: boolean;
setNoMoreDataBottom: (flag: boolean) => void;
methodsFilter: Array<string>;
statusFilter: Array<string>;
pathFilter: string
}
enum FetchOperator {
@ -21,91 +25,107 @@ enum FetchOperator {
GT = "gt"
}
export const HarEntriesList: React.FC<HarEntriesListProps> = ({entries, setEntries, focusedEntryId, setFocusedEntryId, connectionOpen, noMoreDataTop, setNoMoreDataTop, noMoreDataBottom, setNoMoreDataBottom}) => {
export const HarEntriesList: React.FC<HarEntriesListProps> = ({entries, setEntries, focusedEntryId, setFocusedEntryId, connectionOpen, noMoreDataTop, setNoMoreDataTop, noMoreDataBottom, setNoMoreDataBottom, methodsFilter, statusFilter, pathFilter}) => {
const [loadMoreTop, setLoadMoreTop] = useState(false);
const [isLoadingTop, setIsLoadingTop] = useState(false);
useEffect(() => {
if(loadMoreTop && !connectionOpen && !noMoreDataTop)
fetchData(FetchOperator.LT);
}, [loadMoreTop, connectionOpen, noMoreDataTop]);
useEffect(() => {
const list = document.getElementById('list').firstElementChild;
list.addEventListener('scroll', (e) => {
const el: any = e.target;
if(el.scrollTop === 0) {
setLoadMoreTop(true);
} else {
setLoadMoreTop(false);
}
});
}, []);
const fetchData = async (operator) => {
const filterEntries = useCallback((entry) => {
if(methodsFilter.length > 0 && !methodsFilter.includes(entry.method.toLowerCase())) return;
if(pathFilter && entry.path?.toLowerCase()?.indexOf(pathFilter) === -1) return;
if(statusFilter.includes(StatusType.SUCCESS) && entry.statusCode >= 400) return;
if(statusFilter.includes(StatusType.ERROR) && entry.statusCode < 400) return;
return entry;
},[methodsFilter, pathFilter, statusFilter])
const timestamp = operator === FetchOperator.LT ? entries[0].timestamp : entries[entries.length - 1].timestamp;
if(operator === FetchOperator.LT)
setIsLoadingTop(true);
const filteredEntries = useMemo(() => {
return entries.filter(filterEntries);
},[entries, filterEntries])
fetch(`http://localhost:8899/api/entries?limit=50&operator=${operator}&timestamp=${timestamp}`)
.then(response => response.json())
.then((data: any[]) => {
let scrollTo;
if(operator === FetchOperator.LT) {
if(data.length === 0) {
setNoMoreDataTop(true);
scrollTo = document.getElementById("noMoreDataTop");
} else {
scrollTo = document.getElementById(entries[0].id);
}
const newEntries = [...data, ...entries];
if(newEntries.length >= 1000) {
newEntries.splice(1000);
}
setEntries(newEntries);
setLoadMoreTop(false);
setIsLoadingTop(false)
if(scrollTo) {
scrollTo.scrollIntoView();
}
}
const fetchData = async (operator, timestamp) => {
const response = await fetch(`http://localhost:8899/api/entries?limit=50&operator=${operator}&timestamp=${timestamp}`);
return await response.json();
}
if(operator === FetchOperator.GT) {
if(data.length === 0) {
setNoMoreDataBottom(true);
}
scrollTo = document.getElementById(entries[entries.length -1].id);
let newEntries = [...entries, ...data];
if(newEntries.length >= 1000) {
setNoMoreDataTop(false);
newEntries = newEntries.slice(-1000);
}
setEntries(newEntries);
if(scrollTo) {
scrollTo.scrollIntoView({behavior: "smooth"});
}
}
});
};
const getOldEntries = useCallback(async () => {
setIsLoadingTop(true);
const data = await fetchData(FetchOperator.LT, entries[0].timestamp);
setLoadMoreTop(false);
let scrollTo;
if(data.length === 0) {
setNoMoreDataTop(true);
scrollTo = document.getElementById("noMoreDataTop");
} else {
scrollTo = document.getElementById(filteredEntries?.[0]?.id);
}
setIsLoadingTop(false);
const newEntries = [...data, ...entries];
if(newEntries.length >= 1000) {
newEntries.splice(1000);
}
setEntries(newEntries);
if(scrollTo) {
scrollTo.scrollIntoView();
}
},[setLoadMoreTop, setIsLoadingTop, entries, setEntries, filteredEntries, setNoMoreDataTop])
useEffect(() => {
if(!loadMoreTop || connectionOpen || noMoreDataTop) return;
getOldEntries();
}, [loadMoreTop, connectionOpen, noMoreDataTop, getOldEntries]);
const getNewEntries = async () => {
const data = await fetchData(FetchOperator.GT, entries[entries.length - 1].timestamp);
let scrollTo;
if(data.length === 0) {
setNoMoreDataBottom(true);
}
scrollTo = document.getElementById(filteredEntries?.[filteredEntries.length -1]?.id);
let newEntries = [...entries, ...data];
if(newEntries.length >= 1000) {
setNoMoreDataTop(false);
newEntries = newEntries.slice(-1000);
}
setEntries(newEntries);
if(scrollTo) {
scrollTo.scrollIntoView({behavior: "smooth"});
}
}
return <>
<div className={styles.list}>
<div id="list" className={styles.list}>
{isLoadingTop && <div style={{display: "flex", justifyContent: "center", marginBottom: 10}}><img alt="spinner" src={spinner} style={{height: 25}}/></div>}
{isLoadingTop && <div className={styles.spinnerContainer}>
<img alt="spinner" src={spinner} style={{height: 25}}/>
</div>}
<ScrollableFeed>
{noMoreDataTop && !connectionOpen && <div id="noMoreDataTop" style={{textAlign: "center", fontWeight: 600, color: "rgba(255,255,255,0.75)"}}>No more data available</div>}
{entries?.map(entry => <HarEntry key={entry.id}
{noMoreDataTop && !connectionOpen && <div id="noMoreDataTop" className={styles.noMoreDataAvailable}>No more data available</div>}
{filteredEntries.map(entry => <HarEntry key={entry.id}
entry={entry}
setFocusedEntryId={setFocusedEntryId}
isSelected={focusedEntryId === entry.id}/>)}
{!connectionOpen && !noMoreDataBottom && <div style={{width: "100%", display: "flex", justifyContent: "center", marginTop: 12, fontWeight: 600, color: "rgba(255,255,255,0.75)"}}>
<div className={styles.styledButton} onClick={() => fetchData(FetchOperator.GT)}>Fetch more entries</div>
{!connectionOpen && !noMoreDataBottom && <div className={styles.fetchButtonContainer}>
<div className={styles.styledButton} onClick={() => getNewEntries()}>Fetch more entries</div>
</div>}
</ScrollableFeed>
</div>
{entries?.length > 0 && <div className={styles.footer}>
<div><b>{entries?.length}</b> requests</div>
<div><b>{filteredEntries?.length !== entries.length && `${filteredEntries?.length} / `} {entries?.length}</b> requests</div>
<div>Started listening at <span style={{marginRight: 5, fontWeight: 600, fontSize: 13}}>{new Date(+entries[0].timestamp)?.toLocaleString()}</span></div>
</div>}
</div>

View File

@ -1,17 +1,24 @@
import React, {useEffect} from "react";
import React from "react";
import styles from './style/HarFilters.module.sass';
import {HARFilterSelect} from "./HARFilterSelect";
import {TextField} from "@material-ui/core";
import {ALL_KEY} from "./Select";
export const HarFilters: React.FC = () => {
interface HarFiltersProps {
methodsFilter: Array<string>;
setMethodsFilter: (methods: Array<string>) => void;
statusFilter: Array<string>;
setStatusFilter: (methods: Array<string>) => void;
pathFilter: string
setPathFilter: (val: string) => void;
}
export const HarFilters: React.FC<HarFiltersProps> = ({methodsFilter, setMethodsFilter, statusFilter, setStatusFilter, pathFilter, setPathFilter}) => {
return <div className={styles.container}>
<ServiceFilter/>
<MethodFilter/>
<StatusTypesFilter/>
<SourcesFilter/>
<FetchModeFilter/>
<PathFilter/>
<MethodFilter methodsFilter={methodsFilter} setMethodsFilter={setMethodsFilter}/>
<StatusTypesFilter statusFilter={statusFilter} setStatusFilter={setStatusFilter}/>
<PathFilter pathFilter={pathFilter} setPathFilter={setPathFilter}/>
</div>;
};
@ -23,77 +30,6 @@ const FilterContainer: React.FC = ({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",
@ -103,58 +39,80 @@ enum HTTPMethod {
PATCH = "patch"
}
const MethodFilter: React.FC = () => {
interface MethodFilterProps {
methodsFilter: Array<string>;
setMethodsFilter: (methods: Array<string>) => void;
}
const selectedMethods = [];
const MethodFilter: React.FC<MethodFilterProps> = ({methodsFilter, setMethodsFilter}) => {
const methodClicked = (val) => {
if(val === ALL_KEY) {
setMethodsFilter([]);
return;
}
if(methodsFilter.includes(val)) {
setMethodsFilter(methodsFilter.filter(method => method !== val))
} else {
setMethodsFilter([...methodsFilter, val]);
}
}
return <FilterContainer>
<HARFilterSelect
items={Object.values(HTTPMethod)}
allowMultiple={true}
value={selectedMethods}
onChange={(val) => {
// harStore.updateFilter({toggleMethod: val}) todo
}}
value={methodsFilter}
onChange={(val) => methodClicked(val)}
transformDisplay={_toUpperCase}
label={"Methods"}
/>
</FilterContainer>;
};
enum StatusType {
export enum StatusType {
SUCCESS = "success",
ERROR = "error"
}
const StatusTypesFilter: React.FC = () => {
interface StatusTypesFilterProps {
statusFilter: Array<string>;
setStatusFilter: (methods: Array<string>) => void;
}
const selectedStatusTypes = [];
const StatusTypesFilter: React.FC<StatusTypesFilterProps> = ({statusFilter, setStatusFilter}) => {
const statusClicked = (val) => {
if(val === ALL_KEY) {
setStatusFilter([]);
return;
}
setStatusFilter([val]);
}
return <FilterContainer>
<HARFilterSelect
items={Object.values(StatusType)}
allowMultiple={true}
value={selectedStatusTypes}
onChange={(val) => {
// harStore.updateFilter({toggleStatusType: val}) todo
}}
value={statusFilter}
onChange={(val) => statusClicked(val)}
transformDisplay={_toUpperCase}
label="Status"
/>
</FilterContainer>;
};
// TODO path search is inclusive of the qs -> we want to avoid this - TRA-1681
const PathFilter: React.FC = () => {
interface PathFilterProps {
pathFilter: string;
setPathFilter: (val: string) => void;
}
const onFilterChange = (value) => {
// harStore.updateFilter({setPathSearch: value}); todo
}
const PathFilter: React.FC<PathFilterProps> = ({pathFilter, setPathFilter}) => {
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)}/>
<TextField value={pathFilter} variant="outlined" className={styles.filterText} style={{minWidth: '150px'}} onChange={(e: any) => setPathFilter(e.target.value)}/>
</div>
</FilterContainer>;
};

View File

@ -1,5 +1,5 @@
import React, {useEffect, useRef, useState} from "react";
// import {HarFilters} from "./HarFilters";
import {HarFilters} from "./HarFilters";
import {HarEntriesList} from "./HarEntriesList";
import {makeStyles} from "@material-ui/core";
import "./style/HarPage.sass";
@ -43,6 +43,10 @@ export const HarPage: React.FC = () => {
const [noMoreDataTop, setNoMoreDataTop] = useState(false);
const [noMoreDataBottom, setNoMoreDataBottom] = useState(false);
const [methodsFilter, setMethodsFilter] = useState([]);
const [statusFilter, setStatusFilter] = useState([]);
const [pathFilter, setPathFilter] = useState("");
const ws = useRef(null);
const openWebSocket = () => {
@ -53,7 +57,6 @@ export const HarPage: React.FC = () => {
if(ws.current) {
ws.current.onmessage = e => {
console.log(connection);
if(!e?.data) return;
const entry = JSON.parse(e.data);
if(connection === ConnectionStatus.Paused) {
@ -98,18 +101,35 @@ export const HarPage: React.FC = () => {
}
}
const getConnectionTitle = () => {
switch (connection) {
case ConnectionStatus.Paused:
return "traffic paused";
case ConnectionStatus.Connected:
return "connected, waiting for traffic"
default:
return "not connected";
}
}
return (
<div className="HarPage">
<div style={{padding: "0 24px 24px 24px", display: "flex", alignItems: "center"}}>
<img style={{cursor: "pointer", marginRight: 15, height: 20}} alt="pause" src={connection === ConnectionStatus.Connected ? pauseIcon : playIcon} onClick={toggleConnection}/>
<div className="connectionText">
{connection === ConnectionStatus.Connected ? "connected, waiting for traffic" : "not connected"}
{getConnectionTitle()}
<div className={getConnectionStatusClass()}/>
</div>
</div>
{entries.length > 0 && <div className="HarPage-Container">
<div className="HarPage-ListContainer">
{/*<HarFilters />*/}
<HarFilters methodsFilter={methodsFilter}
setMethodsFilter={setMethodsFilter}
statusFilter={statusFilter}
setStatusFilter={setStatusFilter}
pathFilter={pathFilter}
setPathFilter={setPathFilter}
/>
<div className={styles.container}>
<HarEntriesList entries={entries}
setEntries={setEntries}
@ -119,7 +139,11 @@ export const HarPage: React.FC = () => {
noMoreDataBottom={noMoreDataBottom}
setNoMoreDataBottom={setNoMoreDataBottom}
noMoreDataTop={noMoreDataTop}
setNoMoreDataTop={setNoMoreDataTop}/>
setNoMoreDataTop={setNoMoreDataTop}
methodsFilter={methodsFilter}
statusFilter={statusFilter}
pathFilter={pathFilter}
/>
</div>
</div>
<div className={classes.details}>

View File

@ -1,10 +1,10 @@
import {ReactComponent as DefaultIconDown} from '../../../assets/default_icon_down.svg';
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';
export const ALL_KEY= 'All';
const menuProps: any = {
anchorOrigin: {

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="7.237" height="7.237" viewBox="0 0 7.237 7.237" fill="white">
<path id="icon_down" d="M5.117 0H3.07v3.07H0v2.047h5.117V0z" transform="rotate(45 1.809 4.367)"/>
</svg>

After

Width:  |  Height:  |  Size: 218 B

View File

@ -0,0 +1,3 @@
.HARSelectLabel
color: #8f9bb2
font-size: 11px

View File

@ -36,3 +36,21 @@
.styledButton:hover
border: 1px solid #627ef7
background-color: rgba(255, 255, 255, 0.06)
.spinnerContainer
display: flex
justify-content: center
margin-bottom: 10px
.noMoreDataAvailable
text-align: center
font-weight: 600
color: rgba(255,255,255,0.75)
.fetchButtonContainer
width: 100%
display: flex
justify-content: center
margin-top: 12px
font-weight: 600
color: rgba(255,255,255,0.75)

View File

@ -16,7 +16,8 @@
border: solid 1px lighten(#4253a5, 20%)
.rowSelected
border: solid 1px #4253a5
background: #293053
border: 1px #ffffff61 solid
.service
text-overflow: ellipsis

View File

@ -27,5 +27,6 @@
background: #171922
border-radius: 12px
font-size: 12px
color: white
fieldset
border: none

View File

@ -16,6 +16,124 @@ body
code
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace
.uppercase
text-transform: uppercase
/****
* Button
***/
button
span
line-height: 1
&:not(.MuiFab-root)
&.MuiButtonBase-root
box-sizing: border-box
font-weight: 500
line-height: 1
border-radius: 20px
letter-spacing: 0.02857em
text-transform: uppercase
img:not(.custom)
max-width: 13px
max-height: 13px
&.tiny
min-width: 0
/****
* Select
***/
.select
display: flex
align-items: center
justify-content: flex-start
color: white
.MuiInput-underline
&::before,
&::after
display: none
content: ''
.MuiSelect-root
&.MuiSelect-select
border-radius: 20px
cursor: pointer
min-width: 2rem
font-weight: normal
border: solid 0
padding: 3px 16px 4px 12px
.MuiInputBase-input
border-radius: 20px
background-color: rgba(255, 255, 255, 0.06)
cursor: pointer
padding-top: 0
padding-bottom: 0
font-size: 12px
font-weight: normal
font-stretch: normal
font-style: normal
letter-spacing: normal
text-align: left
line-height: 1.25
min-height: initial
&:focus
background-color: rgba(255, 255, 255, 0.06) !important
.MuiSelect-icon
top: 50%
transform: translateY(-50%)
right: 5px
position: absolute
pointer-events: none
&.MuiSelect-iconOpen
transform: translateY(-50%) rotate(180deg)
.ellipsis
display: block
overflow: hidden
white-space: nowrap
width: 100px
text-overflow: ellipsis
color: white
.selectLabel
margin-right: 8px
&.labelOnTop
flex-direction: column
align-items: flex-start
.selectLabel
margin-right: 0
margin-bottom: 4px
/****
* Paper/List/Menu list
***/
.MuiPaper-root
background-color: #344073 !important
&.MuiPaper-rounded
border-radius: 4px
.MuiList-root
padding: 0
&.MuiMenu-list
border-radius: 4px
box-shadow: 0 10px 20px 0 rgba(0, 0, 0, 0.5)
color: #a0b2ff
.MuiListItem-root
&.MuiMenuItem-root
padding: 14px
font-size: 11px
font-weight: 600
font-stretch: normal
font-style: normal
line-height: 1.25
&:not(:last-child)
border-bottom: 1px solid rgb(53, 65, 114)
&.Mui-selected
background-color: #3f519a6e
color: white
// scroll-bar css
::-webkit-scrollbar