From 2a83b847e1a6b93a8d18a16cd5adc2fb6422b078 Mon Sep 17 00:00:00 2001 From: Ben Elferink Date: Tue, 29 Oct 2024 11:12:01 +0200 Subject: [PATCH] [GEN-1591]: make "add action" in overview functional (#1650) --- .../main/overview/add-entity/index.tsx | 74 ++++------- .../hooks/overview/useNodeDataFlowHandlers.ts | 19 +-- .../nodes-data-flow/builder.ts | 121 ++++++++++-------- .../nodes-data-flow/index.tsx | 20 +-- .../{add-action-node.tsx => add-node.tsx} | 55 ++++---- frontend/webapp/store/index.ts | 4 +- frontend/webapp/store/useModalStore.tsx | 11 ++ frontend/webapp/types/common.ts | 8 +- 8 files changed, 159 insertions(+), 153 deletions(-) rename frontend/webapp/reuseable-components/nodes-data-flow/nodes/{add-action-node.tsx => add-node.tsx} (55%) create mode 100644 frontend/webapp/store/useModalStore.tsx diff --git a/frontend/webapp/containers/main/overview/add-entity/index.tsx b/frontend/webapp/containers/main/overview/add-entity/index.tsx index f158c2a93..2743a05ba 100644 --- a/frontend/webapp/containers/main/overview/add-entity/index.tsx +++ b/frontend/webapp/containers/main/overview/add-entity/index.tsx @@ -1,6 +1,7 @@ import React, { useState, useRef, useCallback } from 'react'; import Image from 'next/image'; import theme from '@/styles/theme'; +import { useModalStore } from '@/store'; import { AddActionModal } from '../../actions'; import styled, { css } from 'styled-components'; import { useActualSources, useOnClickOutside } from '@/hooks'; @@ -61,9 +62,9 @@ const ButtonText = styled(Text)` // Default options for the dropdown const DEFAULT_OPTIONS: DropdownOption[] = [ - { id: 'source', value: 'Source' }, - { id: 'action', value: 'Action' }, - { id: 'destination', value: 'Destination' }, + { id: OVERVIEW_ENTITY_TYPES.SOURCE, value: 'Source' }, + { id: OVERVIEW_ENTITY_TYPES.ACTION, value: 'Action' }, + { id: OVERVIEW_ENTITY_TYPES.DESTINATION, value: 'Destination' }, ]; interface AddEntityButtonDropdownProps { @@ -71,80 +72,53 @@ interface AddEntityButtonDropdownProps { placeholder?: string; } -const AddEntityButtonDropdown: React.FC = ({ - options = DEFAULT_OPTIONS, - placeholder = 'ADD...', -}) => { - const [isOpen, setIsOpen] = useState(false); +const AddEntityButtonDropdown: React.FC = ({ options = DEFAULT_OPTIONS, placeholder = 'ADD...' }) => { + const { currentModal, setCurrentModal } = useModalStore(); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); const dropdownRef = useRef(null); - const [currentModal, setCurrentModal] = useState(''); - const { isPolling, createSourcesForNamespace, persistNamespaceItems } = - useActualSources(); + const { isPolling, createSourcesForNamespace, persistNamespaceItems } = useActualSources(); - useOnClickOutside(dropdownRef, () => setIsOpen(false)); + useOnClickOutside(dropdownRef, () => setIsDropdownOpen(false)); - const handleToggle = useCallback(() => { - setIsOpen((prev) => !prev); - }, []); + const handleToggle = () => { + setIsDropdownOpen((prev) => !prev); + }; - const handleSelect = useCallback((option: DropdownOption) => { + const handleSelect = (option: DropdownOption) => { setCurrentModal(option.id); - setIsOpen(false); - }, []); + setIsDropdownOpen(false); + }; - const handleCloseModal = useCallback(() => { + const handleCloseModal = () => { setCurrentModal(''); - }, []); + }; return ( - {isPolling ? ( - - ) : ( - Add - )} + {isPolling ? : Add} {placeholder} - {isOpen && ( + {isDropdownOpen && ( {options.map((option) => ( - handleSelect(option)} - > - {`Add + handleSelect(option)}> + {`Add {option.value} ))} )} + - - + + ); }; diff --git a/frontend/webapp/hooks/overview/useNodeDataFlowHandlers.ts b/frontend/webapp/hooks/overview/useNodeDataFlowHandlers.ts index d6216d013..c5ffcc312 100644 --- a/frontend/webapp/hooks/overview/useNodeDataFlowHandlers.ts +++ b/frontend/webapp/hooks/overview/useNodeDataFlowHandlers.ts @@ -1,7 +1,7 @@ // src/hooks/useNodeDataFlowHandlers.ts import { useCallback } from 'react'; -import { useDrawerStore } from '@/store'; -import { K8sActualSource, ActualDestination, ActionDataParsed, OVERVIEW_ENTITY_TYPES } from '@/types'; +import { useDrawerStore, useModalStore } from '@/store'; +import { K8sActualSource, ActualDestination, ActionDataParsed, OVERVIEW_ENTITY_TYPES, OVERVIEW_NODE_TYPES } from '@/types'; export function useNodeDataFlowHandlers({ sources, @@ -12,6 +12,7 @@ export function useNodeDataFlowHandlers({ actions: ActionDataParsed[]; destinations: ActualDestination[]; }) { + const { setCurrentModal } = useModalStore(); const setSelectedItem = useDrawerStore(({ setSelectedItem }) => setSelectedItem); const handleNodeClick = useCallback( @@ -31,9 +32,7 @@ export function useNodeDataFlowHandlers({ type, item: selectedDrawerItem, }); - } - - if (type === OVERVIEW_ENTITY_TYPES.ACTION) { + } else if (type === OVERVIEW_ENTITY_TYPES.ACTION) { const selectedDrawerItem = actions.find((action) => action.id === id); if (!selectedDrawerItem) return; @@ -42,9 +41,7 @@ export function useNodeDataFlowHandlers({ type, item: selectedDrawerItem, }); - } - - if (type === OVERVIEW_ENTITY_TYPES.DESTINATION) { + } else if (type === OVERVIEW_ENTITY_TYPES.DESTINATION) { const selectedDrawerItem = destinations.find((destination) => destination.id === id); if (!selectedDrawerItem) return; @@ -53,6 +50,12 @@ export function useNodeDataFlowHandlers({ type, item: selectedDrawerItem, }); + } else if (type === OVERVIEW_NODE_TYPES.ADD_SOURCE) { + setCurrentModal(OVERVIEW_ENTITY_TYPES.SOURCE); + } else if (type === OVERVIEW_NODE_TYPES.ADD_ACTION) { + setCurrentModal(OVERVIEW_ENTITY_TYPES.ACTION); + } else if (type === OVERVIEW_NODE_TYPES.ADD_DESTIONATION) { + setCurrentModal(OVERVIEW_ENTITY_TYPES.DESTINATION); } }, [sources, actions, destinations, setSelectedItem] diff --git a/frontend/webapp/reuseable-components/nodes-data-flow/builder.ts b/frontend/webapp/reuseable-components/nodes-data-flow/builder.ts index c37402216..52391bc01 100644 --- a/frontend/webapp/reuseable-components/nodes-data-flow/builder.ts +++ b/frontend/webapp/reuseable-components/nodes-data-flow/builder.ts @@ -2,7 +2,7 @@ import theme from '@/styles/theme'; import { getActionIcon } from '@/utils'; import { Node, Edge } from 'react-flow-renderer'; import { getMainContainerLanguageLogo } from '@/utils/constants/programming-languages'; -import type { ActionData, ActionItem, ActualDestination, K8sActualSource } from '@/types'; +import { OVERVIEW_ENTITY_TYPES, OVERVIEW_NODE_TYPES, type ActionData, type ActionItem, type ActualDestination, type K8sActualSource } from '@/types'; // Constants const NODE_HEIGHT = 80; @@ -56,20 +56,30 @@ export const buildNodesAndEdges = ({ title: 'Sources', tagValue: sources.length, }), - ...sources.map((source, index) => - createNode(`source-${index}`, 'base', leftColumnX, NODE_HEIGHT * (index + 1), { - type: 'source', - title: source.name + (source.reportedName ? ` (${source.reportedName})` : ''), - subTitle: source.kind, - imageUri: getMainContainerLanguageLogo(source), - status: 'healthy', - id: { - kind: source.kind, - name: source.name, - namespace: source.namespace, - }, - }) - ), + ...(!sources.length + ? [ + createNode(`source-0`, 'add', leftColumnX, NODE_HEIGHT, { + type: OVERVIEW_NODE_TYPES.ADD_SOURCE, + title: 'ADD SOURCE', + subTitle: 'Add first source to collect OpenTelemetry data', + imageUri: '', + status: 'healthy', + }), + ] + : sources.map((source, index) => + createNode(`source-${index}`, 'base', leftColumnX, NODE_HEIGHT * (index + 1), { + type: OVERVIEW_ENTITY_TYPES.SOURCE, + title: source.name + (source.reportedName ? ` (${source.reportedName})` : ''), + subTitle: source.kind, + imageUri: getMainContainerLanguageLogo(source), + status: 'healthy', + id: { + kind: source.kind, + name: source.name, + namespace: source.namespace, + }, + }) + )), ]; // Build Destination Nodes @@ -79,17 +89,27 @@ export const buildNodesAndEdges = ({ title: 'Destinations', tagValue: destinations.length, }), - ...destinations.map((destination, index) => - createNode(`destination-${index}`, 'base', rightColumnX, NODE_HEIGHT * (index + 1), { - type: 'destination', - title: destination.name, - subTitle: destination.destinationType.displayName, - imageUri: destination.destinationType.imageUrl, - status: 'healthy', - monitors: extractMonitors(destination.exportedSignals), - id: destination.id, - }) - ), + ...(!destinations.length + ? [ + createNode(`destination-0`, 'add', rightColumnX, NODE_HEIGHT, { + type: OVERVIEW_NODE_TYPES.ADD_DESTIONATION, + title: 'ADD DESTIONATION', + subTitle: 'Add first destination to monitor OpenTelemetry data', + imageUri: '', + status: 'healthy', + }), + ] + : destinations.map((destination, index) => + createNode(`destination-${index}`, 'base', rightColumnX, NODE_HEIGHT * (index + 1), { + type: OVERVIEW_ENTITY_TYPES.DESTINATION, + title: destination.name, + subTitle: destination.destinationType.displayName, + imageUri: destination.destinationType.imageUrl, + status: 'healthy', + monitors: extractMonitors(destination.exportedSignals), + id: destination.id, + }) + )), ]; // Build Action Nodes @@ -99,34 +119,31 @@ export const buildNodesAndEdges = ({ title: 'Actions', tagValue: actions.length, }), - ...actions.map((action, index) => { - const actionSpec: ActionItem = typeof action.spec === 'string' ? JSON.parse(action.spec) : (action.spec as ActionItem); - - return createNode(`action-${index}`, 'base', centerColumnX, NODE_HEIGHT * (index + 1), { - type: 'action', - title: actionSpec.actionName, - subTitle: action.type, - imageUri: getActionIcon(action.type), - monitors: actionSpec.signals, - status: 'healthy', - id: action.id, - }); - }), + ...(!actions.length + ? [ + createNode(`action-0`, 'add', centerColumnX, NODE_HEIGHT, { + type: OVERVIEW_NODE_TYPES.ADD_ACTION, + title: 'ADD ACTION', + subTitle: 'Add first action to modify the OpenTelemetry data', + imageUri: '', + status: 'healthy', + }), + ] + : actions.map((action, index) => { + const actionSpec: ActionItem = typeof action.spec === 'string' ? JSON.parse(action.spec) : (action.spec as ActionItem); + + return createNode(`action-${index}`, 'base', centerColumnX, NODE_HEIGHT * (index + 1), { + type: OVERVIEW_ENTITY_TYPES.ACTION, + title: actionSpec.actionName, + subTitle: action.type, + imageUri: getActionIcon(action.type), + monitors: actionSpec.signals, + status: 'healthy', + id: action.id, + }); + })), ]; - if (actionsNode.length === 1) { - actionsNode.push( - createNode(`action-0`, 'addAction', centerColumnX, NODE_HEIGHT * (actions.length + 1), { - type: 'addAction', - title: 'ADD ACTION', - subTitle: '', - imageUri: getActionIcon(), - status: 'healthy', - onClick: () => console.log('Add Action'), - }) - ); - } - // Combine all nodes const nodes = [...sourcesNode, ...destinationNode, ...actionsNode]; diff --git a/frontend/webapp/reuseable-components/nodes-data-flow/index.tsx b/frontend/webapp/reuseable-components/nodes-data-flow/index.tsx index 53e684fd9..fe339032f 100644 --- a/frontend/webapp/reuseable-components/nodes-data-flow/index.tsx +++ b/frontend/webapp/reuseable-components/nodes-data-flow/index.tsx @@ -4,12 +4,12 @@ import '@xyflow/react/dist/style.css'; import BaseNode from './nodes/base-node'; import { ReactFlow } from '@xyflow/react'; import headerNode from './nodes/header-node'; -import AddActionNode from './nodes/add-action-node'; +import AddNode from './nodes/add-node'; const nodeTypes = { - base: BaseNode, header: headerNode, - addAction: AddActionNode, + add: AddNode, + base: BaseNode, }; interface NodeBaseDataFlowProps { @@ -18,20 +18,10 @@ interface NodeBaseDataFlowProps { onNodeClick?: (event: React.MouseEvent, object: any) => void; } -export function NodeBaseDataFlow({ - nodes, - edges, - onNodeClick, -}: NodeBaseDataFlowProps) { +export function NodeBaseDataFlow({ nodes, edges, onNodeClick }: NodeBaseDataFlowProps) { return (
- +
); } diff --git a/frontend/webapp/reuseable-components/nodes-data-flow/nodes/add-action-node.tsx b/frontend/webapp/reuseable-components/nodes-data-flow/nodes/add-node.tsx similarity index 55% rename from frontend/webapp/reuseable-components/nodes-data-flow/nodes/add-action-node.tsx rename to frontend/webapp/reuseable-components/nodes-data-flow/nodes/add-node.tsx index a9bddaf50..b09d5fc55 100644 --- a/frontend/webapp/reuseable-components/nodes-data-flow/nodes/add-action-node.tsx +++ b/frontend/webapp/reuseable-components/nodes-data-flow/nodes/add-node.tsx @@ -1,5 +1,5 @@ +import React from 'react'; import Image from 'next/image'; -import React, { memo } from 'react'; import styled from 'styled-components'; import { Text } from '@/reuseable-components'; import { Handle, Position } from '@xyflow/react'; @@ -42,36 +42,41 @@ const SubTitle = styled(Text)` `; interface BaseNodeProps { + data: Record; + + id: string; + parentId?: any; + type: string; + isConnectable: boolean; + selectable: boolean; + selected?: any; + deletable: boolean; + draggable: boolean; + dragging: boolean; + dragHandle?: any; + + width: number; + height: number; + zIndex: number; + positionAbsoluteX: number; + positionAbsoluteY: number; + sourcePosition?: any; + targetPosition?: any; } -export default memo(({ isConnectable }: BaseNodeProps) => { +const AddNode = ({ id, isConnectable, data }: BaseNodeProps) => { return ( - plus - {'ADD ACTION'} + plus + {data.title} - {'Add first action to modify the OpenTelemetry data'} - - + {data.subTitle} + + ); -}); +}; + +export default AddNode; diff --git a/frontend/webapp/store/index.ts b/frontend/webapp/store/index.ts index aa72ad258..f116f1d8a 100644 --- a/frontend/webapp/store/index.ts +++ b/frontend/webapp/store/index.ts @@ -7,8 +7,7 @@ const rootReducer = combineReducers({ export const store = configureStore({ reducer: rootReducer, - middleware: (getDefaultMiddleware) => - getDefaultMiddleware({ serializableCheck: false }), + middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: false }), }); export type RootState = ReturnType; @@ -17,3 +16,4 @@ export type AppDispatch = typeof store.dispatch; export * from './slices'; export * from './useAppStore'; export * from './useDrawerStore'; +export * from './useModalStore'; diff --git a/frontend/webapp/store/useModalStore.tsx b/frontend/webapp/store/useModalStore.tsx new file mode 100644 index 000000000..161308c49 --- /dev/null +++ b/frontend/webapp/store/useModalStore.tsx @@ -0,0 +1,11 @@ +import { create } from 'zustand'; + +interface ModalStoreState { + currentModal: string; + setCurrentModal: (str: string) => void; +} + +export const useModalStore = create((set) => ({ + currentModal: '', + setCurrentModal: (str) => set({ currentModal: str }), +})); diff --git a/frontend/webapp/types/common.ts b/frontend/webapp/types/common.ts index c90203c9e..b6b6b553c 100644 --- a/frontend/webapp/types/common.ts +++ b/frontend/webapp/types/common.ts @@ -38,6 +38,12 @@ export interface StepProps { export enum OVERVIEW_ENTITY_TYPES { SOURCE = 'source', - DESTINATION = 'destination', ACTION = 'action', + DESTINATION = 'destination', +} + +export enum OVERVIEW_NODE_TYPES { + ADD_SOURCE = 'addSource', + ADD_ACTION = 'addAction', + ADD_DESTIONATION = 'addDestination', }