From 6a91aba00582c7c3cb8910b297bf0e5ea124ad33 Mon Sep 17 00:00:00 2001 From: Ben Elferink Date: Wed, 30 Oct 2024 17:15:04 +0200 Subject: [PATCH] [GEN-1603]: add rules column to overview (#1661) --- .../choose-action-modal/action-options.ts | 3 + .../actions/choose-action-modal/index.tsx | 9 +- .../choose-rule/index.tsx | 30 +--- .../main/overview/add-entity/index.tsx | 10 +- .../overview/overview-data-flow/index.tsx | 12 +- .../main/rules/add-rule-modal/index.tsx | 104 ++++++++++++ .../main/rules/add-rule-modal/rule-options.ts | 25 +++ .../webapp/containers/main/rules/index.ts | 1 + .../hooks/overview/useNodeDataFlowHandlers.ts | 4 + .../webapp/public/icons/overview/rules.svg | 4 + .../public/icons/rules/payloadcollection.svg | 3 + .../auto-complete-input/index.tsx | 40 +++-- .../nodes-data-flow/builder.ts | 153 +++++++++++------- .../nodes-data-flow/index.tsx | 24 +-- .../nodes-data-flow/nodes/add-node.tsx | 13 +- .../nodes-data-flow/nodes/base-node.tsx | 23 ++- .../nodes-data-flow/nodes/header-node.tsx | 19 ++- .../notification-note/index.tsx | 55 ++----- frontend/webapp/types/common.ts | 2 + .../webapp/types/instrumentation-rules.ts | 2 +- .../webapp/utils/functions/get-action-icon.ts | 12 +- .../webapp/utils/functions/get-rule-icon.ts | 9 ++ frontend/webapp/utils/functions/index.ts | 1 + 23 files changed, 375 insertions(+), 183 deletions(-) create mode 100644 frontend/webapp/containers/main/rules/add-rule-modal/index.tsx create mode 100644 frontend/webapp/containers/main/rules/add-rule-modal/rule-options.ts create mode 100644 frontend/webapp/containers/main/rules/index.ts create mode 100644 frontend/webapp/public/icons/overview/rules.svg create mode 100644 frontend/webapp/public/icons/rules/payloadcollection.svg create mode 100644 frontend/webapp/utils/functions/get-rule-icon.ts diff --git a/frontend/webapp/containers/main/actions/choose-action-modal/action-options.ts b/frontend/webapp/containers/main/actions/choose-action-modal/action-options.ts index c2c16a9b8..50de8bdb3 100644 --- a/frontend/webapp/containers/main/actions/choose-action-modal/action-options.ts +++ b/frontend/webapp/containers/main/actions/choose-action-modal/action-options.ts @@ -66,6 +66,7 @@ export const ACTION_OPTIONS: ActionOption[] = [ label: 'Error Sampler', description: 'Sample errors based on percentage.', type: ActionsType.ERROR_SAMPLER, + icon: getActionIcon('sampler'), docsEndpoint: '/pipeline/actions/sampling/errorsampler', docsDescription: 'The “Error Sampler” Odigos Action is a Global Action that supports error sampling by filtering out non-error traces.', allowedSignals: ['TRACES'], @@ -75,6 +76,7 @@ export const ACTION_OPTIONS: ActionOption[] = [ label: 'Probabilistic Sampler', description: 'Sample traces based on percentage.', type: ActionsType.PROBABILISTIC_SAMPLER, + icon: getActionIcon('sampler'), docsEndpoint: '/pipeline/actions/sampling/probabilisticsampler', docsDescription: 'The “Probabilistic Sampler” Odigos Action supports probabilistic sampling based on a configured sampling percentage applied to the TraceID.', @@ -85,6 +87,7 @@ export const ACTION_OPTIONS: ActionOption[] = [ label: 'Latency Action', description: 'Add latency to your traces.', type: ActionsType.LATENCY_SAMPLER, + icon: getActionIcon('sampler'), docsEndpoint: '/pipeline/actions/sampling/latencysampler', docsDescription: 'The “Latency Sampler” Odigos Action is an Endpoint Action that samples traces based on their duration for a specific service and endpoint (HTTP route) filter.', diff --git a/frontend/webapp/containers/main/actions/choose-action-modal/index.tsx b/frontend/webapp/containers/main/actions/choose-action-modal/index.tsx index 8bbaabc05..43033bdca 100644 --- a/frontend/webapp/containers/main/actions/choose-action-modal/index.tsx +++ b/frontend/webapp/containers/main/actions/choose-action-modal/index.tsx @@ -10,7 +10,7 @@ const Container = styled.section` max-width: 640px; height: 640px; margin: 0 15vw; - padding-top: 64px; + padding: 64px 12px 0 12px; display: flex; flex-direction: column; overflow-y: scroll; @@ -18,6 +18,7 @@ const Container = styled.section` const Center = styled.div` width: 100%; + margin-top: 24px; display: flex; flex-direction: column; align-items: center; @@ -32,7 +33,7 @@ interface AddActionModalProps { export const AddActionModal: React.FC = ({ isModalOpen, handleCloseModal }) => { const { formData, handleFormChange, resetFormData, validateForm } = useActionFormData(); const { createAction, loading } = useActionCRUD({ onSuccess: handleClose }); - const [selectedItem, setSelectedItem] = useState(null); + const [selectedItem, setSelectedItem] = useState(undefined); const isFormOk = useMemo(() => !!selectedItem && validateForm(), [selectedItem, formData]); @@ -42,7 +43,7 @@ export const AddActionModal: React.FC = ({ isModalOpen, han function handleClose() { resetFormData(); - setSelectedItem(null); + setSelectedItem(undefined); handleCloseModal(); } @@ -75,7 +76,7 @@ export const AddActionModal: React.FC = ({ isModalOpen, han title='Define Action' description='Actions are a way to modify the OpenTelemetry data recorded by Odigos sources before it is exported to your Odigos destinations. Choose an action type and provide necessary information.' /> - + {!!selectedItem?.type ? (
diff --git a/frontend/webapp/containers/main/instrumentation-rules/choose-rule/index.tsx b/frontend/webapp/containers/main/instrumentation-rules/choose-rule/index.tsx index f66661a29..0bd04be25 100644 --- a/frontend/webapp/containers/main/instrumentation-rules/choose-rule/index.tsx +++ b/frontend/webapp/containers/main/instrumentation-rules/choose-rule/index.tsx @@ -2,22 +2,17 @@ import React from 'react'; import { useRouter } from 'next/navigation'; import { NewActionCard } from '@/components'; import { KeyvalLink, KeyvalText } from '@/design.system'; -import { ActionItemCard, InstrumentationRuleType } from '@/types'; +import { ActionItemCard, RulesType } from '@/types'; import { ACTION, INSTRUMENTATION_RULES_DOCS_LINK, OVERVIEW } from '@/utils'; -import { - ActionCardWrapper, - ActionsListWrapper, - DescriptionWrapper, - LinkWrapper, -} from './styled'; +import { ActionCardWrapper, ActionsListWrapper, DescriptionWrapper, LinkWrapper } from './styled'; const ITEMS = [ { id: 'payload-collection', title: 'Payload Collection', description: 'Record operation payloads as span attributes where supported.', - type: InstrumentationRuleType.PAYLOAD_COLLECTION, - icon: InstrumentationRuleType.PAYLOAD_COLLECTION, + type: RulesType.PAYLOAD_COLLECTION, + icon: RulesType.PAYLOAD_COLLECTION, }, ]; @@ -31,10 +26,7 @@ export function ChooseInstrumentationRuleContainer(): React.JSX.Element { function renderActionsList() { return ITEMS.map((item) => { return ( - + ); @@ -44,17 +36,9 @@ export function ChooseInstrumentationRuleContainer(): React.JSX.Element { return ( <> - - {OVERVIEW.INSTRUMENTATION_RULE_DESCRIPTION} - + {OVERVIEW.INSTRUMENTATION_RULE_DESCRIPTION} - - window.open(INSTRUMENTATION_RULES_DOCS_LINK, '_blank') - } - /> + window.open(INSTRUMENTATION_RULES_DOCS_LINK, '_blank')} /> {renderActionsList()} diff --git a/frontend/webapp/containers/main/overview/add-entity/index.tsx b/frontend/webapp/containers/main/overview/add-entity/index.tsx index 2743a05ba..2916be866 100644 --- a/frontend/webapp/containers/main/overview/add-entity/index.tsx +++ b/frontend/webapp/containers/main/overview/add-entity/index.tsx @@ -1,8 +1,9 @@ -import React, { useState, useRef, useCallback } from 'react'; import Image from 'next/image'; import theme from '@/styles/theme'; import { useModalStore } from '@/store'; +import { AddRuleModal } from '../../rules'; import { AddActionModal } from '../../actions'; +import React, { useState, useRef } from 'react'; import styled, { css } from 'styled-components'; import { useActualSources, useOnClickOutside } from '@/hooks'; import { DropdownOption, OVERVIEW_ENTITY_TYPES } from '@/types'; @@ -29,7 +30,7 @@ const DropdownListContainer = styled.div` right: 0; top: 48px; border-radius: 24px; - width: 131px; + width: 200px; overflow-y: auto; background-color: ${({ theme }) => theme.colors.dropdown_bg}; border: 1px solid ${({ theme }) => theme.colors.border}; @@ -62,6 +63,7 @@ const ButtonText = styled(Text)` // Default options for the dropdown const DEFAULT_OPTIONS: DropdownOption[] = [ + { id: OVERVIEW_ENTITY_TYPES.RULE, value: 'Instrumentation Rule' }, { id: OVERVIEW_ENTITY_TYPES.SOURCE, value: 'Source' }, { id: OVERVIEW_ENTITY_TYPES.ACTION, value: 'Action' }, { id: OVERVIEW_ENTITY_TYPES.DESTINATION, value: 'Destination' }, @@ -100,6 +102,7 @@ const AddEntityButtonDropdown: React.FC = ({ optio {isPolling ? : Add} {placeholder} + {isDropdownOpen && ( {options.map((option) => ( @@ -111,14 +114,15 @@ const AddEntityButtonDropdown: React.FC = ({ optio )} + - + ); }; diff --git a/frontend/webapp/containers/main/overview/overview-data-flow/index.tsx b/frontend/webapp/containers/main/overview/overview-data-flow/index.tsx index 116e41f66..1d9217aee 100644 --- a/frontend/webapp/containers/main/overview/overview-data-flow/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-data-flow/index.tsx @@ -21,13 +21,19 @@ export function OverviewDataFlowContainer() { const { sources } = useActualSources(); const { destinations } = useActualDestination(); const { containerRef, containerWidth } = useContainerWidth(); - const { handleNodeClick } = useNodeDataFlowHandlers({ sources, actions, destinations }); + const { handleNodeClick } = useNodeDataFlowHandlers({ + rules: [], + sources, + actions, + destinations, + }); - const columnWidth = 296; + const columnWidth = 255; // Memoized node and edge builder to improve performance const { nodes, edges } = useMemo(() => { return buildNodesAndEdges({ + rules: [], sources, actions, destinations, @@ -40,7 +46,7 @@ export function OverviewDataFlowContainer() { - + ); } diff --git a/frontend/webapp/containers/main/rules/add-rule-modal/index.tsx b/frontend/webapp/containers/main/rules/add-rule-modal/index.tsx new file mode 100644 index 000000000..1cbc4319a --- /dev/null +++ b/frontend/webapp/containers/main/rules/add-rule-modal/index.tsx @@ -0,0 +1,104 @@ +import styled from 'styled-components'; +import React, { useEffect, useState } from 'react'; +import { RULE_OPTIONS, RuleOption } from './rule-options'; +import { AutocompleteInput, Divider, FadeLoader, Modal, NavigationButtons, NotificationNote, SectionTitle } from '@/reuseable-components'; + +const Container = styled.section` + width: 100%; + max-width: 640px; + height: 640px; + margin: 0 15vw; + padding: 64px 12px 0 12px; + display: flex; + flex-direction: column; + overflow-y: scroll; +`; + +const Center = styled.div` + width: 100%; + margin-top: 24px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +`; + +interface Props { + isModalOpen: boolean; + handleCloseModal: () => void; +} + +export const AddRuleModal: React.FC = ({ isModalOpen, handleCloseModal }) => { + // const { formData, handleFormChange, resetFormData, validateForm } = useRuleFormData(); + // const { createRule, loading } = useRuleCRUD({ onSuccess: handleClose }); + const [selectedItem, setSelectedItem] = useState(undefined); + + useEffect(() => { + if (!selectedItem) handleSelect(RULE_OPTIONS[0]); + }, [selectedItem]); + + // const isFormOk = useMemo(() => !!selectedItem && validateForm(), [selectedItem, formData]); + + const handleSubmit = async () => { + // createRule(formData); + }; + + function handleClose() { + // resetFormData(); + setSelectedItem(undefined); + handleCloseModal(); + } + + const handleSelect = (item: RuleOption) => { + // resetFormData(); + // handleFormChange('type', item.type); + setSelectedItem(item); + }; + + return ( + + } + > + + + + + + {!!selectedItem?.type ? ( +
+ + + {/* {loading ? ( */} +
+ +
+ {/* ) : ( + + )} */} +
+ ) : null} +
+
+ ); +}; diff --git a/frontend/webapp/containers/main/rules/add-rule-modal/rule-options.ts b/frontend/webapp/containers/main/rules/add-rule-modal/rule-options.ts new file mode 100644 index 000000000..bea858d5c --- /dev/null +++ b/frontend/webapp/containers/main/rules/add-rule-modal/rule-options.ts @@ -0,0 +1,25 @@ +import { InstrumentationRuleType } from '@/types'; +import { getRuleIcon } from '@/utils/functions'; + +export type RuleOption = { + id: string; + label: string; + type?: InstrumentationRuleType; + icon?: string; + description?: string; + docsEndpoint?: string; + docsDescription?: string; + items?: RuleOption[]; +}; + +export const RULE_OPTIONS: RuleOption[] = [ + { + id: 'payload_collection', + label: 'Payload Collection', + description: 'Collect span attributes containing payload data to traces.', + type: InstrumentationRuleType.PAYLOAD_COLLECTION, + icon: getRuleIcon(InstrumentationRuleType.PAYLOAD_COLLECTION), + docsEndpoint: '/pipeline/rules/payloadcollection', + docsDescription: 'The “Payload Collection” Rule can be used to add span attributes containing payload data to traces.', + }, +]; diff --git a/frontend/webapp/containers/main/rules/index.ts b/frontend/webapp/containers/main/rules/index.ts new file mode 100644 index 000000000..c5c586edb --- /dev/null +++ b/frontend/webapp/containers/main/rules/index.ts @@ -0,0 +1 @@ +export * from './add-rule-modal'; diff --git a/frontend/webapp/hooks/overview/useNodeDataFlowHandlers.ts b/frontend/webapp/hooks/overview/useNodeDataFlowHandlers.ts index c5ffcc312..697a9b906 100644 --- a/frontend/webapp/hooks/overview/useNodeDataFlowHandlers.ts +++ b/frontend/webapp/hooks/overview/useNodeDataFlowHandlers.ts @@ -4,10 +4,12 @@ import { useDrawerStore, useModalStore } from '@/store'; import { K8sActualSource, ActualDestination, ActionDataParsed, OVERVIEW_ENTITY_TYPES, OVERVIEW_NODE_TYPES } from '@/types'; export function useNodeDataFlowHandlers({ + rules, sources, actions, destinations, }: { + rules: any[]; sources: K8sActualSource[]; actions: ActionDataParsed[]; destinations: ActualDestination[]; @@ -50,6 +52,8 @@ export function useNodeDataFlowHandlers({ type, item: selectedDrawerItem, }); + } else if (type === OVERVIEW_NODE_TYPES.ADD_RULE) { + setCurrentModal(OVERVIEW_ENTITY_TYPES.RULE); } else if (type === OVERVIEW_NODE_TYPES.ADD_SOURCE) { setCurrentModal(OVERVIEW_ENTITY_TYPES.SOURCE); } else if (type === OVERVIEW_NODE_TYPES.ADD_ACTION) { diff --git a/frontend/webapp/public/icons/overview/rules.svg b/frontend/webapp/public/icons/overview/rules.svg new file mode 100644 index 000000000..bb97b05e7 --- /dev/null +++ b/frontend/webapp/public/icons/overview/rules.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/webapp/public/icons/rules/payloadcollection.svg b/frontend/webapp/public/icons/rules/payloadcollection.svg new file mode 100644 index 000000000..f7d8a2a5e --- /dev/null +++ b/frontend/webapp/public/icons/rules/payloadcollection.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/webapp/reuseable-components/auto-complete-input/index.tsx b/frontend/webapp/reuseable-components/auto-complete-input/index.tsx index c33616d75..f700cc99b 100644 --- a/frontend/webapp/reuseable-components/auto-complete-input/index.tsx +++ b/frontend/webapp/reuseable-components/auto-complete-input/index.tsx @@ -1,7 +1,7 @@ import Image from 'next/image'; import { Text } from '../text'; import styled from 'styled-components'; -import React, { useState, ChangeEvent, KeyboardEvent, FC } from 'react'; +import React, { useState, ChangeEvent, KeyboardEvent, FC, useEffect } from 'react'; export interface Option { id: string; @@ -14,7 +14,9 @@ export interface Option { interface AutocompleteInputProps { options: Option[]; placeholder?: string; + selectedOption?: Option; onOptionSelect?: (option: Option) => void; + style?: React.CSSProperties; } const filterOptions = (optionsList: Option[], input: string): Option[] => { @@ -31,15 +33,24 @@ const filterOptions = (optionsList: Option[], input: string): Option[] => { }, []); }; -const AutocompleteInput: FC = ({ options, placeholder = 'Type to search...', onOptionSelect }) => { +const AutocompleteInput: FC = ({ placeholder = 'Type to search...', options, selectedOption, onOptionSelect, style }) => { const [query, setQuery] = useState(''); + const [icon, setIcon] = useState(''); const [filteredOptions, setFilteredOptions] = useState(filterOptions(options, '')); const [showOptions, setShowOptions] = useState(false); const [activeIndex, setActiveIndex] = useState(-1); + useEffect(() => { + if (!!selectedOption && !query) { + setQuery(selectedOption.label); + setIcon(selectedOption.icon || ''); + } + }, [selectedOption, query]); + const handleChange = (e: ChangeEvent) => { const input = e.target.value; setQuery(input); + setIcon(''); if (input) { const filtered = filterOptions(options, input); setFilteredOptions(filtered); @@ -50,6 +61,7 @@ const AutocompleteInput: FC = ({ options, placeholder = }; const handleOptionClick = (option: Option) => { + setIcon(option.icon || ''); setQuery(option.label); setShowOptions(false); if (onOptionSelect) { @@ -81,8 +93,9 @@ const AutocompleteInput: FC = ({ options, placeholder = }; return ( - + + {icon && } = ({ options, placeholder = onFocus={() => setShowOptions(true)} /> + {showOptions && ( {filteredOptions.map((option, index) => ( @@ -107,15 +121,16 @@ const AutocompleteInput: FC = ({ options, placeholder = interface OptionItemProps { option: Option; isActive: boolean; + renderIcon?: boolean; onClick: (option: Option) => void; } -const OptionItem: FC = ({ option, isActive, onClick }) => { +const OptionItem: FC = ({ option, isActive, renderIcon = true, onClick }) => { const hasSubItems = !!option.items && option.items.length > 0; return ( (hasSubItems ? null : onClick(option))}> - {option.icon && {option.label}} + {option.icon && renderIcon && } @@ -128,7 +143,7 @@ const OptionItem: FC = ({ option, isActive, onClick }) => { {option.items?.map((subOption) => ( - + ))} @@ -138,13 +153,16 @@ const OptionItem: FC = ({ option, isActive, onClick }) => { ); }; +const Icon = ({ src, alt = '' }: { src: string; alt?: string }) => { + return {alt}; +}; + export { AutocompleteInput }; /** Styled Components */ const AutocompleteContainer = styled.div` position: relative; - margin-top: 24px; `; const InputWrapper = styled.div` @@ -152,8 +170,8 @@ const InputWrapper = styled.div` display: flex; align-items: center; height: 36px; - gap: 12px; - padding: 0 8px; + gap: 8px; + padding-left: 12px; transition: border-color 0.3s; border-radius: 32px; border: 1px solid rgba(249, 249, 249, 0.24); @@ -195,7 +213,7 @@ const OptionsList = styled.ul` max-height: 348px; top: 32px; border-radius: 24px; - width: calc(100% - 16px); + width: calc(100% - 24px); overflow-y: auto; background-color: ${({ theme }) => theme.colors.dropdown_bg}; border: 1px solid ${({ theme }) => theme.colors.border}; @@ -209,7 +227,7 @@ interface OptionItemContainerProps { } const OptionItemContainer = styled.li` - width: 100%; + width: calc(100% - 24px); padding: 8px 12px; cursor: ${({ isList }) => (isList ? 'default' : 'pointer')}; border-radius: 24px; diff --git a/frontend/webapp/reuseable-components/nodes-data-flow/builder.ts b/frontend/webapp/reuseable-components/nodes-data-flow/builder.ts index 095297743..b8c78ce2b 100644 --- a/frontend/webapp/reuseable-components/nodes-data-flow/builder.ts +++ b/frontend/webapp/reuseable-components/nodes-data-flow/builder.ts @@ -1,6 +1,7 @@ import theme from '@/styles/theme'; import { getActionIcon } from '@/utils'; import { Node, Edge } from 'react-flow-renderer'; +import { getRuleIcon } from '@/utils/functions'; import { getMainContainerLanguageLogo } from '@/utils/constants/programming-languages'; import { OVERVIEW_ENTITY_TYPES, @@ -40,12 +41,14 @@ const extractMonitors = (exportedSignals: Record) => Object.keys(exportedSignals).filter((signal) => exportedSignals[signal] === true); export const buildNodesAndEdges = ({ + rules, sources, actions, destinations, columnWidth, containerWidth, }: { + rules: any[]; sources: K8sActualSource[]; actions: ActionData[]; destinations: ActualDestination[]; @@ -53,29 +56,60 @@ export const buildNodesAndEdges = ({ containerWidth: number; }) => { // Calculate x positions for each column - const leftColumnX = 0; - const rightColumnX = containerWidth - columnWidth; - const centerColumnX = (containerWidth - columnWidth) / 2; + const columnPostions = { + rules: 0, + sources: (containerWidth - columnWidth) / 3, + actions: (containerWidth - columnWidth) / 1.5, + destinations: containerWidth - columnWidth, + }; + + // Build Rules Nodes + const ruleNodes: Node[] = [ + createNode('header-rule', 'header', columnPostions['rules'], 0, { + icon: `${HEADER_ICON_PATH}rules.svg`, + title: 'Instrumentation Rules', + tagValue: rules.length, + }), + ...(!rules.length + ? [ + createNode(`rule-0`, 'add', columnPostions['rules'], NODE_HEIGHT, { + type: OVERVIEW_NODE_TYPES.ADD_RULE, + title: 'ADD RULE', + subTitle: 'Add first rule to modify the OpenTelemetry data', + status: STATUSES.HEALTHY, + }), + ] + : rules.map((rule, index) => + createNode(`rule-${index}`, 'base', columnPostions['rules'], NODE_HEIGHT * (index + 1), { + id: rule.id, + type: OVERVIEW_ENTITY_TYPES.RULE, + status: STATUSES.HEALTHY, + title: rule.actionName || rule.type, + subTitle: rule.type, + imageUri: getRuleIcon(rule.type), + isActive: false, + }) + )), + ]; // Build Source Nodes - const sourcesNode: Node[] = [ - createNode('header-source', 'header', leftColumnX, 0, { + const sourceNodes: Node[] = [ + createNode('header-source', 'header', columnPostions['sources'], 0, { icon: `${HEADER_ICON_PATH}sources.svg`, title: 'Sources', tagValue: sources.length, }), ...(!sources.length ? [ - createNode(`source-0`, 'add', leftColumnX, NODE_HEIGHT, { + createNode(`source-0`, 'add', columnPostions['sources'], NODE_HEIGHT, { type: OVERVIEW_NODE_TYPES.ADD_SOURCE, title: 'ADD SOURCE', subTitle: 'Add first source to collect OpenTelemetry data', - imageUri: '', status: STATUSES.HEALTHY, }), ] : sources.map((source, index) => - createNode(`source-${index}`, 'base', leftColumnX, NODE_HEIGHT * (index + 1), { + createNode(`source-${index}`, 'base', columnPostions['sources'], NODE_HEIGHT * (index + 1), { type: OVERVIEW_ENTITY_TYPES.SOURCE, title: source.name + (source.reportedName ? ` (${source.reportedName})` : ''), subTitle: source.kind, @@ -90,57 +124,26 @@ export const buildNodesAndEdges = ({ )), ]; - // Build Destination Nodes - const destinationNode: Node[] = [ - createNode('header-destination', 'header', rightColumnX, 0, { - icon: `${HEADER_ICON_PATH}destinations.svg`, - title: 'Destinations', - tagValue: destinations.length, - }), - ...(!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: STATUSES.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: STATUSES.HEALTHY, - monitors: extractMonitors(destination.exportedSignals), - id: destination.id, - }) - )), - ]; - // Build Action Nodes - const actionsNode: Node[] = [ - createNode('header-action', 'header', centerColumnX, 0, { + const actionNodes: Node[] = [ + createNode('header-action', 'header', columnPostions['actions'], 0, { icon: `${HEADER_ICON_PATH}actions.svg`, title: 'Actions', tagValue: actions.length, }), ...(!actions.length ? [ - createNode(`action-0`, 'add', centerColumnX, NODE_HEIGHT, { + createNode(`action-0`, 'add', columnPostions['actions'], NODE_HEIGHT, { type: OVERVIEW_NODE_TYPES.ADD_ACTION, title: 'ADD ACTION', subTitle: 'Add first action to modify the OpenTelemetry data', - imageUri: '', status: STATUSES.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), { + return createNode(`action-${index}`, 'base', columnPostions['actions'], NODE_HEIGHT * (index + 1), { id: action.id, type: OVERVIEW_ENTITY_TYPES.ACTION, status: STATUSES.HEALTHY, @@ -153,32 +156,60 @@ export const buildNodesAndEdges = ({ })), ]; + // Build Destination Nodes + const destinationNodes: Node[] = [ + createNode('header-destination', 'header', columnPostions['destinations'], 0, { + icon: `${HEADER_ICON_PATH}destinations.svg`, + title: 'Destinations', + tagValue: destinations.length, + }), + ...(!destinations.length + ? [ + createNode(`destination-0`, 'add', columnPostions['destinations'], NODE_HEIGHT, { + type: OVERVIEW_NODE_TYPES.ADD_DESTIONATION, + title: 'ADD DESTIONATION', + subTitle: 'Add first destination to monitor OpenTelemetry data', + status: STATUSES.HEALTHY, + }), + ] + : destinations.map((destination, index) => + createNode(`destination-${index}`, 'base', columnPostions['destinations'], NODE_HEIGHT * (index + 1), { + type: OVERVIEW_ENTITY_TYPES.DESTINATION, + title: destination.name, + subTitle: destination.destinationType.displayName, + imageUri: destination.destinationType.imageUrl, + status: STATUSES.HEALTHY, + monitors: extractMonitors(destination.exportedSignals), + id: destination.id, + }) + )), + ]; + // Combine all nodes - const nodes = [...sourcesNode, ...destinationNode, ...actionsNode]; + const nodes = [...ruleNodes, ...sourceNodes, ...actionNodes, ...destinationNodes]; // Build edges - connecting sources to actions, and actions to destinations const edges: Edge[] = []; // Connect sources to actions - const sourceToActionEdges: Edge[] = sources.map((_, sourceIndex) => { - const actionIndex = actionsNode.length === 2 ? 0 : sourceIndex % actions.length; - return createEdge(`source-${sourceIndex}-to-action-${actionIndex}`, `source-${sourceIndex}`, `action-${actionIndex}`, false); - }); - // Connect actions to destinations - const actionToDestinationEdges: Edge[] = actions.flatMap((_, actionIndex) => { - return destinations.map((_, destinationIndex) => - createEdge(`action-${actionIndex}-to-destination-${destinationIndex}`, `action-${actionIndex}`, `destination-${destinationIndex}`) - ); - }); - - if (actions.length === 0) { - for (let i = 0; i < destinations.length; i++) { - actionToDestinationEdges.push(createEdge(`action-0-to-destination-${i}`, `action-0`, `destination-${i}`, false)); - } + if (!sources.length) { + edges.push(createEdge('source-0-to-action-0', 'source-0', 'action-0', false)); + } else { + sources.forEach((_, sourceIndex) => { + const actionIndex = 0; + edges.push(createEdge(`source-${sourceIndex}-to-action-${actionIndex}`, `source-${sourceIndex}`, `action-${actionIndex}`, false)); + }); } - // Combine all edges - edges.push(...sourceToActionEdges, ...actionToDestinationEdges); + // Connect actions to destinations + if (!destinations.length) { + edges.push(createEdge('action-0-to-destination-0', 'action-0', 'destination-0')); + } else { + destinations.forEach((_, destinationIndex) => { + const actionIndex = !actions.length ? 0 : actions.length - 1; + edges.push(createEdge(`action-${actionIndex}-to-destination-${destinationIndex}`, `action-${actionIndex}`, `destination-${destinationIndex}`)); + }); + } return { nodes, edges }; }; diff --git a/frontend/webapp/reuseable-components/nodes-data-flow/index.tsx b/frontend/webapp/reuseable-components/nodes-data-flow/index.tsx index fe339032f..33a0039f2 100644 --- a/frontend/webapp/reuseable-components/nodes-data-flow/index.tsx +++ b/frontend/webapp/reuseable-components/nodes-data-flow/index.tsx @@ -1,24 +1,28 @@ 'use client'; -import React from 'react'; +import React, { useMemo } from 'react'; import '@xyflow/react/dist/style.css'; +import AddNode from './nodes/add-node'; import BaseNode from './nodes/base-node'; import { ReactFlow } from '@xyflow/react'; -import headerNode from './nodes/header-node'; -import AddNode from './nodes/add-node'; - -const nodeTypes = { - header: headerNode, - add: AddNode, - base: BaseNode, -}; +import HeaderNode from './nodes/header-node'; interface NodeBaseDataFlowProps { nodes: any[]; edges: any[]; onNodeClick?: (event: React.MouseEvent, object: any) => void; + columnWidth: number; } -export function NodeBaseDataFlow({ nodes, edges, onNodeClick }: NodeBaseDataFlowProps) { +export function NodeBaseDataFlow({ nodes, edges, onNodeClick, columnWidth }: NodeBaseDataFlowProps) { + const nodeTypes = useMemo( + () => ({ + header: (props) => , + add: (props) => , + base: (props) => , + }), + [columnWidth] + ); + return (
diff --git a/frontend/webapp/reuseable-components/nodes-data-flow/nodes/add-node.tsx b/frontend/webapp/reuseable-components/nodes-data-flow/nodes/add-node.tsx index b09d5fc55..2e02a89a9 100644 --- a/frontend/webapp/reuseable-components/nodes-data-flow/nodes/add-node.tsx +++ b/frontend/webapp/reuseable-components/nodes-data-flow/nodes/add-node.tsx @@ -4,10 +4,10 @@ import styled from 'styled-components'; import { Text } from '@/reuseable-components'; import { Handle, Position } from '@xyflow/react'; -const BaseNodeContainer = styled.div` - display: flex; - width: 296px; +const BaseNodeContainer = styled.div<{ columnWidth: number }>` + width: ${({ columnWidth }) => `${columnWidth}px`}; padding: 16px 24px 16px 16px; + display: flex; flex-direction: column; justify-content: center; align-items: center; @@ -39,6 +39,7 @@ const Title = styled(Text)` const SubTitle = styled(Text)` font-size: 12px; color: ${({ theme }) => theme.text.grey}; + text-align: center; `; interface BaseNodeProps { @@ -63,11 +64,13 @@ interface BaseNodeProps { positionAbsoluteY: number; sourcePosition?: any; targetPosition?: any; + + columnWidth: number; } -const AddNode = ({ id, isConnectable, data }: BaseNodeProps) => { +const AddNode = ({ id, isConnectable, data, columnWidth }: BaseNodeProps) => { return ( - + plus {data.title} diff --git a/frontend/webapp/reuseable-components/nodes-data-flow/nodes/base-node.tsx b/frontend/webapp/reuseable-components/nodes-data-flow/nodes/base-node.tsx index ee004451f..8b32f881a 100644 --- a/frontend/webapp/reuseable-components/nodes-data-flow/nodes/base-node.tsx +++ b/frontend/webapp/reuseable-components/nodes-data-flow/nodes/base-node.tsx @@ -5,14 +5,14 @@ import type { STATUSES } from '@/types'; import { Handle, Position } from '@xyflow/react'; import { Status, Text } from '@/reuseable-components'; -const BaseNodeContainer = styled.div` - display: flex; +const BaseNodeContainer = styled.div<{ columnWidth: number }>` + width: ${({ columnWidth }) => `${columnWidth}px`}; padding: 16px 24px 16px 16px; - align-items: center; gap: 8px; + display: flex; + align-items: center; align-self: stretch; border-radius: 16px; - width: 296px; cursor: pointer; background-color: ${({ theme }) => theme.colors.white_opacity['004']}; @@ -45,6 +45,13 @@ const FooterWrapper = styled.div` align-items: center; `; +const Title = styled(Text)<{ columnWidth: number }>` + width: ${({ columnWidth }) => `${columnWidth - 42}px`}; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +`; + const FooterText = styled(Text)` color: ${({ theme }) => theme.text.grey}; font-size: 10px; @@ -65,9 +72,10 @@ interface BaseNodeProps { id: string; isConnectable: boolean; data: NodeDataProps; + columnWidth: number; } -const BaseNode = ({ isConnectable, data }: BaseNodeProps) => { +const BaseNode = ({ isConnectable, data, columnWidth }: BaseNodeProps) => { const { title, subTitle, imageUri, type, monitors, isActive } = data; function renderHandles() { @@ -128,15 +136,14 @@ const BaseNode = ({ isConnectable, data }: BaseNodeProps) => { } return ( - + source - {title} + {title} {subTitle} - {renderMonitors()} {renderStatus()} diff --git a/frontend/webapp/reuseable-components/nodes-data-flow/nodes/header-node.tsx b/frontend/webapp/reuseable-components/nodes-data-flow/nodes/header-node.tsx index f8060475b..0a48fcd53 100644 --- a/frontend/webapp/reuseable-components/nodes-data-flow/nodes/header-node.tsx +++ b/frontend/webapp/reuseable-components/nodes-data-flow/nodes/header-node.tsx @@ -1,15 +1,15 @@ 'use client'; -import React, { memo } from 'react'; +import React from 'react'; import Image from 'next/image'; import styled from 'styled-components'; import { Text } from '@/reuseable-components'; -const ColumnContainer = styled.div` +const ColumnContainer = styled.div<{ columnWidth: number }>` + width: ${({ columnWidth }) => `${columnWidth + 40}px`}; + padding: 12px 0px 16px 0px; + gap: 8px; display: flex; align-items: center; - gap: 8px; - padding: 12px 0px 16px 0px; - width: 336px; border-bottom: 1px solid ${({ theme }) => theme.colors.border}; `; @@ -39,11 +39,12 @@ interface Column { interface HeaderNodeProps { data: Column; + columnWidth: number; } -export default memo(({ data }: HeaderNodeProps) => { +const HeaderNode = ({ data, columnWidth }: HeaderNodeProps) => { return ( - + {data.title} {data.title} @@ -51,4 +52,6 @@ export default memo(({ data }: HeaderNodeProps) => { ); -}); +}; + +export default HeaderNode; diff --git a/frontend/webapp/reuseable-components/notification-note/index.tsx b/frontend/webapp/reuseable-components/notification-note/index.tsx index 457ec7cf4..d61749943 100644 --- a/frontend/webapp/reuseable-components/notification-note/index.tsx +++ b/frontend/webapp/reuseable-components/notification-note/index.tsx @@ -4,7 +4,7 @@ import Image from 'next/image'; import { Text } from '../text'; // Define the notification types -type NotificationType = 'warning' | 'error' | 'success' | 'info'; +type NotificationType = 'warning' | 'error' | 'success' | 'info' | 'default'; interface NotificationProps { type: NotificationType; @@ -13,6 +13,7 @@ interface NotificationProps { label: string; onClick: () => void; }; + style?: React.CSSProperties; } const NotificationContainer = styled.div<{ type: NotificationType }>` @@ -31,9 +32,10 @@ const NotificationContainer = styled.div<{ type: NotificationType }>` case 'success': return '#28A745'; // Green case 'info': - return '#181944'; // Blue + return '#F9F9F90A'; // Default to info color + case 'default': default: - return '#2B2D66'; // Default to info color + return '#181944'; // Blue } }}; `; @@ -56,9 +58,10 @@ const Title = styled(Text)<{ type: NotificationType }>` case 'success': return '#28A745'; case 'info': - return '#AABEF7'; + return '#B8B8B8'; + case 'default': default: - return '#2B2D66'; + return '#AABEF7'; } }}; `; @@ -86,47 +89,21 @@ const ActionButton = styled(Text)` const NotificationIcon = ({ type }: { type: NotificationType }) => { switch (type) { case 'warning': - return ( - warning - ); + return warning; case 'error': - return ( - error - ); + return error; case 'success': - return ( - success - ); + return success; case 'info': + return info; default: - return ( - info - ); + return info; } }; -const NotificationNote: React.FC = ({ - type, - text, - action, -}) => { +const NotificationNote: React.FC = ({ type, text, action, style }) => { return ( - + @@ -135,7 +112,7 @@ const NotificationNote: React.FC = ({ {action && ( - {action.label} + {action.label} )} diff --git a/frontend/webapp/types/common.ts b/frontend/webapp/types/common.ts index 814a09ae3..8fe055b18 100644 --- a/frontend/webapp/types/common.ts +++ b/frontend/webapp/types/common.ts @@ -37,12 +37,14 @@ export interface StepProps { } export enum OVERVIEW_ENTITY_TYPES { + RULE = 'rule', SOURCE = 'source', ACTION = 'action', DESTINATION = 'destination', } export enum OVERVIEW_NODE_TYPES { + ADD_RULE = 'addRule', ADD_SOURCE = 'addSource', ADD_ACTION = 'addAction', ADD_DESTIONATION = 'addDestination', diff --git a/frontend/webapp/types/instrumentation-rules.ts b/frontend/webapp/types/instrumentation-rules.ts index c2c8d560f..681d87a75 100644 --- a/frontend/webapp/types/instrumentation-rules.ts +++ b/frontend/webapp/types/instrumentation-rules.ts @@ -1,6 +1,6 @@ // Enumeration of possible Instrumentation Rule Types export enum InstrumentationRuleType { - PAYLOAD_COLLECTION = 'payload-collection', + PAYLOAD_COLLECTION = 'PayloadCollection', } export enum RulesType { diff --git a/frontend/webapp/utils/functions/get-action-icon.ts b/frontend/webapp/utils/functions/get-action-icon.ts index d4fadfb07..a01f5323f 100644 --- a/frontend/webapp/utils/functions/get-action-icon.ts +++ b/frontend/webapp/utils/functions/get-action-icon.ts @@ -1,12 +1,10 @@ -const ACTION_ICON_PATH = '/icons/actions/'; +import type { ActionsType } from '@/types'; -export const getActionIcon = (actionType?: string) => { - if (!actionType) { - return `${ACTION_ICON_PATH}add-action.svg`; - } +const ICON_PATH = '/icons/actions/'; - const typeLowerCased = actionType.toLowerCase(); +export const getActionIcon = (type: ActionsType | 'sampler') => { + const typeLowerCased = type.toLowerCase(); const isSampler = typeLowerCased.includes('sampler'); - return `${ACTION_ICON_PATH}${isSampler ? 'sampler' : typeLowerCased}.svg`; + return `${ICON_PATH}${isSampler ? 'sampler' : typeLowerCased}.svg`; }; diff --git a/frontend/webapp/utils/functions/get-rule-icon.ts b/frontend/webapp/utils/functions/get-rule-icon.ts new file mode 100644 index 000000000..3583ca039 --- /dev/null +++ b/frontend/webapp/utils/functions/get-rule-icon.ts @@ -0,0 +1,9 @@ +import type { InstrumentationRuleType } from '@/types'; + +const ICON_PATH = '/icons/rules/'; + +export const getRuleIcon = (type: InstrumentationRuleType) => { + const typeLowerCased = type.replaceAll('-', '').toLowerCase(); + + return `${ICON_PATH}${typeLowerCased}.svg`; +}; diff --git a/frontend/webapp/utils/functions/index.ts b/frontend/webapp/utils/functions/index.ts index df073e059..58f9f1740 100644 --- a/frontend/webapp/utils/functions/index.ts +++ b/frontend/webapp/utils/functions/index.ts @@ -1,3 +1,4 @@ export * from './strings'; export * from './get-action-icon'; export * from './get-status-icon'; +export * from './get-rule-icon';