From c7a20ed9c0eb46a52c8422e63b5525ec3e323190 Mon Sep 17 00:00:00 2001 From: lirazyehezkel <61656597+lirazyehezkel@users.noreply.github.com> Date: Thu, 6 May 2021 17:40:35 +0300 Subject: [PATCH] Feature/UI/filters (#32) * UI filters * refactor * Revert "refactor" This reverts commit 70e7d4b6acf2e214f95c7b497a6043ee3b3f2086. * remove recursive func --- ui/src/components/HarEntriesList.tsx | 132 ++++++++------- 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, 309 insertions(+), 163 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..f446d135d 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, {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; + statusFilter: Array; + pathFilter: string } enum FetchOperator { @@ -21,91 +25,107 @@ 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); - 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}×tamp=${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}×tamp=${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 <>
- {isLoadingTop &&
spinner
} + {isLoadingTop &&
+ spinner +
} - {noMoreDataTop && !connectionOpen &&
No more data available
} - {entries?.map(entry => No more data available
} + {filteredEntries.map(entry => )} - {!connectionOpen && !noMoreDataBottom &&
-
fetchData(FetchOperator.GT)}>Fetch more entries
+ {!connectionOpen && !noMoreDataBottom &&
+
getNewEntries()}>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