ServiceMapModal filters (#981)

* fixed toast
fixed filter refresh on reload

* revarted

* sticky selectlist header

* apply check to filtered items

* grpc filter Bug

* should almost fix filtering

* working without disabled

* handle disabled items

* small refactor

* servicesFilterList height

* after PR notes

* remove redunded var

* pr review

Co-authored-by: Leon <>
This commit is contained in:
leon-up9
2022-04-11 17:26:28 +03:00
committed by GitHub
parent 45611c4c13
commit 22e3b3d8b2
5 changed files with 54 additions and 42 deletions

View File

@@ -8,6 +8,7 @@ import openApiLogo from 'assets/openApiLogo.png'
import { redocThemeOptions } from "./redocThemeOptions"; import { redocThemeOptions } from "./redocThemeOptions";
import React from "react"; import React from "react";
import { Select } from "../UI/Select"; import { Select } from "../UI/Select";
import { TOAST_CONTAINER_ID } from "../../configs/Consts";
const modalStyle = { const modalStyle = {
@@ -43,7 +44,7 @@ const OasModal = ({ openModal, handleCloseModal, getOasServices, getOasByService
const data = await getOasByService(selectedService ? selectedService : oasServices[0]); const data = await getOasByService(selectedService ? selectedService : oasServices[0]);
setSelectedServiceSpec(data); setSelectedServiceSpec(data);
} catch (e) { } catch (e) {
toast.error("Error occurred while fetching service OAS spec"); toast.error("Error occurred while fetching service OAS spec", { containerId: TOAST_CONTAINER_ID });
console.error(e); console.error(e);
} }
}; };

View File

@@ -53,7 +53,7 @@
& .servicesFilterList & .servicesFilterList
overflow-y: auto overflow-y: auto
height: 92% height: calc(100% - 30px - 5px)
.separtorLine .separtorLine
margin-top: 10px margin-top: 10px

View File

@@ -15,6 +15,7 @@ import { GraphData, ServiceMapGraph } from "./ServiceMapModalTypes"
import { ResizableBox } from "react-resizable" import { ResizableBox } from "react-resizable"
import "react-resizable/css/styles.css" import "react-resizable/css/styles.css"
import { Utils } from "../../helpers/Utils"; import { Utils } from "../../helpers/Utils";
import { TOAST_CONTAINER_ID } from "../../configs/Consts";
const modalStyle = { const modalStyle = {
position: 'absolute', position: 'absolute',
@@ -46,12 +47,12 @@ const LegentLabel: React.FC<LegentLabelProps> = ({ color, name }) => {
} }
const protocols = [ const protocols = [
{ key: "http", value: "HTTP", component: <LegentLabel color="#205cf5" name="HTTP" /> }, { key: "HTTP", value: "HTTP", component: <LegentLabel color="#205cf5" name="HTTP" /> },
{ key: "http/2", value: "HTTP/2", component: <LegentLabel color='#244c5a' name="HTTP/2" /> }, { key: "HTTP/2", value: "HTTP/2", component: <LegentLabel color='#244c5a' name="HTTP/2" /> },
{ key: "grpc", value: "gRPC", component: <LegentLabel color='#244c5a' name="gRPC" /> }, { key: "gRPC", value: "gRPC", component: <LegentLabel color='#244c5a' name="gRPC" /> },
{ key: "amqp", value: "AMQP", component: <LegentLabel color='#ff6600' name="AMQP" /> }, { key: "AMQP", value: "AMQP", component: <LegentLabel color='#ff6600' name="AMQP" /> },
{ key: "kafka", value: "KAFKA", component: <LegentLabel color='#000000' name="KAFKA" /> }, { key: "KAFKA", value: "KAFKA", component: <LegentLabel color='#000000' name="KAFKA" /> },
{ key: "redis", value: "REDIS", component: <LegentLabel color='#a41e11' name="REDIS" /> },] { key: "REDIS", value: "REDIS", component: <LegentLabel color='#a41e11' name="REDIS" /> },]
interface ServiceMapModalProps { interface ServiceMapModalProps {
@@ -65,8 +66,8 @@ export const ServiceMapModal: React.FC<ServiceMapModalProps> = ({ isOpen, onClos
const commonClasses = useCommonStyles(); const commonClasses = useCommonStyles();
const [isLoading, setIsLoading] = useState<boolean>(true); const [isLoading, setIsLoading] = useState<boolean>(true);
const [graphData, setGraphData] = useState<GraphData>({ nodes: [], edges: [] }); const [graphData, setGraphData] = useState<GraphData>({ nodes: [], edges: [] });
const [filteredProtocols, setFilteredProtocols] = useState(protocols.map(x => x.key)) const [checkedProtocols, setCheckedProtocols] = useState(protocols.map(x => x.key))
const [filteredServices, setFilteredServices] = useState([]) const [checkedServices, setCheckedServices] = useState([])
const [serviceMapApiData, setServiceMapApiData] = useState<ServiceMapGraph>({ edges: [], nodes: [] }) const [serviceMapApiData, setServiceMapApiData] = useState<ServiceMapGraph>({ edges: [], nodes: [] })
const [servicesSearchVal, setServicesSearchVal] = useState("") const [servicesSearchVal, setServicesSearchVal] = useState("")
const [graphOptions, setGraphOptions] = useState(ServiceMapOptions); const [graphOptions, setGraphOptions] = useState(ServiceMapOptions);
@@ -89,7 +90,7 @@ export const ServiceMapModal: React.FC<ServiceMapModalProps> = ({ isOpen, onClos
setGraphData(newGraphData) setGraphData(newGraphData)
} catch (ex) { } catch (ex) {
toast.error("An error occurred while loading Mizu Service Map, see console for mode details"); toast.error("An error occurred while loading Mizu Service Map, see console for mode details", { containerId: TOAST_CONTAINER_ID });
console.error(ex); console.error(ex);
} finally { } finally {
setIsLoading(false) setIsLoading(false)
@@ -131,21 +132,20 @@ export const ServiceMapModal: React.FC<ServiceMapModalProps> = ({ isOpen, onClos
}, [serviceMapApiData]) }, [serviceMapApiData])
const filterServiceMap = (newProtocolsFilters?: any[], newServiceFilters?: string[]) => { const filterServiceMap = (newProtocolsFilters?: any[], newServiceFilters?: string[]) => {
const filterProt = newProtocolsFilters || filteredProtocols const filterProt = newProtocolsFilters || checkedProtocols
const filterService = newServiceFilters || filteredServices || getServicesForFilter.map(x => x.key) const filterService = newServiceFilters || checkedServices
setFilteredProtocols(filterProt) setCheckedProtocols(filterProt)
setFilteredServices(filterService) setCheckedServices(filterService)
const newGraphData: GraphData = { const newGraphData: GraphData = {
nodes: serviceMapApiData.nodes?.map(mapNodesDatatoGraph).filter(node => filterService.includes(node.label)), nodes: serviceMapApiData.nodes?.map(mapNodesDatatoGraph).filter(node => filterService.includes(node.label)),
edges: serviceMapApiData.edges?.filter(edge => filterProt.includes(edge.protocol.name)).map(mapEdgesDatatoGraph) edges: serviceMapApiData.edges?.filter(edge => filterProt.includes(edge.protocol.abbr)).map(mapEdgesDatatoGraph)
} }
setGraphData(newGraphData); setGraphData(newGraphData);
} }
useEffect(() => { useEffect(() => {
const resolvedServices = getServicesForFilter.map(x => x.key).filter(serviceName => !Utils.isIpAddress(serviceName)) if (checkedServices.length > 0) return // only after refresh
setFilteredServices(resolvedServices) filterServiceMap(checkedProtocols, getServicesForFilter.map(x => x.key).filter(serviceName => !Utils.isIpAddress(serviceName)))
filterServiceMap(filteredProtocols, resolvedServices)
}, [getServicesForFilter]) }, [getServicesForFilter])
useEffect(() => { useEffect(() => {
@@ -182,14 +182,14 @@ export const ServiceMapModal: React.FC<ServiceMapModalProps> = ({ isOpen, onClos
<div className={styles.filterWrapper}> <div className={styles.filterWrapper}>
<div className={styles.protocolsFilterList}> <div className={styles.protocolsFilterList}>
<SelectList items={protocols} checkBoxWidth="5%" tableName={"Protocols"} multiSelect={true} <SelectList items={protocols} checkBoxWidth="5%" tableName={"Protocols"} multiSelect={true}
checkedValues={filteredProtocols} setCheckedValues={filterServiceMap} tableClassName={styles.filters} /> checkedValues={checkedProtocols} setCheckedValues={filterServiceMap} tableClassName={styles.filters} />
</div> </div>
<div className={styles.separtorLine}></div> <div className={styles.separtorLine}></div>
<div className={styles.servicesFilter}> <div className={styles.servicesFilter}>
<input className={commonClasses.textField + ` ${styles.servicesFilterSearch}`} placeholder="search service" value={servicesSearchVal} onChange={(event) => setServicesSearchVal(event.target.value)} /> <input className={commonClasses.textField + ` ${styles.servicesFilterSearch}`} placeholder="search service" value={servicesSearchVal} onChange={(event) => setServicesSearchVal(event.target.value)} />
<div className={styles.servicesFilterList}> <div className={styles.servicesFilterList}>
<SelectList items={getServicesForFilter} tableName={"Services"} tableClassName={styles.filters} multiSelect={true} searchValue={servicesSearchVal} <SelectList items={getServicesForFilter} tableName={"Services"} tableClassName={styles.filters} multiSelect={true} searchValue={servicesSearchVal}
checkBoxWidth="5%" checkedValues={filteredServices} setCheckedValues={(newServicesForFilter) => filterServiceMap(null, newServicesForFilter)} /> checkBoxWidth="5%" checkedValues={checkedServices} setCheckedValues={(newServicesForFilter) => filterServiceMap(null, newServicesForFilter)} />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,4 @@
import React, { useMemo } from "react"; import React, { useCallback, useEffect, useMemo, useState } from "react";
import Radio from "./Radio"; import Radio from "./Radio";
import styles from './style/SelectList.module.sass' import styles from './style/SelectList.module.sass'
import NoDataMessage from "./NoDataMessage"; import NoDataMessage from "./NoDataMessage";
@@ -19,12 +19,16 @@ export interface Props {
const SelectList: React.FC<Props> = ({ items, tableName, checkedValues = [], multiSelect = true, searchValue = "", setCheckedValues, tableClassName, const SelectList: React.FC<Props> = ({ items, tableName, checkedValues = [], multiSelect = true, searchValue = "", setCheckedValues, tableClassName,
checkBoxWidth = 50 }) => { checkBoxWidth = 50 }) => {
const noItemsMessage = "No items to show"; const noItemsMessage = "No items to show";
const enabledItemsLength = useMemo(() => items.filter(item => !item.disabled).length, [items]); const [headerChecked, setHeaderChecked] = useState(false)
const filteredValues = useMemo(() => { const filteredValues = useMemo(() => {
return items.filter((listValue) => listValue?.value?.includes(searchValue)); return items.filter((listValue) => listValue?.value?.includes(searchValue));
}, [items, searchValue]) }, [items, searchValue])
const filteredValuesKeys = useMemo(() => {
return filteredValues.map(x => x.key)
}, [filteredValues])
const toggleValue = (checkedKey) => { const toggleValue = (checkedKey) => {
if (!multiSelect) { if (!multiSelect) {
const newCheckedValues = []; const newCheckedValues = [];
@@ -34,25 +38,31 @@ const SelectList: React.FC<Props> = ({ items, tableName, checkedValues = [], mul
else { else {
const newCheckedValues = [...checkedValues]; const newCheckedValues = [...checkedValues];
let index = newCheckedValues.indexOf(checkedKey); let index = newCheckedValues.indexOf(checkedKey);
if (index > -1) if (index > -1)
newCheckedValues.splice(index, 1); newCheckedValues.splice(index, 1);
else else
newCheckedValues.push(checkedKey); newCheckedValues.push(checkedKey);
setCheckedValues(newCheckedValues); setCheckedValues(newCheckedValues);
} }
} }
const toggleAll = () => { useEffect(() => {
const newCheckedValues = [...checkedValues]; const setAllChecked = filteredValuesKeys.every(val => checkedValues.includes(val))
if (newCheckedValues.length === enabledItemsLength) setCheckedValues([]); setHeaderChecked(setAllChecked)
else { }, [filteredValuesKeys, checkedValues])
items.forEach((obj) => {
if (!obj.disabled && !newCheckedValues.includes(obj.key)) const toggleAll = useCallback((shouldCheckAll) => {
newCheckedValues.push(obj.key); let newChecked = checkedValues.filter(x => !filteredValuesKeys.includes(x))
})
setCheckedValues(newCheckedValues); if (shouldCheckAll) {
const disabledItems = items.filter(i => i.disabled).map(x => x.key)
newChecked = [...filteredValuesKeys, ...newChecked].filter(x => !disabledItems.includes(x))
} }
}
setCheckedValues(newChecked)
}, [searchValue, checkedValues, filteredValuesKeys])
const dataFieldFunc = (listValue) => listValue.component ? listValue.component : const dataFieldFunc = (listValue) => listValue.component ? listValue.component :
<span className={styles.nowrap} title={listValue.value}> <span className={styles.nowrap} title={listValue.value}>
@@ -60,8 +70,8 @@ const SelectList: React.FC<Props> = ({ items, tableName, checkedValues = [], mul
</span> </span>
const tableHead = multiSelect ? <tr style={{ borderBottomWidth: "2px" }}> const tableHead = multiSelect ? <tr style={{ borderBottomWidth: "2px" }}>
<th style={{ width: checkBoxWidth }}><Checkbox data-cy="checkbox-all" checked={enabledItemsLength === checkedValues.length} <th style={{ width: checkBoxWidth }}><Checkbox data-cy="checkbox-all" checked={headerChecked}
onToggle={toggleAll} /></th> onToggle={(isChecked) => toggleAll(isChecked)} /></th>
<th>{tableName}</th> <th>{tableName}</th>
</tr> : </tr> :
<tr style={{ borderBottomWidth: "2px" }}> <tr style={{ borderBottomWidth: "2px" }}>
@@ -70,7 +80,7 @@ const SelectList: React.FC<Props> = ({ items, tableName, checkedValues = [], mul
const tableBody = filteredValues.length === 0 ? const tableBody = filteredValues.length === 0 ?
<tr> <tr>
<td> <td colSpan={2}>
<NoDataMessage messageText={noItemsMessage} /> <NoDataMessage messageText={noItemsMessage} />
</td> </td>
</tr> </tr>

View File

@@ -1,25 +1,26 @@
@import '../../../variables.module' @import '../../../variables.module'
.selectListTable .selectListTable
overflow: auto
height: 100%
table table
width: 100% width: 100%
margin-top: 20px margin-top: 20px
height: 100% border-collapse: collapse
tbody
display: block
th th
color: $blue-gray color: $blue-gray
text-align: left text-align: left
padding: 10px padding: 10px
position: sticky
top: 0
background: $main-background-color
tr tr
border-bottom-width: 1px border-bottom-width: 1px
border-bottom-color: $data-background-color border-bottom-color: $data-background-color
border-bottom-style: solid border-bottom-style: solid
display: table
table-layout: fixed
width: 100% width: 100%
td td