mirror of
https://github.com/kubeshark/kubeshark.git
synced 2025-09-05 04:23:09 +00:00
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:
@@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -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
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user