diff --git a/src/floatPanels/TopDisplayOptionsPanel.tsx b/src/floatPanels/TopDisplayOptionsPanel.tsx new file mode 100644 index 0000000..9ad96e0 --- /dev/null +++ b/src/floatPanels/TopDisplayOptionsPanel.tsx @@ -0,0 +1,24 @@ +import { DisplayPanelOptionEnv } from "../model"; + +interface TopDisplayOptionsPanelProps { + options: DisplayPanelOptionEnv[]; + setShowOptionsPanel: (v: boolean) => void; +} + +export default function TopDisplayOptionsPanel({ options, setShowOptionsPanel }: TopDisplayOptionsPanelProps) { + return ( +
setShowOptionsPanel(false)}> + {options.map((opt, index) => ( +
opt.onClick()}> + {}} + className="top-display-options-panel-item" + /> + {opt.label} +
+ ))} +
+ ); +} diff --git a/src/floatPanels/TopDisplayPanel.tsx b/src/floatPanels/TopDisplayPanel.tsx new file mode 100644 index 0000000..f56d301 --- /dev/null +++ b/src/floatPanels/TopDisplayPanel.tsx @@ -0,0 +1,23 @@ +import { useState } from 'react'; +import TopDisplayOptionsPanel from "./TopDisplayOptionsPanel"; +import { DisplayPanelOptionEnv } from "../model"; + +interface TopDisplayPanelProps { + options: DisplayPanelOptionEnv[]; +} + +export default function TopDisplayPanel({ options }: TopDisplayPanelProps) { + const [showOptionsPanel, setShowOptionsPanel] = useState(false); + + return ( + <> +
setShowOptionsPanel(!showOptionsPanel)} + > +
Display
+
+ {showOptionsPanel && } + + ); +} diff --git a/src/model.ts b/src/model.ts index 5bafc56..257f293 100644 --- a/src/model.ts +++ b/src/model.ts @@ -23,6 +23,7 @@ export type Edge = { export enum NodeType { Cluster = "cluster", Interface = "interface", + LCInterface = "loopConIfNT", Client = "client", Forwarder = "forwarder", Manager = "manager", @@ -34,19 +35,20 @@ export enum NodeType { export enum EdgeType { InterfaceConnection = "interfaceConnection", InterfaceCrossConnection = "interfaceCrossConnection", + InterfaceLoopedConnection = "interfaceLoopedConnection", ServiceRequest = "serviceRequest", RegistryRequest = "registryRequest" } export const AllowedNodeTypes = { - Dataplane: [NodeType.Cluster, NodeType.Interface, NodeType.Forwarder, NodeType.Client, NodeType.Endpoint], + Dataplane: [NodeType.Cluster, NodeType.Interface, NodeType.LCInterface, NodeType.Forwarder, NodeType.Client, NodeType.Endpoint], NetworkServices: [NodeType.Client, NodeType.Service], NetworkServiceRequests: [NodeType.Forwarder, NodeType.Client, NodeType.Endpoint, NodeType.Manager], RegistryRequests: [NodeType.Endpoint, NodeType.Forwarder, NodeType.Manager, NodeType.Registry], } export const AllowedEdgeTypes = { - Dataplane: [EdgeType.InterfaceConnection, EdgeType.InterfaceCrossConnection], + Dataplane: [EdgeType.InterfaceConnection, EdgeType.InterfaceCrossConnection, EdgeType.InterfaceLoopedConnection], NetworkServices: [EdgeType.ServiceRequest], NetworkServiceRequests: [EdgeType.ServiceRequest], RegistryRequests: [EdgeType.RegistryRequest], @@ -80,3 +82,22 @@ export enum LineStyle { Dashed = "dashed", Dotted = "dotted" } + +export enum Page { + Dataplane = "dataplane" +} + +export enum Option { + ShowLoopedConnections = "showLoopedConnections" +} + +export type DisplayPanelOption = { + page: Page, + option: Option +} + +export type DisplayPanelOptionEnv = { + label: string, + onClick: () => void, + checked: boolean, +} diff --git a/src/pages/Dataplane.tsx b/src/pages/Dataplane.tsx index edff26c..bdd9c28 100644 --- a/src/pages/Dataplane.tsx +++ b/src/pages/Dataplane.tsx @@ -1,7 +1,11 @@ import * as React from "react"; +import { useDispatch, useSelector } from 'react-redux'; +import { switchDisplayPanelOption } from '../store/actions'; +import { AppDispatch, RootState } from '../store/store'; import { Stylesheet } from "cytoscape"; import cloneDeep from 'lodash/cloneDeep'; import CytoscapeCanvas from "./CytoscapeCanvas"; +import TopDisplayPanel from "../floatPanels/TopDisplayPanel"; import { NodeType, InterfaceSize, @@ -13,7 +17,10 @@ import { AllowedNodeTypes, AllowedEdgeTypes, Node, - Edge + Edge, + Page, + Option, + DisplayPanelOptionEnv } from "../model"; interface DataplaneProps { @@ -51,6 +58,20 @@ function getDataplaneStylesheet() { "text-border-opacity": 1 } }, + { + selector: `node[type = '${NodeType.LCInterface}']`, + style: { + width: InterfaceSize, + height: InterfaceSize, + "font-size": "8px", + 'text-background-color': Color.Gray, + 'text-background-opacity': 1, + "text-background-shape": "roundrectangle", + 'text-border-color': Color.Gray, + "text-border-width": "0.2em", + "text-border-opacity": 1 + } + }, { selector: `node[type = '${NodeType.Interface}'][label]`, style: { @@ -95,24 +116,51 @@ function getDataplaneStylesheet() { "line-color": (edge: { data: (arg0: string) => boolean }) => edge.data("healthy") === false ? Color.Red : Color.Green } + }, + { + selector: `edge[type = '${EdgeType.InterfaceLoopedConnection}']`, + style: { + "line-color": Color.Gray + } } ]; } export default function Dataplane({ nodes, edges }: DataplaneProps) { + const dispatch = useDispatch(); + const showLoopedConnections = useSelector((state: RootState) => state.app.pages.dataplane.topDisplayPanelOptions.showLoopedConnections); + + const displayPanelOptions: DisplayPanelOptionEnv[] = [ + { + label: 'Looped Connections', + onClick: () => dispatch(switchDisplayPanelOption({ + page: Page.Dataplane, + option: Option.ShowLoopedConnections + })), + checked: showLoopedConnections, + }, + ]; + const [stylesheet] = React.useState(getDataplaneStylesheet() as Stylesheet[]); const dataplaneNodes = nodes .filter(n => AllowedNodeTypes.Dataplane.includes(n.data.type)) + .filter(n => showLoopedConnections ? n : n.data.type !== NodeType.LCInterface) .map(n => ({ ...n, data: cloneDeep(n.data) })); const dataplaneEdges = edges .filter(e => AllowedEdgeTypes.Dataplane.includes(e.data.type)) + .filter(e => showLoopedConnections ? e : e.data.type !== EdgeType.InterfaceLoopedConnection) .map(e => ({ ...e, data: cloneDeep(e.data) })); return ( - + <> + + + ); } diff --git a/src/store/actions.ts b/src/store/actions.ts index 1d1e077..2f9f2af 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -1,5 +1,10 @@ -import { SET_NODES, SET_EDGES, SET_SELECTED_MENU_ITEM } from './types'; -import { Node, Edge } from "../model"; +import { + SET_NODES, + SET_EDGES, + SET_SELECTED_MENU_ITEM, + SWITCH_DISPLAY_PANEL_OPTION +} from './types'; +import { Node, Edge, DisplayPanelOption } from "../model"; export const setNodes = (nodes: Node[]) => ({ type: SET_NODES, @@ -15,3 +20,8 @@ export const setSelectedMenuItem = (selectedMenuItem: number) => ({ type: SET_SELECTED_MENU_ITEM, payload: selectedMenuItem, }); + +export const switchDisplayPanelOption = (displayPanelOption: DisplayPanelOption) => ({ + type: SWITCH_DISPLAY_PANEL_OPTION, + payload: displayPanelOption, +}); diff --git a/src/store/reducer.ts b/src/store/reducer.ts index fd40f86..ecc82ea 100644 --- a/src/store/reducer.ts +++ b/src/store/reducer.ts @@ -1,5 +1,10 @@ -import { SET_NODES, SET_EDGES, SET_SELECTED_MENU_ITEM } from './types'; -import { Node, Edge } from "../model"; +import { + SET_NODES, + SET_EDGES, + SET_SELECTED_MENU_ITEM, + SWITCH_DISPLAY_PANEL_OPTION +} from './types'; +import { Node, Edge, Page, Option } from "../model"; // eslint-disable-next-line @typescript-eslint/no-explicit-any const initialState: { nodes: Node[]; edges: Edge[]; app: any } = { @@ -7,6 +12,13 @@ const initialState: { nodes: Node[]; edges: Edge[]; app: any } = { edges: [], app: { selectedMenuItem: 2, + pages: { + [Page.Dataplane]: { + topDisplayPanelOptions: { + [Option.ShowLoopedConnections]: false + } + } + } } }; @@ -18,7 +30,24 @@ const rootReducer = (state = initialState, action: any) => { case SET_EDGES: return { ...state, edges: action.payload }; case SET_SELECTED_MENU_ITEM: - return { ...state, app: { selectedMenuItem: action.payload} }; + return { ...state, app: { ...state.app, selectedMenuItem: action.payload} }; + case SWITCH_DISPLAY_PANEL_OPTION: + return { + ...state, + app: { + ...state.app, + pages: { + ...state.app.pages, + [action.payload.page]: { + ...state.app.pages[action.payload.page], + topDisplayPanelOptions: { + ...state.app.pages[action.payload.page].topDisplayPanelOptions, + [action.payload.option]: !state.app.pages[action.payload.page].topDisplayPanelOptions[action.payload.option] + } + } + } + } + }; default: return state; } diff --git a/src/store/types.ts b/src/store/types.ts index c30a7e3..2e3dd3b 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -1,3 +1,4 @@ export const SET_NODES = 'SET_NODES'; export const SET_EDGES = 'SET_EDGES'; export const SET_SELECTED_MENU_ITEM ='SET_SELECTED_MENU_ITEM'; +export const SWITCH_DISPLAY_PANEL_OPTION ='SWITCH_DISPLAY_PANEL_OPTION'; diff --git a/src/styles.css b/src/styles.css index e3ce22e..9ee47e9 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,3 +1,9 @@ +:root { + --theme-color: #202020; + --theme-text-color: white; + --floating-panel-background-color: #D9D9D9; +} + body { margin: 0; padding: 0; @@ -5,7 +11,7 @@ body { } .header { - background-color: #202020; + background-color: var(--theme-color); height: 39px; display: flex; justify-content: center; @@ -19,12 +25,12 @@ body { } .header-title { - color: white; + color: var(--theme-text-color); font-size: 16px; } .footer { - background-color: #202020; + background-color: var(--theme-color); height: 16px; display: flex; justify-content: center; @@ -33,17 +39,43 @@ body { } .footer-text { - color: white; + color: var(--theme-text-color); font-size: 9px; } .left-panel { - background-color: #202020; + background-color: var(--theme-color); width: 200px; height: calc(100vh - 55px); padding-left: 10px; } +.top-display-panel { + position: fixed; + left: 220px; + top: 49px; + font-size: 14px; + background-color: var(--floating-panel-background-color); + padding: 5px; + border-radius: 5px; + user-select: none; + cursor: pointer; +} + +.top-display-options-panel { + position: fixed; + left: 220px; + top: 79px; + padding: 10px; + background-color: var(--floating-panel-background-color); + user-select: none; +} + +.top-display-options-panel-item { + cursor: pointer; + margin-bottom: 5px; +} + .menu { list-style: none; padding: 0; @@ -51,7 +83,7 @@ body { } .menu-item { - color: white; + color: var(--theme-text-color); font-size: 16px; cursor: pointer; padding: 5px 0;