mirror of
https://github.com/kubeshark/kubeshark.git
synced 2025-09-01 10:36:55 +00:00
Ui/Service-map-split-to-ui-common (#966)
* added serviceModal & selectList & noDataMessage removed leftovers from split * scroll fix * sort by name * search alightment * space removed * margin-bottom * utils class Co-authored-by: Leon <> Co-authored-by: Igor Gov <iggvrv@gmail.com>
This commit is contained in:
@@ -49,6 +49,7 @@
|
||||
"node-fetch": "^3.1.1",
|
||||
"numeral": "^2.0.6",
|
||||
"protobuf-decoder": "^0.1.0",
|
||||
"react-resizable": "^3.0.4",
|
||||
"react-graph-vis": "^1.0.7",
|
||||
"react-lowlight": "^3.0.0",
|
||||
"react-router-dom": "^6.2.1",
|
||||
@@ -90,4 +91,4 @@
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
@import "../../variables.module"
|
||||
|
||||
.modalContainer
|
||||
display: flex
|
||||
flex-wrap: nowrap
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
.graphSection
|
||||
flex: 85%
|
||||
|
||||
.filterSection
|
||||
flex: 15%
|
||||
border-right: 1px solid $blue-color
|
||||
margin-right: 2%
|
||||
height: 100%
|
||||
|
||||
.filters table
|
||||
margin-top: 0px
|
||||
|
||||
tr
|
||||
border-style: none
|
||||
|
||||
td
|
||||
color: #8f9bb2
|
||||
font-size: 11px
|
||||
font-weight: 600
|
||||
padding-top: 2px
|
||||
padding-bottom: 2px
|
||||
|
||||
.colorBlock
|
||||
display: inline-block
|
||||
height: 15px
|
||||
width: 50px
|
||||
|
||||
.filterWrapper
|
||||
height: 100%
|
||||
display: flex
|
||||
flex-direction: column
|
||||
margin-right: 10px
|
||||
|
||||
.servicesFilterSearch
|
||||
width: calc(100% - 10px)
|
||||
max-width: 300px
|
||||
box-shadow: 0px 1px 5px #979797
|
||||
margin-left: 10px
|
||||
margin-bottom: 5px
|
||||
|
||||
.servicesFilter
|
||||
margin-top: clamp(25px,15%,35px)
|
||||
height: 100%
|
||||
overflow: hidden
|
||||
|
||||
& .servicesFilterList
|
||||
overflow-y: auto
|
||||
height: 92%
|
||||
|
||||
.separtorLine
|
||||
margin-top: 10px
|
||||
border: 1px solid #E9EBF8
|
227
ui-common/src/components/ServiceMapModal/ServiceMapModal.tsx
Normal file
227
ui-common/src/components/ServiceMapModal/ServiceMapModal.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { Box, Fade, Modal, Backdrop, Button } from "@material-ui/core";
|
||||
import { toast } from "react-toastify";
|
||||
import spinnerStyle from '../UI/style/Spinner.module.sass';
|
||||
import spinnerImg from 'assets/spinner.svg';
|
||||
import Graph from "react-graph-vis";
|
||||
import debounce from 'lodash/debounce';
|
||||
import ServiceMapOptions from './ServiceMapOptions'
|
||||
import { useCommonStyles } from "../../helpers/commonStyle";
|
||||
import refreshIcon from "assets/refresh.svg";
|
||||
import closeIcon from "assets/close.svg"
|
||||
import styles from './ServiceMapModal.module.sass'
|
||||
import SelectList from "../UI/SelectList";
|
||||
import { GraphData, ServiceMapGraph } from "./ServiceMapModalTypes"
|
||||
import { ResizableBox } from "react-resizable"
|
||||
import "react-resizable/css/styles.css"
|
||||
import { Utils } from "../../helpers/Utils";
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute',
|
||||
top: '6%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, 0%)',
|
||||
width: '89vw',
|
||||
height: '82vh',
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: '5px',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
color: '#000',
|
||||
padding: "25px 15px"
|
||||
};
|
||||
|
||||
interface LegentLabelProps {
|
||||
color: string,
|
||||
name: string
|
||||
}
|
||||
|
||||
const LegentLabel: React.FC<LegentLabelProps> = ({ color, name }) => {
|
||||
return <React.Fragment>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<span title={name}>{name}</span>
|
||||
<span style={{ background: color }} className={styles.colorBlock}></span>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
}
|
||||
|
||||
const protocols = [
|
||||
{ key: "http", value: "HTTP", component: <LegentLabel color="#205cf5" name="HTTP" /> },
|
||||
{ key: "http/2", value: "HTTP/2", component: <LegentLabel color='#244c5a' name="HTTP/2" /> },
|
||||
{ key: "grpc", value: "gRPC", component: <LegentLabel color='#244c5a' name="gRPC" /> },
|
||||
{ key: "amqp", value: "AMQP", component: <LegentLabel color='#ff6600' name="AMQP" /> },
|
||||
{ key: "kafka", value: "KAFKA", component: <LegentLabel color='#000000' name="KAFKA" /> },
|
||||
{ key: "redis", value: "REDIS", component: <LegentLabel color='#a41e11' name="REDIS" /> },]
|
||||
|
||||
|
||||
interface ServiceMapModalProps {
|
||||
isOpen: boolean;
|
||||
onOpen: () => void;
|
||||
onClose: () => void;
|
||||
getServiceMapDataApi: () => Promise<any>
|
||||
}
|
||||
|
||||
export const ServiceMapModal: React.FC<ServiceMapModalProps> = ({ isOpen, onClose, getServiceMapDataApi }) => {
|
||||
const commonClasses = useCommonStyles();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [graphData, setGraphData] = useState<GraphData>({ nodes: [], edges: [] });
|
||||
const [filteredProtocols, setFilteredProtocols] = useState(protocols.map(x => x.key))
|
||||
const [filteredServices, setFilteredServices] = useState([])
|
||||
const [serviceMapApiData, setServiceMapApiData] = useState<ServiceMapGraph>({ edges: [], nodes: [] })
|
||||
const [servicesSearchVal, setServicesSearchVal] = useState("")
|
||||
const [graphOptions, setGraphOptions] = useState(ServiceMapOptions);
|
||||
|
||||
const getServiceMapData = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
|
||||
const serviceMapData: ServiceMapGraph = await getServiceMapDataApi()
|
||||
setServiceMapApiData(serviceMapData)
|
||||
const newGraphData: GraphData = { nodes: [], edges: [] }
|
||||
|
||||
if (serviceMapData.nodes) {
|
||||
newGraphData.nodes = serviceMapData.nodes.map(mapNodesDatatoGraph)
|
||||
}
|
||||
|
||||
if (serviceMapData.edges) {
|
||||
newGraphData.edges = serviceMapData.edges.map(mapEdgesDatatoGraph)
|
||||
}
|
||||
|
||||
setGraphData(newGraphData)
|
||||
} catch (ex) {
|
||||
toast.error("An error occurred while loading Mizu Service Map, see console for mode details");
|
||||
console.error(ex);
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [isOpen])
|
||||
|
||||
const mapNodesDatatoGraph = node => {
|
||||
return {
|
||||
id: node.id,
|
||||
value: node.count,
|
||||
label: (node.entry.name === "unresolved") ? node.name : `${node.entry.name} (${node.name})`,
|
||||
title: "Count: " + node.name,
|
||||
isResolved: node.entry.resolved
|
||||
}
|
||||
}
|
||||
|
||||
const mapEdgesDatatoGraph = edge => {
|
||||
return {
|
||||
from: edge.source.id,
|
||||
to: edge.destination.id,
|
||||
value: edge.count,
|
||||
label: edge.count.toString(),
|
||||
color: {
|
||||
color: edge.protocol.backgroundColor,
|
||||
highlight: edge.protocol.backgroundColor
|
||||
},
|
||||
}
|
||||
}
|
||||
const mapToKeyValForFilter = (arr) => arr.map(mapNodesDatatoGraph)
|
||||
.map((edge) => { return { key: edge.label, value: edge.label } })
|
||||
.sort((a, b) => { return a.key.localeCompare(b.key) });
|
||||
|
||||
const getServicesForFilter = useMemo(() => {
|
||||
|
||||
const resolved = mapToKeyValForFilter(serviceMapApiData.nodes?.filter(x => x.resolved))
|
||||
const unResolved = mapToKeyValForFilter(serviceMapApiData.nodes?.filter(x => !x.resolved))
|
||||
return [...resolved, ...unResolved]
|
||||
}, [serviceMapApiData])
|
||||
|
||||
const filterServiceMap = (newProtocolsFilters?: any[], newServiceFilters?: string[]) => {
|
||||
const filterProt = newProtocolsFilters || filteredProtocols
|
||||
const filterService = newServiceFilters || filteredServices || getServicesForFilter.map(x => x.key)
|
||||
setFilteredProtocols(filterProt)
|
||||
setFilteredServices(filterService)
|
||||
const newGraphData: GraphData = {
|
||||
nodes: serviceMapApiData.nodes?.map(mapNodesDatatoGraph).filter(node => filterService.includes(node.label)),
|
||||
edges: serviceMapApiData.edges?.filter(edge => filterProt.includes(edge.protocol.name)).map(mapEdgesDatatoGraph)
|
||||
}
|
||||
setGraphData(newGraphData);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const resolvedServices = getServicesForFilter.map(x => x.key).filter(serviceName => !Utils.isIpAddress(serviceName))
|
||||
setFilteredServices(resolvedServices)
|
||||
filterServiceMap(filteredProtocols, resolvedServices)
|
||||
}, [getServicesForFilter])
|
||||
|
||||
useEffect(() => {
|
||||
getServiceMapData()
|
||||
}, [getServiceMapData])
|
||||
|
||||
useEffect(() => {
|
||||
if (graphData?.nodes?.length === 0) return;
|
||||
let options = { ...graphOptions };
|
||||
options.physics.barnesHut.avoidOverlap = graphData?.nodes?.length > 10 ? 0 : 1;
|
||||
setGraphOptions(options);
|
||||
// eslint-disable-next-line
|
||||
}, [graphData?.nodes?.length])
|
||||
|
||||
const refreshServiceMap = debounce(() => {
|
||||
getServiceMapData();
|
||||
}, 500);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
aria-labelledby="transition-modal-title"
|
||||
aria-describedby="transition-modal-description"
|
||||
open={isOpen}
|
||||
onClose={onClose}
|
||||
closeAfterTransition
|
||||
BackdropComponent={Backdrop}
|
||||
BackdropProps={{ timeout: 500 }}>
|
||||
<Fade in={isOpen}>
|
||||
<Box sx={modalStyle}>
|
||||
<div className={styles.modalContainer}>
|
||||
{/* TODO: remove error missing height */}
|
||||
<ResizableBox width={200} style={{ height: '100%', minWidth: "200px" }} axis={"x"}>
|
||||
<div className={styles.filterSection}>
|
||||
<div className={styles.filterWrapper}>
|
||||
<div className={styles.protocolsFilterList}>
|
||||
<SelectList items={protocols} checkBoxWidth="5%" tableName={"Protocols"} multiSelect={true}
|
||||
checkedValues={filteredProtocols} setCheckedValues={filterServiceMap} tableClassName={styles.filters} />
|
||||
</div>
|
||||
<div className={styles.separtorLine}></div>
|
||||
<div className={styles.servicesFilter}>
|
||||
<input className={commonClasses.textField + ` ${styles.servicesFilterSearch}`} placeholder="search service" value={servicesSearchVal} onChange={(event) => setServicesSearchVal(event.target.value)} />
|
||||
<div className={styles.servicesFilterList}>
|
||||
<SelectList items={getServicesForFilter} tableName={"Services"} tableClassName={styles.filters} multiSelect={true} searchValue={servicesSearchVal}
|
||||
checkBoxWidth="5%" checkedValues={filteredServices} setCheckedValues={(newServicesForFilter) => filterServiceMap(null, newServicesForFilter)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ResizableBox>
|
||||
<div className={styles.graphSection}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<Button style={{ marginRight: "3%" }}
|
||||
startIcon={<img src={refreshIcon} className="custom" alt="refresh" style={{ marginRight: "8%" }}></img>}
|
||||
size="medium"
|
||||
variant="contained"
|
||||
className={commonClasses.outlinedButton + " " + commonClasses.imagedButton}
|
||||
onClick={refreshServiceMap}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
<img src={closeIcon} alt="close" onClick={() => onClose()} style={{ cursor: "pointer" }}></img>
|
||||
</div>
|
||||
{isLoading && <div className={spinnerStyle.spinnerContainer}>
|
||||
<img alt="spinner" src={spinnerImg} style={{ height: 50 }} />
|
||||
</div>}
|
||||
{!isLoading && <div style={{ height: "100%", width: "100%" }}>
|
||||
<Graph
|
||||
graph={graphData}
|
||||
options={graphOptions}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</Fade>
|
||||
</Modal>
|
||||
);
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
export interface GraphData {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
}
|
||||
|
||||
export interface Node {
|
||||
id: number;
|
||||
value: number;
|
||||
label: string;
|
||||
title?: string;
|
||||
color?: object;
|
||||
}
|
||||
|
||||
export interface Edge {
|
||||
from: number;
|
||||
to: number;
|
||||
value: number;
|
||||
label: string;
|
||||
title?: string;
|
||||
color?: object;
|
||||
}
|
||||
|
||||
export interface ServiceMapNode {
|
||||
id: number;
|
||||
name: string;
|
||||
entry: Entry;
|
||||
count: number;
|
||||
resolved: boolean;
|
||||
}
|
||||
|
||||
export interface ServiceMapEdge {
|
||||
source: ServiceMapNode;
|
||||
destination: ServiceMapNode;
|
||||
count: number;
|
||||
protocol: Protocol;
|
||||
}
|
||||
|
||||
export interface ServiceMapGraph {
|
||||
nodes: ServiceMapNode[];
|
||||
edges: ServiceMapEdge[];
|
||||
}
|
||||
|
||||
export interface Entry {
|
||||
ip: string;
|
||||
port: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Protocol {
|
||||
name: string;
|
||||
abbr: string;
|
||||
macro: string;
|
||||
version: string;
|
||||
backgroundColor: string;
|
||||
foregroundColor: string;
|
||||
fontSize: number;
|
||||
referenceLink: string;
|
||||
ports: string[];
|
||||
priority: number;
|
||||
}
|
@@ -0,0 +1,83 @@
|
||||
const ServiceMapOptions = {
|
||||
physics: {
|
||||
enabled: true,
|
||||
solver: 'barnesHut',
|
||||
barnesHut: {
|
||||
theta: 0.5,
|
||||
gravitationalConstant: -2000,
|
||||
centralGravity: 0.3,
|
||||
springLength: 180,
|
||||
springConstant: 0.04,
|
||||
damping: 0.09,
|
||||
avoidOverlap: 0
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
hierarchical: false,
|
||||
randomSeed: 1 // always on node 1
|
||||
},
|
||||
nodes: {
|
||||
shape: 'dot',
|
||||
chosen: true,
|
||||
color: {
|
||||
background: '#27AE60',
|
||||
border: '#000000',
|
||||
highlight: {
|
||||
background: '#27AE60',
|
||||
border: '#000000',
|
||||
},
|
||||
},
|
||||
font: {
|
||||
color: '#343434',
|
||||
size: 14, // px
|
||||
face: 'arial',
|
||||
background: 'none',
|
||||
strokeWidth: 0, // px
|
||||
strokeColor: '#ffffff',
|
||||
align: 'center',
|
||||
multi: false
|
||||
},
|
||||
borderWidth: 1.5,
|
||||
borderWidthSelected: 2.5,
|
||||
labelHighlightBold: true,
|
||||
opacity: 1,
|
||||
shadow: true,
|
||||
},
|
||||
edges: {
|
||||
chosen: true,
|
||||
dashes: false,
|
||||
arrowStrikethrough: false,
|
||||
arrows: {
|
||||
to: {
|
||||
enabled: true,
|
||||
},
|
||||
middle: {
|
||||
enabled: false,
|
||||
},
|
||||
from: {
|
||||
enabled: false,
|
||||
}
|
||||
},
|
||||
smooth: {
|
||||
enabled: true,
|
||||
type: 'dynamic',
|
||||
roundness: 1.0
|
||||
},
|
||||
font: {
|
||||
color: '#343434',
|
||||
size: 12, // px
|
||||
face: 'arial',
|
||||
background: 'none',
|
||||
strokeWidth: 2, // px
|
||||
strokeColor: '#ffffff',
|
||||
align: 'horizontal',
|
||||
multi: false,
|
||||
},
|
||||
labelHighlightBold: true,
|
||||
selectionWidth: 1,
|
||||
shadow: true,
|
||||
},
|
||||
autoResize: true,
|
||||
};
|
||||
|
||||
export default ServiceMapOptions
|
@@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.591 9.99997C18.591 14.7446 14.7447 18.5909 10.0001 18.5909C5.25546 18.5909 1.40918 14.7446 1.40918 9.99997C1.40918 5.25534 5.25546 1.40906 10.0001 1.40906C14.7447 1.40906 18.591 5.25534 18.591 9.99997Z" fill="#E9EBF8" stroke="#BCCEFD"/>
|
||||
<path d="M13.1604 8.23038L11.95 7.01994L10.1392 8.83078L8.32832 7.01994L7.11789 8.23038L8.92872 10.0412L7.12046 11.8495L8.33089 13.0599L10.1392 11.2517L11.9474 13.0599L13.1579 11.8495L11.3496 10.0412L13.1604 8.23038Z" fill="#205CF5"/>
|
||||
</svg>
|
After Width: | Height: | Size: 588 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.8337 11.9167H7.69308L7.69416 11.907C7.83561 11.2143 8.11247 10.5564 8.50883 9.97105C9.09865 9.10202 9.92598 8.42097 10.8922 8.00913C11.2193 7.87046 11.5606 7.7643 11.9083 7.69388C12.6297 7.54762 13.3731 7.54762 14.0945 7.69388C15.1312 7.90631 16.0825 8.41908 16.8299 9.1683L18.3639 7.63863C17.6725 6.94707 16.8546 6.39501 15.9546 6.01255C15.4956 5.81823 15.0184 5.67016 14.53 5.57055C13.5223 5.36581 12.4838 5.36581 11.4761 5.57055C10.9873 5.67057 10.5098 5.819 10.0504 6.01363C8.69682 6.58791 7.53808 7.54123 6.71374 8.7588C6.15895 9.5798 5.77099 10.5019 5.57191 11.4725C5.54158 11.6188 5.52533 11.7683 5.50366 11.9167H2.16699L6.50033 16.25L10.8337 11.9167ZM15.167 14.0834H18.3076L18.3065 14.092C18.0234 15.4806 17.205 16.7019 16.0282 17.4915C15.443 17.8882 14.7851 18.1651 14.0923 18.3062C13.3713 18.4525 12.6283 18.4525 11.9072 18.3062C11.2146 18.1648 10.5567 17.8879 9.97133 17.4915C9.68383 17.2971 9.41541 17.0758 9.16966 16.8307L7.63783 18.3625C8.32954 19.0539 9.14791 19.6056 10.0482 19.9875C10.5076 20.1825 10.9875 20.331 11.4728 20.4295C12.4801 20.6344 13.5184 20.6344 14.5257 20.4295C16.4676 20.0265 18.1757 18.8819 19.2869 17.2391C19.8412 16.4187 20.2288 15.4974 20.4277 14.5275C20.4569 14.3813 20.4742 14.2318 20.4959 14.0834H23.8337L19.5003 9.75005L15.167 14.0834Z" fill="#205CF5"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: none; display: block; shape-rendering: auto;" width="200px" height="200px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
|
||||
<circle cx="50" cy="50" fill="none" stroke="#1d3f72" stroke-width="10" r="35" stroke-dasharray="164.93361431346415 56.97787143782138" transform="rotate(275.903 50 50)">
|
||||
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s" values="0 50 50;360 50 50" keyTimes="0;1"></animateTransform>
|
||||
</circle>
|
||||
<!-- [ldio] generated by https://loading.io/ --></svg>
|
After Width: | Height: | Size: 673 B |
20
ui-common/src/components/UI/NoDataMessage.tsx
Normal file
20
ui-common/src/components/UI/NoDataMessage.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
import circleImg from 'assets/dotted-circle.svg';
|
||||
import styles from './style/NoDataMessage.module.sass'
|
||||
|
||||
export interface Props {
|
||||
messageText: string;
|
||||
}
|
||||
|
||||
const NoDataMessage: React.FC<Props> = ({ messageText = "No data found" }) => {
|
||||
return (
|
||||
<div data-cy="noDataMessage" className={styles.messageContainer__noData}>
|
||||
<div className={styles.container}>
|
||||
<img src={circleImg} alt="No data Found"></img>
|
||||
<div className={styles.messageContainer__noDataMessage}>{messageText}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoDataMessage;
|
17
ui-common/src/components/UI/Radio.tsx
Normal file
17
ui-common/src/components/UI/Radio.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
|
||||
export interface Props {
|
||||
checked: boolean;
|
||||
onToggle: (checked: boolean) => any;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const Radio: React.FC<Props> = ({ checked, onToggle, disabled, ...props }) => {
|
||||
return (
|
||||
<div>
|
||||
<input style={!disabled ? { cursor: "pointer" } : {}} type="radio" checked={checked} disabled={disabled} onChange={(event) => onToggle(event.target.checked)} {...props} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Radio;
|
103
ui-common/src/components/UI/SelectList.tsx
Normal file
103
ui-common/src/components/UI/SelectList.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import React, { useMemo } from "react";
|
||||
import Radio from "./Radio";
|
||||
import styles from './style/SelectList.module.sass'
|
||||
import NoDataMessage from "./NoDataMessage";
|
||||
import Checkbox from "./Checkbox";
|
||||
|
||||
|
||||
export interface Props {
|
||||
items;
|
||||
tableName: string;
|
||||
checkedValues?: string[];
|
||||
multiSelect: boolean;
|
||||
searchValue?: string;
|
||||
setCheckedValues: (newValues) => void;
|
||||
tableClassName?
|
||||
checkBoxWidth?: string
|
||||
}
|
||||
|
||||
const SelectList: React.FC<Props> = ({ items, tableName, checkedValues = [], multiSelect = true, searchValue = "", setCheckedValues, tableClassName,
|
||||
checkBoxWidth = 50 }) => {
|
||||
const noItemsMessage = "No items to show";
|
||||
const enabledItemsLength = useMemo(() => items.filter(item => !item.disabled).length, [items]);
|
||||
|
||||
const filteredValues = useMemo(() => {
|
||||
return items.filter((listValue) => listValue?.value?.includes(searchValue));
|
||||
}, [items, searchValue])
|
||||
|
||||
const toggleValue = (checkedKey) => {
|
||||
if (!multiSelect) {
|
||||
const newCheckedValues = [];
|
||||
newCheckedValues.push(checkedKey);
|
||||
setCheckedValues(newCheckedValues);
|
||||
}
|
||||
else {
|
||||
const newCheckedValues = [...checkedValues];
|
||||
let index = newCheckedValues.indexOf(checkedKey);
|
||||
if (index > -1)
|
||||
newCheckedValues.splice(index, 1);
|
||||
else
|
||||
newCheckedValues.push(checkedKey);
|
||||
setCheckedValues(newCheckedValues);
|
||||
}
|
||||
}
|
||||
|
||||
const toggleAll = () => {
|
||||
const newCheckedValues = [...checkedValues];
|
||||
if (newCheckedValues.length === enabledItemsLength) setCheckedValues([]);
|
||||
else {
|
||||
items.forEach((obj) => {
|
||||
if (!obj.disabled && !newCheckedValues.includes(obj.key))
|
||||
newCheckedValues.push(obj.key);
|
||||
})
|
||||
setCheckedValues(newCheckedValues);
|
||||
}
|
||||
}
|
||||
|
||||
const dataFieldFunc = (listValue) => listValue.component ? listValue.component :
|
||||
<span className={styles.nowrap} title={listValue.value}>
|
||||
{listValue.value}
|
||||
</span>
|
||||
|
||||
const tableHead = multiSelect ? <tr style={{ borderBottomWidth: "2px" }}>
|
||||
<th style={{ width: checkBoxWidth }}><Checkbox data-cy="checkbox-all" checked={enabledItemsLength === checkedValues.length}
|
||||
onToggle={toggleAll} /></th>
|
||||
<th>{tableName}</th>
|
||||
</tr> :
|
||||
<tr style={{ borderBottomWidth: "2px" }}>
|
||||
<th>{tableName}</th>
|
||||
</tr>
|
||||
|
||||
const tableBody = filteredValues.length === 0 ?
|
||||
<tr>
|
||||
<td>
|
||||
<NoDataMessage messageText={noItemsMessage} />
|
||||
</td>
|
||||
</tr>
|
||||
:
|
||||
filteredValues?.map(listValue => {
|
||||
return <tr key={listValue.key}>
|
||||
<td style={{ width: checkBoxWidth }}>
|
||||
{multiSelect && <Checkbox data-cy={"checkbox-" + listValue.value} disabled={listValue.disabled} checked={checkedValues.includes(listValue.key)} onToggle={() => toggleValue(listValue.key)} />}
|
||||
{!multiSelect && <Radio data-cy={"radio-" + listValue.value} disabled={listValue.disabled} checked={checkedValues.includes(listValue.key)} onToggle={() => toggleValue(listValue.key)} />}
|
||||
</td>
|
||||
<td>
|
||||
{dataFieldFunc(listValue)}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
)
|
||||
|
||||
return <div className={tableClassName ? tableClassName + ` ${styles.selectListTable}` : ` ${styles.selectListTable}`}>
|
||||
<table cellPadding={5} style={{ borderCollapse: "collapse" }}>
|
||||
<thead>
|
||||
{tableHead}
|
||||
</thead>
|
||||
<tbody>
|
||||
{tableBody}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default SelectList;
|
3
ui-common/src/components/UI/assets/dotted-circle.svg
Normal file
3
ui-common/src/components/UI/assets/dotted-circle.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="55" height="55" viewBox="0 0 55 55" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="27.5" cy="27.5" r="27" stroke="#BCCEFD" stroke-dasharray="6 6"/>
|
||||
</svg>
|
After Width: | Height: | Size: 180 B |
@@ -6,7 +6,9 @@ import Checkbox from "./Checkbox"
|
||||
import { StatusBar } from "./StatusBar";
|
||||
import CustomModal from "./CustomModal";
|
||||
import { InformationIcon } from "./InformationIcon";
|
||||
import SelectList from "./SelectList";
|
||||
import NoDataMessage from "./NoDataMessage";
|
||||
|
||||
|
||||
export {LoadingOverlay,Select,Tabs,Tooltip,Checkbox,CustomModal,InformationIcon}
|
||||
export {StatusBar}
|
||||
export { LoadingOverlay, Select, Tabs, Tooltip, Checkbox, CustomModal, InformationIcon, SelectList, NoDataMessage }
|
||||
export { StatusBar }
|
32
ui-common/src/components/UI/style/NoDataMessage.module.sass
Normal file
32
ui-common/src/components/UI/style/NoDataMessage.module.sass
Normal file
@@ -0,0 +1,32 @@
|
||||
@import '../../../variables.module'
|
||||
|
||||
.messageContainer
|
||||
width: 100%
|
||||
margin-top: 20px
|
||||
|
||||
&__noData
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
flex-direction: column
|
||||
height: 95px
|
||||
margin: 2%
|
||||
align-items: center
|
||||
align-content: center
|
||||
padding-top: 3%
|
||||
padding-bottom: 3%
|
||||
|
||||
& .container
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
flex-direction: column
|
||||
height: 95px
|
||||
margin: 1%
|
||||
align-items: center
|
||||
align-content: center
|
||||
|
||||
&-message
|
||||
font-style: normal
|
||||
font-weight: 600
|
||||
font-size: 12px
|
||||
line-height: 15px
|
||||
color: $light-gray
|
31
ui-common/src/components/UI/style/SelectList.module.sass
Normal file
31
ui-common/src/components/UI/style/SelectList.module.sass
Normal file
@@ -0,0 +1,31 @@
|
||||
@import '../../../variables.module'
|
||||
|
||||
.selectListTable
|
||||
table
|
||||
width: 100%
|
||||
margin-top: 20px
|
||||
height: 100%
|
||||
|
||||
tbody
|
||||
display: block
|
||||
|
||||
th
|
||||
color: $blue-gray
|
||||
text-align: left
|
||||
padding: 10px
|
||||
|
||||
tr
|
||||
border-bottom-width: 1px
|
||||
border-bottom-color: $data-background-color
|
||||
border-bottom-style: solid
|
||||
display: table
|
||||
table-layout: fixed
|
||||
width: 100%
|
||||
|
||||
td
|
||||
color: $light-gray
|
||||
padding: 10px
|
||||
font-size: 16px
|
||||
|
||||
.nowrap
|
||||
white-space: nowrap
|
@@ -1,7 +1,6 @@
|
||||
@import "../../variables.module"
|
||||
@import "../../../variables.module"
|
||||
|
||||
.spinnerContainer
|
||||
display: flex
|
||||
justify-content: center
|
||||
margin-bottom: 10px
|
||||
|
6
ui-common/src/helpers/Utils.ts
Normal file
6
ui-common/src/helpers/Utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
const IP_ADDRESS_REGEX = /([0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3})(:([0-9]{1,5}))?/
|
||||
|
||||
|
||||
export class Utils {
|
||||
static isIpAddress = (address: string): boolean => IP_ADDRESS_REGEX.test(address)
|
||||
}
|
@@ -1,10 +1,11 @@
|
||||
import TrafficViewer from './components/TrafficViewer/TrafficViewer';
|
||||
import * as UI from "./components/UI"
|
||||
import { StatusBar } from './components/UI';
|
||||
import useWS,{DEFAULT_QUERY} from './hooks/useWS';
|
||||
import {AnalyzeButton} from "./components/AnalyzeButton/AnalyzeButton"
|
||||
import useWS, { DEFAULT_QUERY } from './hooks/useWS';
|
||||
import { AnalyzeButton } from "./components/AnalyzeButton/AnalyzeButton"
|
||||
import OasModal from './components/OasModal/OasModal';
|
||||
import { ServiceMapModal } from './components/ServiceMapModal/ServiceMapModal';
|
||||
|
||||
export {UI,AnalyzeButton, StatusBar, OasModal}
|
||||
export { useWS, DEFAULT_QUERY}
|
||||
export { UI, AnalyzeButton, StatusBar, OasModal, ServiceMapModal }
|
||||
export { useWS, DEFAULT_QUERY }
|
||||
export default TrafficViewer;
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { useState} from 'react';
|
||||
import { useState } from 'react';
|
||||
import './App.sass';
|
||||
import {Header} from "./components/Header/Header";
|
||||
import {TrafficPage} from "./components/Pages/TrafficPage/TrafficPage";
|
||||
import { ServiceMapModal } from './components/ServiceMapModal/ServiceMapModal';
|
||||
import {useRecoilState} from "recoil";
|
||||
import { Header } from "./components/Header/Header";
|
||||
import { TrafficPage } from "./components/Pages/TrafficPage/TrafficPage";
|
||||
import { ServiceMapModal } from '@up9/mizu-common';
|
||||
import { useRecoilState } from "recoil";
|
||||
import serviceMapModalOpenAtom from "./recoil/serviceMapModalOpen";
|
||||
import oasModalOpenAtom from './recoil/oasModalOpen/atom';
|
||||
import {OasModal} from '@up9/mizu-common';
|
||||
import { OasModal } from '@up9/mizu-common';
|
||||
import Api from './helpers/api';
|
||||
|
||||
const api = Api.getInstance()
|
||||
@@ -19,20 +19,20 @@ const App = () => {
|
||||
|
||||
return (
|
||||
<div className="mizuApp">
|
||||
<Header analyzeStatus={analyzeStatus} />
|
||||
<TrafficPage setAnalyzeStatus={setAnalyzeStatus}/>
|
||||
{window["isServiceMapEnabled"] && <ServiceMapModal
|
||||
<Header analyzeStatus={analyzeStatus} />
|
||||
<TrafficPage setAnalyzeStatus={setAnalyzeStatus} />
|
||||
{window["isServiceMapEnabled"] && <ServiceMapModal
|
||||
isOpen={serviceMapModalOpen}
|
||||
onOpen={() => setServiceMapModalOpen(true)}
|
||||
onClose={() => setServiceMapModalOpen(false)}
|
||||
getServiceMapDataApi={api.serviceMapData} />}
|
||||
{window["isOasEnabled"] && <OasModal
|
||||
getOasServices={api.getOasServices}
|
||||
getOasByService={api.getOasByService}
|
||||
openModal={oasModalOpen}
|
||||
handleCloseModal={() => setOasModalOpen(false)}
|
||||
/>}
|
||||
{window["isOasEnabled"] && <OasModal
|
||||
getOasServices={api.getOasServices}
|
||||
getOasByService={api.getOasByService}
|
||||
openModal={oasModalOpen}
|
||||
handleCloseModal={() => setOasModalOpen(false)}
|
||||
/>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -1,86 +0,0 @@
|
||||
import {Button} from "@material-ui/core";
|
||||
import React from "react";
|
||||
import {UI} from "@up9/mizu-common";
|
||||
import logo_up9 from "../assets/logo_up9.svg";
|
||||
import {makeStyles} from "@material-ui/core/styles";
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
tooltip: {
|
||||
backgroundColor: "#3868dc",
|
||||
color: "white",
|
||||
fontSize: 13,
|
||||
},
|
||||
}));
|
||||
|
||||
interface AnalyseButtonProps {
|
||||
analyzeStatus: any
|
||||
}
|
||||
|
||||
export const AnalyzeButton: React.FC<AnalyseButtonProps> = ({analyzeStatus}) => {
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
const analysisMessage = analyzeStatus?.isRemoteReady ?
|
||||
<span>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td><b>Available</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Messages</td>
|
||||
<td><b>{analyzeStatus?.sentCount}</b></td>
|
||||
</tr>
|
||||
</table>
|
||||
</span> :
|
||||
analyzeStatus?.sentCount > 0 ?
|
||||
<span>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td><b>Processing</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Messages</td>
|
||||
<td><b>{analyzeStatus?.sentCount}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan={2}> Please allow a few minutes for the analysis to complete</td>
|
||||
</tr>
|
||||
</table>
|
||||
</span> :
|
||||
<span>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td><b>Waiting for traffic</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Messages</td>
|
||||
<td><b>{analyzeStatus?.sentCount}</b></td>
|
||||
</tr>
|
||||
</table>
|
||||
</span>
|
||||
|
||||
return ( <div>
|
||||
<UI.Tooltip title={analysisMessage} isSimple classes={classes}>
|
||||
<div>
|
||||
<Button
|
||||
style={{fontFamily: "system-ui",
|
||||
fontWeight: 600,
|
||||
fontSize: 12,
|
||||
padding: 8}}
|
||||
size={"small"}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<img style={{height: 24, maxHeight: "none", maxWidth: "none"}} src={logo_up9} alt={"up9"}/>}
|
||||
disabled={!analyzeStatus?.isRemoteReady}
|
||||
onClick={() => {
|
||||
window.open(analyzeStatus?.remoteUrl)
|
||||
}}>
|
||||
Analysis
|
||||
</Button>
|
||||
</div>
|
||||
</UI.Tooltip>
|
||||
</div>);
|
||||
}
|
@@ -1,31 +0,0 @@
|
||||
@import "../../variables.module"
|
||||
|
||||
.legend-scale ul
|
||||
margin-top: -29px
|
||||
margin-left: -27px
|
||||
padding: 0
|
||||
float: left
|
||||
list-style: none
|
||||
|
||||
li
|
||||
display: block
|
||||
float: left
|
||||
width: 50px
|
||||
margin-bottom: 6px
|
||||
text-align: center
|
||||
font-size: 80%
|
||||
list-style: none
|
||||
|
||||
ul.legend-labels li span
|
||||
display: block
|
||||
float: left
|
||||
height: 15px
|
||||
width: 50px
|
||||
|
||||
.legend-source
|
||||
font-size: 70%
|
||||
color: #999
|
||||
clear: both
|
||||
|
||||
a
|
||||
color: #777
|
@@ -1,223 +0,0 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { Box, Fade, Modal, Backdrop, Button } from "@material-ui/core";
|
||||
import { toast } from "react-toastify";
|
||||
import Api from "../../helpers/api";
|
||||
import spinnerStyle from '../style/Spinner.module.sass';
|
||||
import './ServiceMapModal.sass';
|
||||
import spinnerImg from '../assets/spinner.svg';
|
||||
import Graph from "react-graph-vis";
|
||||
import debounce from 'lodash/debounce';
|
||||
import ServiceMapOptions from './ServiceMapOptions'
|
||||
import { useCommonStyles } from "../../helpers/commonStyle";
|
||||
import refresh from "../assets/refresh.svg";
|
||||
import close from "../assets/close.svg";
|
||||
import { TOAST_CONTAINER_ID } from "../../consts";
|
||||
|
||||
interface GraphData {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
}
|
||||
|
||||
interface Node {
|
||||
id: number;
|
||||
value: number;
|
||||
label: string;
|
||||
title?: string;
|
||||
color?: object;
|
||||
}
|
||||
|
||||
interface Edge {
|
||||
from: number;
|
||||
to: number;
|
||||
value: number;
|
||||
label: string;
|
||||
title?: string;
|
||||
color?: object;
|
||||
font?: object;
|
||||
}
|
||||
|
||||
interface ServiceMapNode {
|
||||
id: number;
|
||||
name: string;
|
||||
entry: Entry;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface ServiceMapEdge {
|
||||
source: ServiceMapNode;
|
||||
destination: ServiceMapNode;
|
||||
count: number;
|
||||
protocol: Protocol;
|
||||
}
|
||||
|
||||
interface ServiceMapGraph {
|
||||
nodes: ServiceMapNode[];
|
||||
edges: ServiceMapEdge[];
|
||||
}
|
||||
|
||||
interface Entry {
|
||||
ip: string;
|
||||
port: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface Protocol {
|
||||
name: string;
|
||||
abbr: string;
|
||||
macro: string;
|
||||
version: string;
|
||||
backgroundColor: string;
|
||||
foregroundColor: string;
|
||||
fontSize: number;
|
||||
referenceLink: string;
|
||||
ports: string[];
|
||||
priority: number;
|
||||
}
|
||||
|
||||
interface ServiceMapModalProps {
|
||||
isOpen: boolean;
|
||||
onOpen: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute',
|
||||
top: '6%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, 0%)',
|
||||
width: '89vw',
|
||||
height: '82vh',
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: '5px',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
color: '#000',
|
||||
};
|
||||
|
||||
const api = Api.getInstance();
|
||||
|
||||
export const ServiceMapModal: React.FC<ServiceMapModalProps> = ({ isOpen, onOpen, onClose }) => {
|
||||
const commonClasses = useCommonStyles();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [graphData, setGraphData] = useState<GraphData>({ nodes: [], edges: [] });
|
||||
const [graphOptions, setGraphOptions] = useState(ServiceMapOptions);
|
||||
|
||||
const getServiceMapData = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
|
||||
const serviceMapData: ServiceMapGraph = await api.serviceMapData()
|
||||
const newGraphData: GraphData = { nodes: [], edges: [] }
|
||||
|
||||
if (serviceMapData.nodes) {
|
||||
newGraphData.nodes = serviceMapData.nodes.map<Node>(node => {
|
||||
return {
|
||||
id: node.id,
|
||||
value: node.count,
|
||||
label: (node.entry.name === "unresolved") ? node.name : `${node.entry.name} (${node.name})`,
|
||||
title: "Count: " + node.name,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (serviceMapData.edges) {
|
||||
newGraphData.edges = serviceMapData.edges.map<Edge>(edge => {
|
||||
return {
|
||||
from: edge.source.id,
|
||||
to: edge.destination.id,
|
||||
value: edge.count,
|
||||
label: edge.count.toString(),
|
||||
color: {
|
||||
color: edge.protocol.backgroundColor,
|
||||
highlight: edge.protocol.backgroundColor
|
||||
},
|
||||
font: {
|
||||
color: edge.protocol.backgroundColor,
|
||||
strokeColor: edge.protocol.backgroundColor
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setGraphData(newGraphData)
|
||||
|
||||
} catch (ex) {
|
||||
toast.error("An error occurred while loading Mizu Service Map, see console for mode details", { containerId: TOAST_CONTAINER_ID });
|
||||
console.error(ex);
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [isOpen])
|
||||
|
||||
useEffect(() => {
|
||||
if(graphData?.nodes?.length === 0) return;
|
||||
let options = {...graphOptions};
|
||||
options.physics.barnesHut.avoidOverlap = graphData?.nodes?.length > 10 ? 0 : 1;
|
||||
setGraphOptions(options);
|
||||
// eslint-disable-next-line
|
||||
},[graphData?.nodes?.length])
|
||||
|
||||
useEffect(() => {
|
||||
getServiceMapData();
|
||||
return () => setGraphData({ nodes: [], edges: [] })
|
||||
}, [getServiceMapData])
|
||||
|
||||
const refreshServiceMap = debounce(() => {
|
||||
getServiceMapData();
|
||||
}, 500);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
aria-labelledby="transition-modal-title"
|
||||
aria-describedby="transition-modal-description"
|
||||
open={isOpen}
|
||||
onClose={onClose}
|
||||
closeAfterTransition
|
||||
BackdropComponent={Backdrop}
|
||||
BackdropProps={{
|
||||
timeout: 500,
|
||||
}}
|
||||
style={{ overflow: 'auto' }}
|
||||
>
|
||||
<Fade in={isOpen}>
|
||||
<Box sx={modalStyle}>
|
||||
{isLoading && <div className={spinnerStyle.spinnerContainer}>
|
||||
<img alt="spinner" src={spinnerImg} style={{ height: 50 }} />
|
||||
</div>}
|
||||
{!isLoading && <div style={{ height: "100%", width: "100%" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<div>
|
||||
<Button
|
||||
startIcon={<img src={refresh} className="custom" alt="refresh" style={{ marginRight: "8%" }}/>}
|
||||
size="medium"
|
||||
variant="contained"
|
||||
className={commonClasses.outlinedButton + " " + commonClasses.imagedButton}
|
||||
onClick={refreshServiceMap}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
<img src={close} alt="close" onClick={() => onClose()} style={{ cursor: "pointer" }}/>
|
||||
</div>
|
||||
<Graph
|
||||
graph={graphData}
|
||||
options={graphOptions}
|
||||
/>
|
||||
<div className='legend-scale'>
|
||||
<ul className='legend-labels'>
|
||||
<li><span style={{ background: '#205cf5' }}/>HTTP</li>
|
||||
<li><span style={{ background: '#244c5a' }}/>HTTP/2</li>
|
||||
<li><span style={{ background: '#244c5a' }}/>gRPC</li>
|
||||
<li><span style={{ background: '#ff6600' }}/>AMQP</li>
|
||||
<li><span style={{ background: '#000000' }}/>KAFKA</li>
|
||||
<li><span style={{ background: '#a41e11' }}/>REDIS</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>}
|
||||
</Box>
|
||||
</Fade>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
}
|
@@ -1,174 +0,0 @@
|
||||
|
||||
const minNodeScaling = 10
|
||||
const maxNodeScaling = 30
|
||||
|
||||
const minEdgeScaling = 1
|
||||
const maxEdgeScaling = maxNodeScaling / 2
|
||||
|
||||
const minLabelScaling = 11
|
||||
const maxLabelScaling = 16
|
||||
const selectedNodeColor = "#0C0B1A"
|
||||
const selectedNodeBorderColor = "#205CF5"
|
||||
const selectedNodeLabelColor = "#205CF5"
|
||||
const selectedEdgeLabelColor = "#205CF5"
|
||||
|
||||
const customScaling = (min, max, total, value) => {
|
||||
if (max === min) {
|
||||
return 0.5;
|
||||
}
|
||||
else {
|
||||
const scale = 1 / (max - min);
|
||||
return Math.max(0, (value - min) * scale);
|
||||
}
|
||||
}
|
||||
|
||||
const nodeSelected = (values, id, selected, hovering) => {
|
||||
values.color = selectedNodeColor;
|
||||
values.borderColor = selectedNodeBorderColor;
|
||||
values.borderWidth = 4;
|
||||
}
|
||||
|
||||
const nodeLabelSelected = (values, id, selected, hovering) => {
|
||||
values.size = values.size + 1;
|
||||
values.color = selectedNodeLabelColor;
|
||||
values.strokeColor = selectedNodeLabelColor;
|
||||
values.strokeWidth = 0.2
|
||||
}
|
||||
|
||||
const edgeSelected = (values, id, selected, hovering) => {
|
||||
values.opacity = 0.4;
|
||||
values.width = values.width + 1;
|
||||
}
|
||||
|
||||
const edgeLabelSelected = (values, id, selected, hovering) => {
|
||||
values.size = values.size + 1;
|
||||
values.color = selectedEdgeLabelColor;
|
||||
values.strokeColor = selectedEdgeLabelColor;
|
||||
values.strokeWidth = 0.2
|
||||
}
|
||||
|
||||
const nodeOptions = {
|
||||
shape: 'dot',
|
||||
chosen: {
|
||||
node: nodeSelected,
|
||||
label: nodeLabelSelected,
|
||||
},
|
||||
color: {
|
||||
background: '#494677',
|
||||
border: selectedNodeColor,
|
||||
},
|
||||
font: {
|
||||
color: selectedNodeColor,
|
||||
size: 11, // px
|
||||
face: 'Roboto',
|
||||
background: '#FFFFFFBF',
|
||||
strokeWidth: 0.2, // px
|
||||
strokeColor: selectedNodeColor,
|
||||
align: 'center',
|
||||
multi: false,
|
||||
},
|
||||
// defines the node min and max sizes when zoom in/out, based on the node value
|
||||
scaling: {
|
||||
min: minNodeScaling,
|
||||
max: maxNodeScaling,
|
||||
// defines the label scaling size in px
|
||||
label: {
|
||||
enabled: true,
|
||||
min: minLabelScaling,
|
||||
max: maxLabelScaling,
|
||||
maxVisible: maxLabelScaling,
|
||||
drawThreshold: 5,
|
||||
},
|
||||
customScalingFunction: customScaling,
|
||||
},
|
||||
borderWidth: 2,
|
||||
labelHighlightBold: true,
|
||||
opacity: 1,
|
||||
shadow: true,
|
||||
}
|
||||
|
||||
const edgeOptions = {
|
||||
chosen: {
|
||||
edge: edgeSelected,
|
||||
label: edgeLabelSelected,
|
||||
},
|
||||
dashes: false,
|
||||
arrowStrikethrough: false,
|
||||
arrows: {
|
||||
to: {
|
||||
enabled: true,
|
||||
},
|
||||
middle: {
|
||||
enabled: false,
|
||||
},
|
||||
from: {
|
||||
enabled: false,
|
||||
}
|
||||
},
|
||||
smooth: {
|
||||
enabled: true,
|
||||
type: 'dynamic',
|
||||
roundness: 1.0
|
||||
},
|
||||
font: {
|
||||
color: '#000000',
|
||||
size: 11, // px
|
||||
face: 'Roboto',
|
||||
background: '#FFFFFFCC',
|
||||
strokeWidth: 0.2, // px
|
||||
strokeColor: '#000000',
|
||||
align: 'horizontal',
|
||||
multi: false,
|
||||
},
|
||||
scaling: {
|
||||
min: minEdgeScaling,
|
||||
max: maxEdgeScaling,
|
||||
label: {
|
||||
enabled: true,
|
||||
min: minLabelScaling,
|
||||
max: maxLabelScaling,
|
||||
maxVisible: maxLabelScaling,
|
||||
drawThreshold: 5
|
||||
},
|
||||
customScalingFunction: customScaling,
|
||||
},
|
||||
labelHighlightBold: true,
|
||||
selectionWidth: 1,
|
||||
shadow: true,
|
||||
}
|
||||
|
||||
const ServiceMapOptions = {
|
||||
physics: {
|
||||
enabled: true,
|
||||
solver: 'barnesHut',
|
||||
barnesHut: {
|
||||
theta: 0.5,
|
||||
gravitationalConstant: -2000,
|
||||
centralGravity: 0.4,
|
||||
springLength: 180,
|
||||
springConstant: 0.04,
|
||||
damping: 0.2,
|
||||
avoidOverlap: 0
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
hierarchical: false,
|
||||
randomSeed: 1 // always on node 1
|
||||
},
|
||||
nodes: nodeOptions,
|
||||
edges: edgeOptions,
|
||||
autoResize: true,
|
||||
interaction: {
|
||||
selectable: true,
|
||||
selectConnectedEdges: true,
|
||||
multiselect: true,
|
||||
dragNodes: true,
|
||||
dragView: true,
|
||||
hover: true,
|
||||
hoverConnectedEdges: true,
|
||||
zoomView: true,
|
||||
zoomSpeed: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export default ServiceMapOptions
|
@@ -1,7 +0,0 @@
|
||||
@import "../../variables.module"
|
||||
|
||||
.spinnerContainer
|
||||
display: flex
|
||||
justify-content: center
|
||||
margin-bottom: 10px
|
||||
|
@@ -1,7 +0,0 @@
|
||||
const dictionary = {
|
||||
ctrlEnter : [{metaKey : true, code:"Enter"}, {ctrlKey:true, code:"Enter"}], // support Ctrl/command
|
||||
enter : [{code:"Enter"}]
|
||||
};
|
||||
|
||||
|
||||
export default dictionary;
|
@@ -25,21 +25,11 @@ export default class Api {
|
||||
source = null;
|
||||
}
|
||||
|
||||
serviceMapStatus = async () => {
|
||||
const response = await client.get("/servicemap/status");
|
||||
return response.data;
|
||||
}
|
||||
|
||||
serviceMapData = async () => {
|
||||
const response = await client.get(`/servicemap/get`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
serviceMapReset = async () => {
|
||||
const response = await client.get(`/servicemap/reset`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
tapStatus = async () => {
|
||||
const response = await client.get("/status/tap");
|
||||
return response.data;
|
||||
|
@@ -1,5 +0,0 @@
|
||||
export enum RouterRoutes {
|
||||
LOGIN = "/login",
|
||||
SETUP = "/setup",
|
||||
SETTINGS = "/settings"
|
||||
}
|
@@ -1,38 +0,0 @@
|
||||
import { useCallback, useEffect, useLayoutEffect, useRef } from 'react';
|
||||
|
||||
const useKeyPress = (eventConfigs, callback, node = null) => {
|
||||
// implement the callback ref pattern
|
||||
const callbackRef = useRef(callback);
|
||||
useLayoutEffect(() => {
|
||||
callbackRef.current = callback;
|
||||
});
|
||||
|
||||
// handle what happens on key press
|
||||
const handleKeyPress = useCallback(
|
||||
(event) => {
|
||||
|
||||
// check if one of the key is part of the ones we want
|
||||
if (eventConfigs.some((eventConfig) => Object.keys(eventConfig).every(nameKey => eventConfig[nameKey] === event[nameKey]))) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault();
|
||||
callbackRef.current(event);
|
||||
}
|
||||
},
|
||||
[eventConfigs]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// target is either the provided node or the document
|
||||
const targetNode = node ?? document;
|
||||
// attach the event listener
|
||||
targetNode &&
|
||||
targetNode.addEventListener("keydown", handleKeyPress);
|
||||
|
||||
// remove the event listener
|
||||
return () =>
|
||||
targetNode &&
|
||||
targetNode.removeEventListener("keydown", handleKeyPress);
|
||||
}, [handleKeyPress, node]);
|
||||
};
|
||||
|
||||
export default useKeyPress;
|
@@ -1,8 +0,0 @@
|
||||
import { atom } from "recoil"
|
||||
|
||||
const entPageAtom = atom({
|
||||
key: "entPageAtom",
|
||||
default: 0
|
||||
})
|
||||
|
||||
export default entPageAtom
|
@@ -1,11 +0,0 @@
|
||||
import atom from "./atom";
|
||||
|
||||
enum Page {
|
||||
Traffic,
|
||||
Setup,
|
||||
Login
|
||||
}
|
||||
|
||||
export { Page };
|
||||
|
||||
export default atom;
|
@@ -1,8 +0,0 @@
|
||||
import { atom } from "recoil";
|
||||
|
||||
const entriesAtom = atom({
|
||||
key: "entriesAtom",
|
||||
default: []
|
||||
});
|
||||
|
||||
export default entriesAtom;
|
@@ -1,3 +0,0 @@
|
||||
import atom from "./atom";
|
||||
|
||||
export default atom
|
@@ -1,9 +0,0 @@
|
||||
import { atom } from "recoil";
|
||||
import {TappingStatusPod} from "./index";
|
||||
|
||||
const tappingStatusAtom = atom({
|
||||
key: "tappingStatusAtom",
|
||||
default: null as TappingStatusPod[]
|
||||
});
|
||||
|
||||
export default tappingStatusAtom;
|
@@ -1,22 +0,0 @@
|
||||
import {selector} from "recoil";
|
||||
import tappingStatusAtom from "./atom";
|
||||
|
||||
const tappingStatusDetails = selector({
|
||||
key: 'tappingStatusDetails',
|
||||
get: ({get}) => {
|
||||
const tappingStatus = get(tappingStatusAtom);
|
||||
const uniqueNamespaces = Array.from(new Set(tappingStatus.map(pod => pod.namespace)));
|
||||
const amountOfPods = tappingStatus.length;
|
||||
const amountOfTappedPods = tappingStatus.filter(pod => pod.isTapped).length;
|
||||
const amountOfUntappedPods = amountOfPods - amountOfTappedPods;
|
||||
|
||||
return {
|
||||
uniqueNamespaces,
|
||||
amountOfPods,
|
||||
amountOfTappedPods,
|
||||
amountOfUntappedPods,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default tappingStatusDetails;
|
@@ -1,17 +0,0 @@
|
||||
import atom from "./atom";
|
||||
import tappingStatusDetails from './details';
|
||||
|
||||
interface TappingStatusPod {
|
||||
name: string;
|
||||
namespace: string;
|
||||
isTapped: boolean;
|
||||
}
|
||||
|
||||
interface TappingStatus {
|
||||
pods: TappingStatusPod[];
|
||||
}
|
||||
|
||||
export type {TappingStatus, TappingStatusPod};
|
||||
export {tappingStatusDetails};
|
||||
|
||||
export default atom;
|
Reference in New Issue
Block a user