From 17b7b313fbed721c7ce3cb080c70e05a8d5675bd Mon Sep 17 00:00:00 2001 From: Vedant Gupta <49195734+veds-g@users.noreply.github.com> Date: Tue, 31 Oct 2023 00:51:13 +0530 Subject: [PATCH] feat: added separate colors for sideInput and dynamic legend (#1292) Signed-off-by: veds-g Co-authored-by: Bradley Behnke --- ui/src/App.tsx | 2 +- .../partials/GeneratorDetails/index.tsx | 2 +- .../common/SummaryPageLayout/index.tsx | 13 +- ui/src/components/pages/Pipeline/index.tsx | 31 +- .../pages/Pipeline/partials/Graph/index.tsx | 507 ++++++++++-------- .../Graph/partials/CustomNode/index.tsx | 64 ++- .../pages/Pipeline/partials/Graph/style.css | 9 +- .../images/{generator.svg => generator0.svg} | 0 ui/src/images/generator1.svg | 3 + ui/src/images/generator2.svg | 3 + ui/src/images/generator3.svg | 3 + ui/src/images/generator4.svg | 3 + ui/src/images/{input.svg => input0.svg} | 0 ui/src/images/input1.svg | 4 + ui/src/images/input2.svg | 4 + ui/src/images/input3.svg | 4 + ui/src/images/input4.svg | 4 + ui/src/types/declarations/graph.d.ts | 3 + .../utils/fetcherHooks/pipelineViewFetch.ts | 13 +- 19 files changed, 410 insertions(+), 262 deletions(-) rename ui/src/images/{generator.svg => generator0.svg} (100%) create mode 100644 ui/src/images/generator1.svg create mode 100644 ui/src/images/generator2.svg create mode 100644 ui/src/images/generator3.svg create mode 100644 ui/src/images/generator4.svg rename ui/src/images/{input.svg => input0.svg} (100%) create mode 100644 ui/src/images/input1.svg create mode 100644 ui/src/images/input2.svg create mode 100644 ui/src/images/input3.svg create mode 100644 ui/src/images/input4.svg diff --git a/ui/src/App.tsx b/ui/src/App.tsx index f3211bf7e..727df9075 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -308,7 +308,7 @@ function App() { width: "100%", height: "100%", overflow: "auto", - marginTop: EXCLUDE_CRUMBS[location.pathname] ? 0 : "2.75rem", + marginTop: EXCLUDE_CRUMBS[location.pathname] ? 0 : "2.5rem", }} > {routes} diff --git a/ui/src/components/common/SlidingSidebar/partials/GeneratorDetails/index.tsx b/ui/src/components/common/SlidingSidebar/partials/GeneratorDetails/index.tsx index 142bf68f7..c90d3b91f 100644 --- a/ui/src/components/common/SlidingSidebar/partials/GeneratorDetails/index.tsx +++ b/ui/src/components/common/SlidingSidebar/partials/GeneratorDetails/index.tsx @@ -3,7 +3,7 @@ import Tabs from "@mui/material/Tabs"; import Tab from "@mui/material/Tab"; import Box from "@mui/material/Box"; import { GeneratorUpdate } from "./partials/GeneratorUpdate"; -import generatorIcon from "../../../../../images/generator.svg"; +import generatorIcon from "../../../../../images/generator0.svg"; import "./style.css"; diff --git a/ui/src/components/common/SummaryPageLayout/index.tsx b/ui/src/components/common/SummaryPageLayout/index.tsx index 76ca90dea..6edc7354b 100644 --- a/ui/src/components/common/SummaryPageLayout/index.tsx +++ b/ui/src/components/common/SummaryPageLayout/index.tsx @@ -35,6 +35,7 @@ export interface SummarySection { } export interface SummaryPageLayoutProps { + excludeContentMargin?: boolean; collapsable?: boolean; defaultCollapsed?: boolean; offsetOnCollapse?: boolean; // Add top margin to content when collapsed to avoid content overlap @@ -45,6 +46,8 @@ export interface SummaryPageLayoutProps { contentHideOverflow?: boolean; } +export const CollapseContext = React.createContext(false); + const SUMMARY_HEIGHT = "6.5625rem"; const COLLAPSED_HEIGHT = "2.25rem"; @@ -193,6 +196,7 @@ const getSummaryComponent = (summarySections: SummarySection[]) => { }; export function SummaryPageLayout({ + excludeContentMargin = false, collapsable = false, defaultCollapsed = false, offsetOnCollapse = false, @@ -240,9 +244,11 @@ export function SummaryPageLayout({ boxShadow: "0 0.25rem 0.375rem rgba(39, 76, 119, 0.16)", zIndex: (theme) => theme.zIndex.drawer - 1, position: "fixed", - top: "5.75rem", + top: "6.25rem", padding: "0 1.25rem", alignItems: "center", + borderBottomLeftRadius: "1.25rem", + borderBottomRightRadius: "1.25rem", }} > @@ -301,6 +307,7 @@ export function SummaryPageLayout({ ]); const contentMargin = useMemo(() => { + if (excludeContentMargin) return 0; if (collapsed) { return offsetOnCollapse ? `${summaryHeight}px` : undefined; } @@ -323,7 +330,9 @@ export function SummaryPageLayout({ height: "100%", }} > - {contentComponent} + + {contentComponent} + ); diff --git a/ui/src/components/pages/Pipeline/index.tsx b/ui/src/components/pages/Pipeline/index.tsx index 06de57e49..2da9d525e 100644 --- a/ui/src/components/pages/Pipeline/index.tsx +++ b/ui/src/components/pages/Pipeline/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useContext, useMemo } from "react"; +import { useCallback, useContext, useMemo, createContext } from "react"; import { useParams } from "react-router-dom"; import CircularProgress from "@mui/material/CircularProgress"; import Box from "@mui/material/Box"; @@ -26,6 +26,10 @@ export interface PipelineProps { namespaceId?: string; } +export const GeneratorColorContext = createContext>( + new Map() +); + export function Pipeline({ namespaceId: nsIdProp }: PipelineProps) { const { namespaceId: nsIdParam, pipelineId } = useParams(); const namespaceId = nsIdProp || nsIdParam; @@ -41,6 +45,7 @@ export function Pipeline({ namespaceId: nsIdProp }: PipelineProps) { pipeline, vertices, edges, + generatorToColorIdxMap, pipelineErr, buffersErr, loading, @@ -184,18 +189,21 @@ export function Pipeline({ namespaceId: nsIdProp }: PipelineProps) { ); } return ( - + + + ); }, [ + generatorToColorIdxMap, pipelineErr, buffersErr, loading, @@ -209,6 +217,7 @@ export function Pipeline({ namespaceId: nsIdProp }: PipelineProps) { return ( { const { zoomIn, zoomOut, fitView } = useReactFlow(); const [isLocked, setIsLocked] = useState(false); const [isPanOnScrollLocked, setIsPanOnScrollLocked] = useState(false); + const [isMap, setIsMap] = useState(false); + const [isReduce, setIsReduce] = useState(false); + const [isSideInput, setIsSideInput] = useState(false); + const isCollapsed = useContext(CollapseContext); const { nodes, edges, @@ -146,6 +152,9 @@ const Flow = (props: FlowProps) => { handleNodeClick, handleEdgeClick, handlePaneClick, + refresh, + namespaceId, + data, } = props; const onIsLockedChange = useCallback( @@ -160,6 +169,105 @@ const Flow = (props: FlowProps) => { const onZoomIn = useCallback(() => zoomIn({ duration: 500 }), [zoomLevel]); const onZoomOut = useCallback(() => zoomOut({ duration: 500 }), [zoomLevel]); + const [error, setError] = useState(undefined); + const [successMessage, setSuccessMessage] = useState( + undefined + ); + const [statusPayload, setStatusPayload] = useState(undefined); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [timerDateStamp, setTimerDateStamp] = useState(undefined); + const [timer, setTimer] = useState(undefined); + + const handlePlayClick = useCallback(() => { + handleTimer(); + setStatusPayload({ + spec: { + lifecycle: { + desiredPhase: RUNNING, + }, + }, + }); + }, []); + + const handlePauseClick = useCallback(() => { + handleTimer(); + setStatusPayload({ + spec: { + lifecycle: { + desiredPhase: PAUSED, + }, + }, + }); + }, []); + + const handleTimer = useCallback(() => { + const dateString = new Date().toISOString(); + const time = timeAgo(dateString); + setTimerDateStamp(time); + const pauseTimer = setInterval(() => { + const time = timeAgo(dateString); + setTimerDateStamp(time); + }, 1000); + setTimer(pauseTimer); + }, []); + + useEffect(() => { + const patchStatus = async () => { + try { + const response = await fetch( + `/api/v1/namespaces/${namespaceId}/pipelines/${data?.pipeline?.metadata?.name}`, + { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(statusPayload), + } + ); + const error = await getAPIResponseError(response); + if (error) { + setError(error); + } else { + refresh(); + setSuccessMessage("Status updated successfully"); + } + } catch (e) { + setError(e); + } + }; + if (statusPayload) { + patchStatus(); + } + }, [statusPayload]); + + useEffect(() => { + if ( + statusPayload?.spec?.lifecycle?.desiredPhase === PAUSED && + data?.pipeline?.status?.phase === PAUSED + ) { + clearInterval(timer); + setStatusPayload(undefined); + } + if ( + statusPayload?.spec?.lifecycle?.desiredPhase === RUNNING && + data?.pipeline?.status?.phase === RUNNING + ) { + clearInterval(timer); + setStatusPayload(undefined); + } + }, [data]); + + useEffect(() => { + nodes.forEach((node) => { + if (node?.data?.type === "sideInput") { + setIsSideInput(true); + } + if (node?.data?.type === "udf") { + node.data?.nodeInfo?.udf?.groupBy ? setIsReduce(true) : setIsMap(true); + } + }); + }, [nodes]); + return ( { panOnScroll={isPanOnScrollLocked} maxZoom={2.75} > + + + + + + {error && statusPayload ? ( + + {error} + + ) : successMessage && + statusPayload && + ((statusPayload.spec.lifecycle.desiredPhase === PAUSED && + data?.pipeline?.status?.phase !== PAUSED) || + (statusPayload.spec.lifecycle.desiredPhase === RUNNING && + data?.pipeline?.status?.phase !== RUNNING)) ? ( +
+ {" "} + + + {statusPayload?.spec?.lifecycle?.desiredPhase === PAUSED + ? "Pipeline Pausing..." + : "Pipeline Resuming..."} + + {timerDateStamp} + +
+ ) : ( + "" + )} +
+
+
+ + + - - {"lock-unlock"} - - - {"panOnScrollLock"} - + + + {"lock-unlock"} + + + + + {"panOnScrollLock"} + +
- - {"fullscreen"} - + + + {"fullscreen"} + +
- - zoom-in - - - zoom-out - + + + zoom-in + + + + + zoom-out + + { - + } @@ -235,26 +463,34 @@ const Flow = (props: FlowProps) => { {"source"}
Source
-
- {"map"} -
Map
-
-
- {"reduce"} -
Reduce
-
+ {isMap && ( +
+ {"map"} +
Map
+
+ )} + {isReduce && ( +
+ {"reduce"} +
Reduce
+
+ )}
{"sink"}
Sink
-
- {"input"} -
Input
-
-
- {"generator"} -
Generator
-
+ {isSideInput && ( +
+ {"input"} +
Input
+
+ )} + {isSideInput && ( +
+ {"generator"} +
Generator
+
+ )} @@ -293,14 +529,6 @@ export default function Graph(props: GraphProps) { const [sideEdges, setSideEdges] = useState>(new Map()); const initialHiddenValue = getHiddenValue(layoutedEdges); const [hidden, setHidden] = useState(initialHiddenValue); - const [error, setError] = useState(undefined); - const [successMessage, setSuccessMessage] = useState( - undefined - ); - const [statusPayload, setStatusPayload] = useState(undefined); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [timerDateStamp, setTimerDateStamp] = useState(undefined); - const [timer, setTimer] = useState(undefined); useEffect(() => { const nodeSet = new Map(); @@ -518,191 +746,9 @@ export default function Graph(props: GraphProps) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [showSpec, setShowSpec] = useState(true); - const handlePlayClick = useCallback(() => { - handleTimer(); - setStatusPayload({ - spec: { - lifecycle: { - desiredPhase: RUNNING, - }, - }, - }); - }, []); - - const handlePauseClick = useCallback(() => { - handleTimer(); - setStatusPayload({ - spec: { - lifecycle: { - desiredPhase: PAUSED, - }, - }, - }); - }, []); - - const handleTimer = useCallback(() => { - const dateString = new Date().toISOString(); - const time = timeAgo(dateString); - setTimerDateStamp(time); - const pauseTimer = setInterval(() => { - const time = timeAgo(dateString); - setTimerDateStamp(time); - }, 1000); - setTimer(pauseTimer); - }, []); - - useEffect(() => { - const patchStatus = async () => { - try { - const response = await fetch( - `/api/v1/namespaces/${namespaceId}/pipelines/${data?.pipeline?.metadata?.name}`, - { - method: "PATCH", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(statusPayload), - } - ); - const error = await getAPIResponseError(response); - if (error) { - setError(error); - } else { - refresh(); - setSuccessMessage("Status updated successfully"); - } - } catch (e) { - setError(e); - } - }; - if (statusPayload) { - patchStatus(); - } - }, [statusPayload]); - - useEffect(() => { - if ( - statusPayload?.spec?.lifecycle?.desiredPhase === PAUSED && - data?.pipeline?.status?.phase === PAUSED - ) { - clearInterval(timer); - setStatusPayload(undefined); - } - if ( - statusPayload?.spec?.lifecycle?.desiredPhase === RUNNING && - data?.pipeline?.status?.phase === RUNNING - ) { - clearInterval(timer); - setStatusPayload(undefined); - } - }, [data]); - return ( -
+
- - - - - - - - - - diff --git a/ui/src/components/pages/Pipeline/partials/Graph/partials/CustomNode/index.tsx b/ui/src/components/pages/Pipeline/partials/Graph/partials/CustomNode/index.tsx index fd84404e1..75dff1617 100644 --- a/ui/src/components/pages/Pipeline/partials/Graph/partials/CustomNode/index.tsx +++ b/ui/src/components/pages/Pipeline/partials/Graph/partials/CustomNode/index.tsx @@ -2,14 +2,23 @@ import { FC, memo, useCallback, useContext, useMemo } from "react"; import { Tooltip } from "@mui/material"; import { Handle, NodeProps, Position } from "reactflow"; import { HighlightContext } from "../../index"; +import { GeneratorColorContext } from "../../../../index"; import { HighlightContextProps } from "../../../../../../../types/declarations/graph"; // import healthy from "../../../../../../../images/heart-fill.svg"; import source from "../../../../../../../images/source.png"; import map from "../../../../../../../images/map.png"; import reduce from "../../../../../../../images/reduce.png"; import sink from "../../../../../../../images/sink.png"; -import input from "../../../../../../../images/input.svg"; -import generator from "../../../../../../../images/generator.svg"; +import input0 from "../../../../../../../images/input0.svg"; +import input1 from "../../../../../../../images/input1.svg"; +import input2 from "../../../../../../../images/input2.svg"; +import input3 from "../../../../../../../images/input3.svg"; +import input4 from "../../../../../../../images/input4.svg"; +import generator0 from "../../../../../../../images/generator0.svg"; +import generator1 from "../../../../../../../images/generator1.svg"; +import generator2 from "../../../../../../../images/generator2.svg"; +import generator3 from "../../../../../../../images/generator3.svg"; +import generator4 from "../../../../../../../images/generator4.svg"; import "reactflow/dist/style.css"; import "./style.css"; @@ -19,11 +28,33 @@ const getBorderColor = (nodeType: string) => { ? "#3874CB" : nodeType === "udf" ? "#009EAC" - : nodeType === "sideInput" - ? "#C9007A" : "#577477"; }; +const inputImage = { + 0: input0, + 1: input1, + 2: input2, + 3: input3, + 4: input4, +}; + +const generatorImage = { + 0: generator0, + 1: generator1, + 2: generator2, + 3: generator3, + 4: generator4, +}; + +const inputColor = { + 0: "#C9007A", + 1: "#73A8AE", + 2: "#8D9096", + 3: "#B61A37", + 4: "#7C00F6", +}; + const isSelected = (selected: boolean) => { return selected ? "0.1875rem solid" : "0.0625rem solid"; }; @@ -45,6 +76,17 @@ const CustomNode: FC = ({ sideInputEdges, } = useContext(HighlightContext); + const generatorToColorMap: Map = useContext( + GeneratorColorContext + ); + + const getSideInputColor = useCallback( + (nodeName: string) => { + return inputColor[generatorToColorMap.get(nodeName)]; + }, + [generatorToColorMap] + ); + const handleClick = useCallback( (e) => { const updatedNodeHighlightValues = {}; @@ -98,7 +140,7 @@ const CustomNode: FC = ({ return { border: `${isSelected( highlightValues[data?.name] && highlightValues[text] - )} ${getBorderColor(data?.type)}`, + )} ${getSideInputColor(data?.name)}`, }; }; @@ -119,7 +161,7 @@ const CustomNode: FC = ({ }} > {"generator"} = ({
= ({ id={`3-${idx}`} position={Position.Bottom} style={{ - left: `${50 - idx * 10}%`, + left: `${50 - idx * 9}%`, }} /> ); })}
- {data?.nodeInfo?.sideInputs?.map((_, idx) => { + {data?.nodeInfo?.sideInputs?.map((input, idx) => { return ( {"input"} + + \ No newline at end of file diff --git a/ui/src/images/generator2.svg b/ui/src/images/generator2.svg new file mode 100644 index 000000000..97ff0d03d --- /dev/null +++ b/ui/src/images/generator2.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ui/src/images/generator3.svg b/ui/src/images/generator3.svg new file mode 100644 index 000000000..b1e5e5350 --- /dev/null +++ b/ui/src/images/generator3.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ui/src/images/generator4.svg b/ui/src/images/generator4.svg new file mode 100644 index 000000000..097d91f65 --- /dev/null +++ b/ui/src/images/generator4.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ui/src/images/input.svg b/ui/src/images/input0.svg similarity index 100% rename from ui/src/images/input.svg rename to ui/src/images/input0.svg diff --git a/ui/src/images/input1.svg b/ui/src/images/input1.svg new file mode 100644 index 000000000..de327243a --- /dev/null +++ b/ui/src/images/input1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/src/images/input2.svg b/ui/src/images/input2.svg new file mode 100644 index 000000000..257cfe733 --- /dev/null +++ b/ui/src/images/input2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/src/images/input3.svg b/ui/src/images/input3.svg new file mode 100644 index 000000000..9a1189a9f --- /dev/null +++ b/ui/src/images/input3.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/src/images/input4.svg b/ui/src/images/input4.svg new file mode 100644 index 000000000..5f5f8cf1d --- /dev/null +++ b/ui/src/images/input4.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/src/types/declarations/graph.d.ts b/ui/src/types/declarations/graph.d.ts index cb7d27999..e27545252 100644 --- a/ui/src/types/declarations/graph.d.ts +++ b/ui/src/types/declarations/graph.d.ts @@ -31,6 +31,9 @@ export interface FlowProps { handleNodeClick: (e: Element | EventType, node: Node) => void; handleEdgeClick: (e: Element | EventType, edge: Edge) => void; handlePaneClick: () => void; + refresh: () => void; + namespaceId: string | undefined; + data: any; } export interface HighlightContextProps { diff --git a/ui/src/utils/fetcherHooks/pipelineViewFetch.ts b/ui/src/utils/fetcherHooks/pipelineViewFetch.ts index d6a827ca6..bcc990406 100644 --- a/ui/src/utils/fetcherHooks/pipelineViewFetch.ts +++ b/ui/src/utils/fetcherHooks/pipelineViewFetch.ts @@ -38,6 +38,9 @@ export const usePipelineViewFetch = ( const [nodeOutDegree, setNodeOutDegree] = useState>( new Map() ); + const [generatorToColorIdxMap, setGeneratorToColorIdxMap] = useState< + Map + >(new Map()); const [pipelineErr, setPipelineErr] = useState(undefined); const [buffersErr, setBuffersErr] = useState(undefined); const [loading, setLoading] = useState(true); @@ -83,7 +86,9 @@ export const usePipelineViewFetch = ( if (data.errMsg) { setPipelineErr(`Error: ${data.errMsg}`); } else { - setPipelineErr(`Error: user is not authorized to execute the requested action.`); + setPipelineErr( + `Error: user is not authorized to execute the requested action.` + ); } } else { setPipelineErr(`Response code: ${response.status}`); @@ -489,8 +494,9 @@ export const usePipelineViewFetch = ( }); } //creating side input nodes + const generatorToColorIdx = new Map(); if (spec?.sideInputs) { - spec.sideInputs.forEach((sideInput) => { + spec.sideInputs.forEach((sideInput, idx) => { const newNode = {} as Node; newNode.id = sideInput?.name; newNode.data = { name: sideInput?.name }; @@ -501,7 +507,9 @@ export const usePipelineViewFetch = ( newNode.data.type = "sideInput"; newNode.data.sideHandle = true; newVertices.push(newNode); + generatorToColorIdx.set(sideInput?.name, `${idx % 5}`); }); + setGeneratorToColorIdxMap(generatorToColorIdx); } return newVertices; }, [ @@ -659,6 +667,7 @@ export const usePipelineViewFetch = ( pipeline, vertices, edges, + generatorToColorIdxMap, pipelineErr, buffersErr, loading,