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 {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 styles from './style/HarEntriesList.module.sass';
import spinner from './assets/spinner.svg'; import spinner from './assets/spinner.svg';
import ScrollableFeed from "react-scrollable-feed"; import ScrollableFeed from "react-scrollable-feed";
import {StatusType} from "./HarFilters";
interface HarEntriesListProps { interface HarEntriesListProps {
entries: any[]; entries: any[];
@ -14,6 +15,9 @@ interface HarEntriesListProps {
setNoMoreDataTop: (flag: boolean) => void; setNoMoreDataTop: (flag: boolean) => void;
noMoreDataBottom: boolean; noMoreDataBottom: boolean;
setNoMoreDataBottom: (flag: boolean) => void; setNoMoreDataBottom: (flag: boolean) => void;
methodsFilter: Array<string>;
statusFilter: Array<string>;
pathFilter: string
} }
enum FetchOperator { enum FetchOperator {
@ -21,91 +25,107 @@ enum FetchOperator {
GT = "gt" 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 [loadMoreTop, setLoadMoreTop] = useState(false);
const [isLoadingTop, setIsLoadingTop] = useState(false); const [isLoadingTop, setIsLoadingTop] = useState(false);
useEffect(() => {
if(loadMoreTop && !connectionOpen && !noMoreDataTop)
fetchData(FetchOperator.LT);
}, [loadMoreTop, connectionOpen, noMoreDataTop]);
useEffect(() => { useEffect(() => {
const list = document.getElementById('list').firstElementChild; const list = document.getElementById('list').firstElementChild;
list.addEventListener('scroll', (e) => { list.addEventListener('scroll', (e) => {
const el: any = e.target; const el: any = e.target;
if(el.scrollTop === 0) { if(el.scrollTop === 0) {
setLoadMoreTop(true); 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; const filteredEntries = useMemo(() => {
if(operator === FetchOperator.LT) return entries.filter(filterEntries);
setIsLoadingTop(true); },[entries, filterEntries])
fetch(`http://localhost:8899/api/entries?limit=50&operator=${operator}&timestamp=${timestamp}`) const fetchData = async (operator, timestamp) => {
.then(response => response.json()) const response = await fetch(`http://localhost:8899/api/entries?limit=50&operator=${operator}&timestamp=${timestamp}`);
.then((data: any[]) => { return await response.json();
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();
}
}
if(operator === FetchOperator.GT) { const getOldEntries = useCallback(async () => {
if(data.length === 0) { setIsLoadingTop(true);
setNoMoreDataBottom(true); const data = await fetchData(FetchOperator.LT, entries[0].timestamp);
} setLoadMoreTop(false);
scrollTo = document.getElementById(entries[entries.length -1].id);
let newEntries = [...entries, ...data]; let scrollTo;
if(newEntries.length >= 1000) { if(data.length === 0) {
setNoMoreDataTop(false); setNoMoreDataTop(true);
newEntries = newEntries.slice(-1000); scrollTo = document.getElementById("noMoreDataTop");
} } else {
setEntries(newEntries); scrollTo = document.getElementById(filteredEntries?.[0]?.id);
if(scrollTo) { }
scrollTo.scrollIntoView({behavior: "smooth"}); 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 <> return <>
<div className={styles.list}> <div className={styles.list}>
<div id="list" 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> <ScrollableFeed>
{noMoreDataTop && !connectionOpen && <div id="noMoreDataTop" style={{textAlign: "center", fontWeight: 600, color: "rgba(255,255,255,0.75)"}}>No more data available</div>} {noMoreDataTop && !connectionOpen && <div id="noMoreDataTop" className={styles.noMoreDataAvailable}>No more data available</div>}
{entries?.map(entry => <HarEntry key={entry.id} {filteredEntries.map(entry => <HarEntry key={entry.id}
entry={entry} entry={entry}
setFocusedEntryId={setFocusedEntryId} setFocusedEntryId={setFocusedEntryId}
isSelected={focusedEntryId === entry.id}/>)} 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)"}}> {!connectionOpen && !noMoreDataBottom && <div className={styles.fetchButtonContainer}>
<div className={styles.styledButton} onClick={() => fetchData(FetchOperator.GT)}>Fetch more entries</div> <div className={styles.styledButton} onClick={() => getNewEntries()}>Fetch more entries</div>
</div>} </div>}
</ScrollableFeed> </ScrollableFeed>
</div> </div>
{entries?.length > 0 && <div className={styles.footer}> {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>Started listening at <span style={{marginRight: 5, fontWeight: 600, fontSize: 13}}>{new Date(+entries[0].timestamp)?.toLocaleString()}</span></div>
</div>} </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 styles from './style/HarFilters.module.sass';
import {HARFilterSelect} from "./HARFilterSelect"; import {HARFilterSelect} from "./HARFilterSelect";
import {TextField} from "@material-ui/core"; 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}> return <div className={styles.container}>
<ServiceFilter/> <MethodFilter methodsFilter={methodsFilter} setMethodsFilter={setMethodsFilter}/>
<MethodFilter/> <StatusTypesFilter statusFilter={statusFilter} setStatusFilter={setStatusFilter}/>
<StatusTypesFilter/> <PathFilter pathFilter={pathFilter} setPathFilter={setPathFilter}/>
<SourcesFilter/>
<FetchModeFilter/>
<PathFilter/>
</div>; </div>;
}; };
@ -23,77 +30,6 @@ const FilterContainer: React.FC = ({children}) => {
</div>; </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 { enum HTTPMethod {
GET = "get", GET = "get",
PUT = "put", PUT = "put",
@ -103,58 +39,80 @@ enum HTTPMethod {
PATCH = "patch" 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> return <FilterContainer>
<HARFilterSelect <HARFilterSelect
items={Object.values(HTTPMethod)} items={Object.values(HTTPMethod)}
allowMultiple={true} allowMultiple={true}
value={selectedMethods} value={methodsFilter}
onChange={(val) => { onChange={(val) => methodClicked(val)}
// harStore.updateFilter({toggleMethod: val}) todo
}}
transformDisplay={_toUpperCase} transformDisplay={_toUpperCase}
label={"Methods"} label={"Methods"}
/> />
</FilterContainer>; </FilterContainer>;
}; };
enum StatusType { export enum StatusType {
SUCCESS = "success", SUCCESS = "success",
ERROR = "error" 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> return <FilterContainer>
<HARFilterSelect <HARFilterSelect
items={Object.values(StatusType)} items={Object.values(StatusType)}
allowMultiple={true} allowMultiple={true}
value={selectedStatusTypes} value={statusFilter}
onChange={(val) => { onChange={(val) => statusClicked(val)}
// harStore.updateFilter({toggleStatusType: val}) todo
}}
transformDisplay={_toUpperCase} transformDisplay={_toUpperCase}
label="Status" label="Status"
/> />
</FilterContainer>; </FilterContainer>;
}; };
// TODO path search is inclusive of the qs -> we want to avoid this - TRA-1681 interface PathFilterProps {
const PathFilter: React.FC = () => { pathFilter: string;
setPathFilter: (val: string) => void;
}
const onFilterChange = (value) => { const PathFilter: React.FC<PathFilterProps> = ({pathFilter, setPathFilter}) => {
// harStore.updateFilter({setPathSearch: value}); todo
}
return <FilterContainer> return <FilterContainer>
<div className={styles.filterLabel}>Path</div> <div className={styles.filterLabel}>Path</div>
<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> </div>
</FilterContainer>; </FilterContainer>;
}; };

View File

@ -1,5 +1,5 @@
import React, {useEffect, useRef, useState} from "react"; import React, {useEffect, useRef, useState} from "react";
// import {HarFilters} from "./HarFilters"; import {HarFilters} from "./HarFilters";
import {HarEntriesList} from "./HarEntriesList"; import {HarEntriesList} from "./HarEntriesList";
import {makeStyles} from "@material-ui/core"; import {makeStyles} from "@material-ui/core";
import "./style/HarPage.sass"; import "./style/HarPage.sass";
@ -43,6 +43,10 @@ export const HarPage: React.FC = () => {
const [noMoreDataTop, setNoMoreDataTop] = useState(false); const [noMoreDataTop, setNoMoreDataTop] = useState(false);
const [noMoreDataBottom, setNoMoreDataBottom] = useState(false); const [noMoreDataBottom, setNoMoreDataBottom] = useState(false);
const [methodsFilter, setMethodsFilter] = useState([]);
const [statusFilter, setStatusFilter] = useState([]);
const [pathFilter, setPathFilter] = useState("");
const ws = useRef(null); const ws = useRef(null);
const openWebSocket = () => { const openWebSocket = () => {
@ -53,7 +57,6 @@ export const HarPage: React.FC = () => {
if(ws.current) { if(ws.current) {
ws.current.onmessage = e => { ws.current.onmessage = e => {
console.log(connection);
if(!e?.data) return; if(!e?.data) return;
const entry = JSON.parse(e.data); const entry = JSON.parse(e.data);
if(connection === ConnectionStatus.Paused) { 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 ( return (
<div className="HarPage"> <div className="HarPage">
<div style={{padding: "0 24px 24px 24px", display: "flex", alignItems: "center"}}> <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}/> <img style={{cursor: "pointer", marginRight: 15, height: 20}} alt="pause" src={connection === ConnectionStatus.Connected ? pauseIcon : playIcon} onClick={toggleConnection}/>
<div className="connectionText"> <div className="connectionText">
{connection === ConnectionStatus.Connected ? "connected, waiting for traffic" : "not connected"} {getConnectionTitle()}
<div className={getConnectionStatusClass()}/> <div className={getConnectionStatusClass()}/>
</div> </div>
</div> </div>
{entries.length > 0 && <div className="HarPage-Container"> {entries.length > 0 && <div className="HarPage-Container">
<div className="HarPage-ListContainer"> <div className="HarPage-ListContainer">
{/*<HarFilters />*/} <HarFilters methodsFilter={methodsFilter}
setMethodsFilter={setMethodsFilter}
statusFilter={statusFilter}
setStatusFilter={setStatusFilter}
pathFilter={pathFilter}
setPathFilter={setPathFilter}
/>
<div className={styles.container}> <div className={styles.container}>
<HarEntriesList entries={entries} <HarEntriesList entries={entries}
setEntries={setEntries} setEntries={setEntries}
@ -119,7 +139,11 @@ export const HarPage: React.FC = () => {
noMoreDataBottom={noMoreDataBottom} noMoreDataBottom={noMoreDataBottom}
setNoMoreDataBottom={setNoMoreDataBottom} setNoMoreDataBottom={setNoMoreDataBottom}
noMoreDataTop={noMoreDataTop} noMoreDataTop={noMoreDataTop}
setNoMoreDataTop={setNoMoreDataTop}/> setNoMoreDataTop={setNoMoreDataTop}
methodsFilter={methodsFilter}
statusFilter={statusFilter}
pathFilter={pathFilter}
/>
</div> </div>
</div> </div>
<div className={classes.details}> <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 {MenuItem, Select as MUISelect} from '@material-ui/core';
import React from 'react'; import React from 'react';
import {SelectProps as MUISelectProps} from '@material-ui/core/Select/Select'; import {SelectProps as MUISelectProps} from '@material-ui/core/Select/Select';
import styles from './style/Select.module.sass'; import styles from './style/Select.module.sass';
const ALL_KEY= 'All'; export const ALL_KEY= 'All';
const menuProps: any = { const menuProps: any = {
anchorOrigin: { 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 .styledButton:hover
border: 1px solid #627ef7 border: 1px solid #627ef7
background-color: rgba(255, 255, 255, 0.06) 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%) border: solid 1px lighten(#4253a5, 20%)
.rowSelected .rowSelected
border: solid 1px #4253a5 background: #293053
border: 1px #ffffff61 solid
.service .service
text-overflow: ellipsis text-overflow: ellipsis

View File

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

View File

@ -16,6 +16,124 @@ body
code code
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace 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 // scroll-bar css
::-webkit-scrollbar ::-webkit-scrollbar