From 5ad26bd51e792b6b44d5c56bdc9d20a71ffc21f6 Mon Sep 17 00:00:00 2001 From: Liraz Yehezkel Date: Thu, 6 May 2021 14:14:47 +0300 Subject: [PATCH] UI filters --- ui/src/components/HarEntriesList.tsx | 51 ++++-- ui/src/components/HarFilters.tsx | 154 +++++++----------- ui/src/components/HarPage.tsx | 34 +++- ui/src/components/Select.tsx | 4 +- .../components/assets/default_icon_down.svg | 3 + .../style/HARFilterSelect.module.sass | 3 + .../style/HarEntriesList.module.sass | 20 ++- ui/src/components/style/HarEntry.module.sass | 3 +- .../components/style/HarFilters.module.sass | 1 + ui/src/index.sass | 118 ++++++++++++++ 10 files changed, 273 insertions(+), 118 deletions(-) create mode 100644 ui/src/components/assets/default_icon_down.svg create mode 100644 ui/src/components/style/HARFilterSelect.module.sass diff --git a/ui/src/components/HarEntriesList.tsx b/ui/src/components/HarEntriesList.tsx index efc6ba52a..6ca53cb45 100644 --- a/ui/src/components/HarEntriesList.tsx +++ b/ui/src/components/HarEntriesList.tsx @@ -1,8 +1,9 @@ import {HarEntry} from "./HarEntry"; -import React, {useEffect, useState} from "react"; +import React, {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; + statusFilter: Array; + pathFilter: string } enum FetchOperator { @@ -21,7 +25,7 @@ enum FetchOperator { GT = "gt" } -export const HarEntriesList: React.FC = ({entries, setEntries, focusedEntryId, setFocusedEntryId, connectionOpen, noMoreDataTop, setNoMoreDataTop, noMoreDataBottom, setNoMoreDataBottom}) => { +export const HarEntriesList: React.FC = ({entries, setEntries, focusedEntryId, setFocusedEntryId, connectionOpen, noMoreDataTop, setNoMoreDataTop, noMoreDataBottom, setNoMoreDataBottom, methodsFilter, statusFilter, pathFilter}) => { const [loadMoreTop, setLoadMoreTop] = useState(false); const [isLoadingTop, setIsLoadingTop] = useState(false); @@ -37,13 +41,23 @@ export const HarEntriesList: React.FC = ({entries, setEntri const el: any = e.target; if(el.scrollTop === 0) { setLoadMoreTop(true); + } else { + setLoadMoreTop(false); } }); }, []); - const fetchData = async (operator) => { + const filterEntries = (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; + } - const timestamp = operator === FetchOperator.LT ? entries[0].timestamp : entries[entries.length - 1].timestamp; + const fetchData = async (operator, firstEntryTimestamp?, lastEntryTimestamp?) => { + + const timestamp = operator === FetchOperator.LT ? (firstEntryTimestamp ?? entries[0].timestamp) : (lastEntryTimestamp ?? entries[entries.length - 1].timestamp); if(operator === FetchOperator.LT) setIsLoadingTop(true); @@ -56,7 +70,11 @@ export const HarEntriesList: React.FC = ({entries, setEntri setNoMoreDataTop(true); scrollTo = document.getElementById("noMoreDataTop"); } else { - scrollTo = document.getElementById(entries[0].id); + if(data.filter(filterEntries).length === 0) { + fetchData(operator, data[0].timestamp); + return; + } + scrollTo = document.getElementById(filteredEntries?.[0]?.id); } const newEntries = [...data, ...entries]; if(newEntries.length >= 1000) { @@ -73,8 +91,13 @@ export const HarEntriesList: React.FC = ({entries, setEntri if(operator === FetchOperator.GT) { if(data.length === 0) { setNoMoreDataBottom(true); + } else { + if(data.filter(filterEntries).length === 0) { + fetchData(operator, null, data[data.length-1].timestamp); + return; + } } - scrollTo = document.getElementById(entries[entries.length -1].id); + scrollTo = document.getElementById(filteredEntries?.[filteredEntries.length -1].id); let newEntries = [...entries, ...data]; if(newEntries.length >= 1000) { setNoMoreDataTop(false); @@ -88,24 +111,30 @@ export const HarEntriesList: React.FC = ({entries, setEntri }); }; + const filteredEntries = useMemo(() => { + return entries.filter(filterEntries); + },[entries, methodsFilter, pathFilter, statusFilter]) + return <>
- {isLoadingTop &&
spinner
} + {isLoadingTop &&
+ spinner +
} - {noMoreDataTop && !connectionOpen &&
No more data available
} - {entries?.map(entry => No more data available
} + {filteredEntries.map(entry => )} - {!connectionOpen && !noMoreDataBottom &&
+ {!connectionOpen && !noMoreDataBottom &&
fetchData(FetchOperator.GT)}>Fetch more entries
}
{entries?.length > 0 &&
-
{entries?.length} requests
+
{filteredEntries?.length !== entries.length && `${filteredEntries?.length} / `} {entries?.length} requests
Started listening at {new Date(+entries[0].timestamp)?.toLocaleString()}
}
diff --git a/ui/src/components/HarFilters.tsx b/ui/src/components/HarFilters.tsx index e5fb55a8f..5dee2b564 100644 --- a/ui/src/components/HarFilters.tsx +++ b/ui/src/components/HarFilters.tsx @@ -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; + setMethodsFilter: (methods: Array) => void; + statusFilter: Array; + setStatusFilter: (methods: Array) => void; + pathFilter: string + setPathFilter: (val: string) => void; +} + +export const HarFilters: React.FC = ({methodsFilter, setMethodsFilter, statusFilter, setStatusFilter, pathFilter, setPathFilter}) => { return
- - - - - - + + +
; }; @@ -23,77 +30,6 @@ const FilterContainer: React.FC = ({children}) => { ; }; -const ServiceFilter: React.FC = () => { - const providerIds = []; //todo - const selectedServices = []; //todo - - return - { - //todo: harStore.updateFilter({toggleService: val}) - }} - allowMultiple={true} - label={"Services"} - transformDisplay={_toUpperCase} - /> - - -}; - -const BROWSER_SOURCE = "_BROWSER_"; - -const SourcesFilter: React.FC = () => { - - const sources = []; //todo - const selectedSource = null; //todo - - useEffect(() => { - //todo: fetch sources - }, []); - - return - { - //todo: harStore.updateFilter({toggleSource: val}); - }} - allowMultiple={true} - label={"Sources"} - transformDisplay={item => item === BROWSER_SOURCE ? "BROWSER" : item.toUpperCase()} - /> - - -}; - -enum HARFetchMode { - UP_TO_REVISION = "Up to revision", - ALL = "All", - QUEUED = "Unprocessed" -} - -const FetchModeFilter: React.FC = () => { - - const selectedHarFetchMode = null; - - return - { - // selectedModelStore.har.setHarFetchMode(val); - // selectedModelStore.har.data.reset(); - // selectedModelStore.har.data.fetch(); - //todo - }} - label={"Processed"} - /> - - -}; - enum HTTPMethod { GET = "get", PUT = "put", @@ -103,58 +39,80 @@ enum HTTPMethod { PATCH = "patch" } -const MethodFilter: React.FC = () => { +interface MethodFilterProps { + methodsFilter: Array; + setMethodsFilter: (methods: Array) => void; +} - const selectedMethods = []; +const MethodFilter: React.FC = ({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 { - // harStore.updateFilter({toggleMethod: val}) todo - }} + value={methodsFilter} + onChange={(val) => methodClicked(val)} transformDisplay={_toUpperCase} label={"Methods"} /> ; }; -enum StatusType { +export enum StatusType { SUCCESS = "success", ERROR = "error" } -const StatusTypesFilter: React.FC = () => { +interface StatusTypesFilterProps { + statusFilter: Array; + setStatusFilter: (methods: Array) => void; +} - const selectedStatusTypes = []; +const StatusTypesFilter: React.FC = ({statusFilter, setStatusFilter}) => { + + const statusClicked = (val) => { + if(val === ALL_KEY) { + setStatusFilter([]); + return; + } + setStatusFilter([val]); + } return { - // harStore.updateFilter({toggleStatusType: val}) todo - }} + value={statusFilter} + onChange={(val) => statusClicked(val)} transformDisplay={_toUpperCase} label="Status" /> ; }; -// 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 = ({pathFilter, setPathFilter}) => { return
Path
- e.key === "Enter" && onFilterChange(e.target.value)}/> + setPathFilter(e.target.value)}/>
; }; diff --git a/ui/src/components/HarPage.tsx b/ui/src/components/HarPage.tsx index 3e0d80ff9..d31dbb75a 100644 --- a/ui/src/components/HarPage.tsx +++ b/ui/src/components/HarPage.tsx @@ -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 (
pause
- {connection === ConnectionStatus.Connected ? "connected, waiting for traffic" : "not connected"} + {getConnectionTitle()}
{entries.length > 0 &&
- {/**/} +
{ noMoreDataBottom={noMoreDataBottom} setNoMoreDataBottom={setNoMoreDataBottom} noMoreDataTop={noMoreDataTop} - setNoMoreDataTop={setNoMoreDataTop}/> + setNoMoreDataTop={setNoMoreDataTop} + methodsFilter={methodsFilter} + statusFilter={statusFilter} + pathFilter={pathFilter} + />
diff --git a/ui/src/components/Select.tsx b/ui/src/components/Select.tsx index a8ca3f078..cd6829032 100644 --- a/ui/src/components/Select.tsx +++ b/ui/src/components/Select.tsx @@ -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: { diff --git a/ui/src/components/assets/default_icon_down.svg b/ui/src/components/assets/default_icon_down.svg new file mode 100644 index 000000000..52aac5735 --- /dev/null +++ b/ui/src/components/assets/default_icon_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/src/components/style/HARFilterSelect.module.sass b/ui/src/components/style/HARFilterSelect.module.sass new file mode 100644 index 000000000..9cbbe228a --- /dev/null +++ b/ui/src/components/style/HARFilterSelect.module.sass @@ -0,0 +1,3 @@ +.HARSelectLabel + color: #8f9bb2 + font-size: 11px \ No newline at end of file diff --git a/ui/src/components/style/HarEntriesList.module.sass b/ui/src/components/style/HarEntriesList.module.sass index 5b0d9830c..a26deccf7 100644 --- a/ui/src/components/style/HarEntriesList.module.sass +++ b/ui/src/components/style/HarEntriesList.module.sass @@ -35,4 +35,22 @@ .styledButton:hover border: 1px solid #627ef7 - background-color: rgba(255, 255, 255, 0.06) \ No newline at end of file + 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) \ No newline at end of file diff --git a/ui/src/components/style/HarEntry.module.sass b/ui/src/components/style/HarEntry.module.sass index 96c949ee1..33bc1c446 100644 --- a/ui/src/components/style/HarEntry.module.sass +++ b/ui/src/components/style/HarEntry.module.sass @@ -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 diff --git a/ui/src/components/style/HarFilters.module.sass b/ui/src/components/style/HarFilters.module.sass index 11e043fb8..53cfbcfab 100644 --- a/ui/src/components/style/HarFilters.module.sass +++ b/ui/src/components/style/HarFilters.module.sass @@ -27,5 +27,6 @@ background: #171922 border-radius: 12px font-size: 12px + color: white fieldset border: none diff --git a/ui/src/index.sass b/ui/src/index.sass index bef187ff9..dd1fb1858 100644 --- a/ui/src/index.sass +++ b/ui/src/index.sass @@ -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