diff --git a/src/components/filters/filters-group/filters-group.js b/src/components/filters/filters-group/filters-group.js index d2b0076860..2edb9af1eb 100644 --- a/src/components/filters/filters-group/filters-group.js +++ b/src/components/filters/filters-group/filters-group.js @@ -8,13 +8,7 @@ import { getDataTestAttribute } from '../../../utils/get-data-test-attribute'; import './filters-group.scss'; /** A group collection of FiltersRow */ -const FiltersGroup = ({ - items = [], - group, - collapsed, - onItemClick, - onItemChange, -}) => ( +const FiltersGroup = ({ items = [], group, collapsed, onItemChange }) => ( (end - start) * nodeListRowHeight} total={items.length} @@ -40,8 +34,8 @@ const FiltersGroup = ({ kind={group.kind} label={item.highlightedLabel} name={item.name} - onChange={(e) => onItemChange(item, !e.target.checked)} - onClick={() => onItemClick(item)} + onChange={(e) => onItemChange(e, item)} + onClick={(e) => onItemChange(e, item)} parentClassName={'node-list-filter-row'} visible={item.visible} indicatorIcon={item.visibleIcon} diff --git a/src/components/filters/filters-group/filters-group.scss b/src/components/filters/filters-group/filters-group.scss index 00dc8045f9..c36a015442 100644 --- a/src/components/filters/filters-group/filters-group.scss +++ b/src/components/filters/filters-group/filters-group.scss @@ -1,5 +1,5 @@ @use '../../../styles/variables' as var; -@use '../../node-list/styles/variables'; +@use '../../node-list-tree/styles/variables'; .filters-group { list-style: none; diff --git a/src/components/filters/filters-group/filters-group.test.js b/src/components/filters/filters-group/filters-group.test.js index edb6d682f9..7f91be5ca0 100644 --- a/src/components/filters/filters-group/filters-group.test.js +++ b/src/components/filters/filters-group/filters-group.test.js @@ -3,7 +3,7 @@ import FiltersGroup from './filters-group'; import { mockState, setup } from '../../../utils/state.mock'; import { getNodeTypes } from '../../../selectors/node-types'; import { getGroupedNodes } from '../../../selectors/nodes'; -import { getGroups } from '../../node-list/node-list-items'; +import { getGroups } from '../../../selectors/filtered-node-list-items'; describe('FiltersGroup Component', () => { const mockProps = () => { diff --git a/src/components/filters/filters-row/filters-row.scss b/src/components/filters/filters-row/filters-row.scss index 43a3d657b3..3f25875237 100644 --- a/src/components/filters/filters-row/filters-row.scss +++ b/src/components/filters/filters-row/filters-row.scss @@ -1,5 +1,5 @@ @use '../../../styles/variables' as var; -@use '../../node-list/styles/variables'; +@use '../../node-list-tree/styles/variables'; .MuiTreeItem-iconContainer svg { z-index: var.$zindex-MuiTreeItem-icon; diff --git a/src/components/filters/filters-section-heading/filters-section-heading.scss b/src/components/filters/filters-section-heading/filters-section-heading.scss index ce0644bb48..cdd1ea8dc1 100644 --- a/src/components/filters/filters-section-heading/filters-section-heading.scss +++ b/src/components/filters/filters-section-heading/filters-section-heading.scss @@ -1,5 +1,5 @@ @use '../../../styles/variables' as var; -@use '../../node-list/styles/variables'; +@use '../../node-list-tree/styles/variables'; .filters-section-heading { background: var(--color-nodelist-filter-panel); diff --git a/src/components/filters/filters-section-heading/filters-section-heading.test.js b/src/components/filters/filters-section-heading/filters-section-heading.test.js index af752fd847..84c57d603d 100755 --- a/src/components/filters/filters-section-heading/filters-section-heading.test.js +++ b/src/components/filters/filters-section-heading/filters-section-heading.test.js @@ -3,7 +3,7 @@ import FiltersSectionHeading from './filters-section-heading'; import { mockState, setup } from '../../../utils/state.mock'; import { getNodeTypes } from '../../../selectors/node-types'; import { getGroupedNodes } from '../../../selectors/nodes'; -import { getGroups } from '../../node-list/node-list-items'; +import { getGroups } from '../../../selectors/filtered-node-list-items'; describe('FiltersSectionHeading', () => { const mockProps = () => { diff --git a/src/components/filters/filters-section/filters-section.js b/src/components/filters/filters-section/filters-section.js index 0a16489c20..808aee952e 100644 --- a/src/components/filters/filters-section/filters-section.js +++ b/src/components/filters/filters-section/filters-section.js @@ -12,9 +12,6 @@ const FiltersSection = ({ items, onGroupToggleChanged, onItemChange, - onItemClick, - onItemMouseEnter, - onItemMouseLeave, onToggleGroupCollapsed, searchValue, }) => { @@ -41,9 +38,6 @@ const FiltersSection = ({ group={group} items={groupItems} onItemChange={onItemChange} - onItemClick={onItemClick} - onItemMouseEnter={onItemMouseEnter} - onItemMouseLeave={onItemMouseLeave} /> ); diff --git a/src/components/filters/filters-section/filters-section.test.js b/src/components/filters/filters-section/filters-section.test.js index 57241db95c..6c476e32cd 100755 --- a/src/components/filters/filters-section/filters-section.test.js +++ b/src/components/filters/filters-section/filters-section.test.js @@ -3,7 +3,7 @@ import FiltersSection from './filters-section'; import { mockState, setup } from '../../../utils/state.mock'; import { getNodeTypes } from '../../../selectors/node-types'; import { getGroupedNodes } from '../../../selectors/nodes'; -import { getGroups } from '../../node-list/node-list-items'; +import { getGroups } from '../../../selectors/filtered-node-list-items'; describe('FiltersSection Component', () => { const mockProps = () => { diff --git a/src/components/filters/filters.js b/src/components/filters/filters.js index 749cd4ac6e..2797ebd33c 100644 --- a/src/components/filters/filters.js +++ b/src/components/filters/filters.js @@ -10,9 +10,6 @@ const Filters = ({ items, onGroupToggleChanged, onItemChange, - onItemClick, - onItemMouseEnter, - onItemMouseLeave, onResetFilter, onToggleGroupCollapsed, searchValue, @@ -41,9 +38,6 @@ const Filters = ({ key={group.id} onGroupToggleChanged={onGroupToggleChanged} onItemChange={onItemChange} - onItemClick={onItemClick} - onItemMouseEnter={onItemMouseEnter} - onItemMouseLeave={onItemMouseLeave} onToggleGroupCollapsed={onToggleGroupCollapsed} searchValue={searchValue} /> diff --git a/src/components/filters/filters.scss b/src/components/filters/filters.scss index dccad4d453..c3b7742276 100644 --- a/src/components/filters/filters.scss +++ b/src/components/filters/filters.scss @@ -1,4 +1,4 @@ -@use '../node-list/styles/variables'; +@use '../node-list-tree/styles/variables'; @use '../../styles/extends'; @use '../../styles/variables' as colors; diff --git a/src/components/filters/filters.test.js b/src/components/filters/filters.test.js index 76af07c597..4b1ac0198b 100644 --- a/src/components/filters/filters.test.js +++ b/src/components/filters/filters.test.js @@ -3,7 +3,7 @@ import Filters from './filters'; import { mockState, setup } from '../../utils/state.mock'; import { getNodeTypes } from '../../selectors/node-types'; import { getGroupedNodes } from '../../selectors/nodes'; -import { getGroups } from '../node-list/node-list-items'; +import { getGroups } from '../../selectors/filtered-node-list-items'; describe('Filters', () => { const mockProps = () => { diff --git a/src/components/node-list/components/row/row.js b/src/components/node-list-tree/node-list-row/node-list-row.js similarity index 61% rename from src/components/node-list/components/row/row.js rename to src/components/node-list-tree/node-list-row/node-list-row.js index 416bcb4947..619bd301c4 100755 --- a/src/components/node-list/components/row/row.js +++ b/src/components/node-list-tree/node-list-row/node-list-row.js @@ -1,15 +1,15 @@ import React from 'react'; import classnames from 'classnames'; -import NodeIcon from '../../../icons/node-icon'; -import VisibleIcon from '../../../icons/visible'; -import InvisibleIcon from '../../../icons/invisible'; -import FocusModeIcon from '../../../icons/focus-mode'; -import { ToggleControl } from '../../../ui/toggle-control/toggle-control'; -import { RowText } from '../../../ui/row-text/row-text'; +import NodeIcon from '../../icons/node-icon'; +import VisibleIcon from '../../icons/visible'; +import InvisibleIcon from '../../icons/invisible'; +import FocusModeIcon from '../../icons/focus-mode'; +import { ToggleControl } from '../../ui/toggle-control/toggle-control'; +import { RowText } from '../../ui/row-text/row-text'; -import './row.scss'; +import './node-list-row.scss'; -const Row = ({ +const NodeListRow = ({ active, checked, children, @@ -44,24 +44,34 @@ const Row = ({ return (
{VisibilityIcon && ( * { opacity: 0.3; } @@ -70,8 +74,8 @@ &--active, &--selected, - .row--visible:hover &, - [data-whatintent='keyboard'] .row__text:focus & { + .node-list-row--visible:hover &, + [data-whatintent='keyboard'] .node-list-row__text:focus & { > * { opacity: 1; } diff --git a/src/components/node-list-tree/node-list-row/node-list-row.test.js b/src/components/node-list-tree/node-list-row/node-list-row.test.js new file mode 100644 index 0000000000..eda3cf1bf0 --- /dev/null +++ b/src/components/node-list-tree/node-list-row/node-list-row.test.js @@ -0,0 +1,78 @@ +import React from 'react'; +import NodeListRow from './node-list-row'; +import { setup } from '../../../utils/state.mock'; + +// Mock props +const mockProps = { + name: 'Test Row', + kind: 'modular-pipeline', + active: false, + disabled: false, + selected: false, + visible: true, + onMouseEnter: jest.fn(), + onMouseLeave: jest.fn(), + onClick: jest.fn(), + icon: null, + type: 'modularPipeline', + checked: true, + focused: false, +}; + +describe('NodeListRow Component', () => { + it('renders without crashing', () => { + expect(() => setup.mount()).not.toThrow(); + }); + + it('handles mouseenter events', () => { + const wrapper = setup.mount(); + const nodeRow = () => wrapper.find('.node-list-row'); + nodeRow().simulate('mouseenter'); + expect(mockProps.onMouseEnter.mock.calls.length).toEqual(1); + }); + + it('handles mouseleave events', () => { + const wrapper = setup.mount(); + const nodeRow = () => wrapper.find('.node-list-row'); + nodeRow().simulate('mouseleave'); + expect(mockProps.onMouseLeave.mock.calls.length).toEqual(1); + }); + + it('applies the node-list-row--active class when active is true', () => { + const wrapper = setup.mount(); + expect( + wrapper.find('.node-list-row').hasClass('node-list-row--active') + ).toBe(true); + }); + + it('applies the node-list-row--selected class when selected is true', () => { + const wrapper = setup.mount(); + expect( + wrapper.find('.node-list-row').hasClass('node-list-row--selected') + ).toBe(true); + }); + + it('applies the node-list-row--selected class when highlight is true and isSlicingPipelineApplied is false', () => { + const wrapper = setup.mount( + + ); + expect( + wrapper.find('.node-list-row').hasClass('node-list-row--selected') + ).toBe(true); + }); + + it('applies the overwrite class if not selected or active', () => { + const activeNodeWrapper = setup.mount( + + ); + expect( + activeNodeWrapper + .find('.node-list-row') + .hasClass('node-list-row--overwrite') + ).toBe(true); + }); +}); diff --git a/src/components/node-list/node-list-tree-item.js b/src/components/node-list-tree/node-list-tree-item/node-list-tree-item.js similarity index 93% rename from src/components/node-list/node-list-tree-item.js rename to src/components/node-list-tree/node-list-tree-item/node-list-tree-item.js index 81c5cebfa3..488ab74d21 100644 --- a/src/components/node-list/node-list-tree-item.js +++ b/src/components/node-list-tree/node-list-tree-item/node-list-tree-item.js @@ -3,8 +3,8 @@ import classnames from 'classnames'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import { TreeItem } from '@mui/x-tree-view'; -import Row from './components/row/row'; -import { getDataTestAttribute } from '../../utils/get-data-test-attribute'; +import NodeListRow from '../node-list-row/node-list-row'; +import { getDataTestAttribute } from '../../../utils/get-data-test-attribute'; const arrowIconColor = '#8e8e90'; @@ -29,7 +29,7 @@ const NodeListTreeItem = ({ collapseIcon={} expandIcon={} label={ - { +const getNodeRowData = (node, disabled, hoveredNode, selected, highlight) => { const checked = !node.disabledNode; + return { ...node, visibleIcon: VisibleIcon, invisibleIcon: InvisibleIcon, - active: node.active, + active: node.active || hoveredNode === node.id, selected, highlight, faded: disabled || node.disabledNode, @@ -111,6 +110,7 @@ const getNodeRowData = (node, disabled, selected, highlight) => { }; const TreeListProvider = ({ + hoveredNode, nodeSelected, modularPipelinesSearchResult, modularPipelinesTree, @@ -154,7 +154,13 @@ const TreeListProvider = ({ const selected = nodeSelected[node.id]; const highlight = slicedPipeline.includes(node.id); - const data = getNodeRowData(node, disabled, selected, highlight); + const data = getNodeRowData( + node, + disabled, + hoveredNode, + selected, + highlight + ); return ( ({ - nodeSelected: getNodeSelected(state), - expanded: state.modularPipeline.expanded, - slicedPipeline: getSlicedPipeline(state), - isSlicingPipelineApplied: state.slice.apply, -}); - -export const mapDispatchToProps = (dispatch) => ({ - onToggleNodeSelected: (nodeID) => { - dispatch(loadNodeData(nodeID)); - }, -}); - -export default connect(mapStateToProps, mapDispatchToProps)(TreeListProvider); +export default TreeListProvider; diff --git a/src/components/node-list/styles/_panels.scss b/src/components/node-list-tree/styles/_panels.scss similarity index 100% rename from src/components/node-list/styles/_panels.scss rename to src/components/node-list-tree/styles/_panels.scss diff --git a/src/components/node-list/styles/_variables.scss b/src/components/node-list-tree/styles/_variables.scss similarity index 100% rename from src/components/node-list/styles/_variables.scss rename to src/components/node-list-tree/styles/_variables.scss diff --git a/src/components/node-list/styles/node-list.scss b/src/components/node-list-tree/styles/node-list.scss similarity index 99% rename from src/components/node-list/styles/node-list.scss rename to src/components/node-list-tree/styles/node-list.scss index 4ebf9cf0ff..d3ca6ac65c 100644 --- a/src/components/node-list/styles/node-list.scss +++ b/src/components/node-list-tree/styles/node-list.scss @@ -114,7 +114,7 @@ position: relative; // Ensure all .row__type-icon path elements have opacity 1 - .row__type-icon path { + .node-list-row__type-icon path { opacity: 1; } diff --git a/src/components/node-list/components/row/row.test.js b/src/components/node-list/components/row/row.test.js deleted file mode 100644 index 42294ab8dd..0000000000 --- a/src/components/node-list/components/row/row.test.js +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react'; -import Row from './row'; -import { setup } from '../../../../utils/state.mock'; - -// Mock props -const mockProps = { - name: 'Test Row', - kind: 'modular-pipeline', - active: false, - disabled: false, - selected: false, - visible: true, - onMouseEnter: jest.fn(), - onMouseLeave: jest.fn(), - onClick: jest.fn(), - icon: null, - type: 'modularPipeline', - checked: true, - focused: false, -}; - -describe('Row Component', () => { - it('renders without crashing', () => { - expect(() => setup.mount()).not.toThrow(); - }); - - it('handles mouseenter events', () => { - const wrapper = setup.mount(); - const nodeRow = () => wrapper.find('.row'); - nodeRow().simulate('mouseenter'); - expect(mockProps.onMouseEnter.mock.calls.length).toEqual(1); - }); - - it('handles mouseleave events', () => { - const wrapper = setup.mount(); - const nodeRow = () => wrapper.find('.row'); - nodeRow().simulate('mouseleave'); - expect(mockProps.onMouseLeave.mock.calls.length).toEqual(1); - }); - - it('applies the row--active class when active is true', () => { - const wrapper = setup.mount(); - expect(wrapper.find('.row').hasClass('row--active')).toBe(true); - }); - - it('applies the row--selected class when selected is true', () => { - const wrapper = setup.mount(); - expect(wrapper.find('.row').hasClass('row--selected')).toBe(true); - }); - - it('applies the row--selected class when highlight is true and isSlicingPipelineApplied is false', () => { - const wrapper = setup.mount( - - ); - expect(wrapper.find('.row').hasClass('row--selected')).toBe(true); - }); - - it('applies the overwrite class if not selected or active', () => { - const activeNodeWrapper = setup.mount( - - ); - expect(activeNodeWrapper.find('.row').hasClass('row--overwrite')).toBe( - true - ); - }); -}); diff --git a/src/components/node-list/index.js b/src/components/node-list/index.js deleted file mode 100644 index e338a34960..0000000000 --- a/src/components/node-list/index.js +++ /dev/null @@ -1,410 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { connect } from 'react-redux'; -import debounce from 'lodash/debounce'; -import NodeList from './node-list'; -import { - getFilteredItems, - getGroups, - isTagType, - isElementType, - isGroupType, -} from './node-list-items'; -import { - getNodeTypes, - isModularPipelineType, -} from '../../selectors/node-types'; -import { getTagData, getTagNodeCounts } from '../../selectors/tags'; -import { - getFocusedModularPipeline, - getModularPipelinesSearchResult, -} from '../../selectors/modular-pipelines'; -import { - getGroupedNodes, - getNodeSelected, - getInputOutputNodesForFocusedModularPipeline, - getModularPipelinesTree, -} from '../../selectors/nodes'; -import { toggleTagActive, toggleTagFilter } from '../../actions/tags'; -import { toggleTypeDisabled } from '../../actions/node-type'; -import { - toggleParametersHovered, - toggleFocusMode, - toggleHoveredFocusMode, -} from '../../actions'; -import { - toggleModularPipelineActive, - toggleModularPipelineDisabled, - toggleModularPipelinesExpanded, -} from '../../actions/modular-pipelines'; -import { resetSlicePipeline } from '../../actions/slice'; -import { - loadNodeData, - toggleNodeHovered, - toggleNodesDisabled, -} from '../../actions/nodes'; -import { useGeneratePathname } from '../../utils/hooks/use-generate-pathname'; -import './styles/node-list.scss'; -import { params, NODE_TYPES, localStorageName } from '../../config'; -import { loadLocalStorage, saveLocalStorage } from '../../store/helpers'; - -const storedState = loadLocalStorage(localStorageName); - -/** - * Provides data from the store to populate a NodeList component. - * Also handles user interaction and dispatches updates back to the store. - */ -const NodeListProvider = ({ - faded, - nodes, - nodeSelected, - tags, - tagNodeCounts, - nodeTypes, - onToggleNodesDisabled, - onToggleNodeSelected, - onToggleNodeActive, - onToggleParametersActive, - onToggleTagActive, - onToggleTagFilter, - onToggleModularPipelineActive, - onToggleModularPipelineDisabled, - onToggleModularPipelineExpanded, - onToggleTypeDisabled, - onToggleFocusMode, - onToggleHoveredFocusMode, - modularPipelinesTree, - focusMode, - disabledModularPipeline, - inputOutputDataNodes, - onResetSlicePipeline, - isSlicingPipelineApplied, -}) => { - const [searchValue, updateSearchValue] = useState(''); - const [isResetFilterActive, setIsResetFilterActive] = useState(false); - const [groupCollapsed, setGroupCollapsed] = useState( - storedState.groupsCollapsed || {} - ); - - const { - toSelectedPipeline, - toSelectedNode, - toFocusedModularPipeline, - toUpdateUrlParamsOnResetFilter, - toUpdateUrlParamsOnFilter, - toSetQueryParam, - } = useGeneratePathname(); - - const items = getFilteredItems({ - nodes, - tags, - nodeTypes, - tagNodeCounts, - nodeSelected, - searchValue, - focusMode, - inputOutputDataNodes, - }); - - const modularPipelinesSearchResult = searchValue - ? getModularPipelinesSearchResult(modularPipelinesTree, searchValue) - : null; - - const groups = getGroups({ items }); - - const onItemClick = (event, item) => { - if (isGroupType(item.type)) { - onGroupItemChange(item, item.checked); - } else if (isModularPipelineType(item.type)) { - onToggleNodeSelected(null); - } else { - if (item.faded || item.selected) { - onToggleNodeSelected(null); - toSelectedPipeline(); - } else { - onToggleNodeSelected(item.id); - toSelectedNode(item); - // Reset the pipeline slicing filters if no slicing is currently applied - if (!isSlicingPipelineApplied) { - onResetSlicePipeline(); - } - } - } - - // to prevent page reload on form submission - event.preventDefault(); - }; - - // To get existing values from URL query parameters - const getExistingValuesFromUrlQueryParams = (paramName, searchParams) => { - const paramValues = searchParams.get(paramName); - return new Set(paramValues ? paramValues.split(',') : []); - }; - - const handleUrlParamsUpdateOnFilter = (item) => { - const searchParams = new URLSearchParams(window.location.search); - const paramName = isElementType(item.type) ? params.types : params.tags; - const existingValues = getExistingValuesFromUrlQueryParams( - paramName, - searchParams - ); - - toUpdateUrlParamsOnFilter(item, paramName, existingValues); - }; - - // To update URL query parameters when a filter group is clicked - const handleUrlParamsUpdateOnGroupFilter = ( - groupType, - groupItems, - groupItemsDisabled - ) => { - if (groupItemsDisabled) { - // If all items in group are disabled - groupItems.forEach((item) => { - handleUrlParamsUpdateOnFilter(item); - }); - } else { - // If some items in group are enabled - const paramName = isElementType(groupType) ? params.types : params.tags; - toSetQueryParam(paramName, []); - } - }; - - const onItemChange = (item, checked, clickedIconType) => { - if (isGroupType(item.type) || isModularPipelineType(item.type)) { - onGroupItemChange(item, checked); - - // Update URL query parameters when a filter item is clicked - if (!clickedIconType) { - handleUrlParamsUpdateOnFilter(item); - } - - if (isModularPipelineType(item.type)) { - if (clickedIconType === 'focus') { - if (focusMode === null) { - onToggleFocusMode(item); - toFocusedModularPipeline(item); - - if (disabledModularPipeline[item.id]) { - onToggleModularPipelineDisabled([item.id], checked); - } - } else { - onToggleFocusMode(null); - toSelectedPipeline(); - } - } else { - onToggleModularPipelineDisabled([item.id], checked); - onToggleModularPipelineActive([item.id], false); - } - } - } else { - if (checked) { - onToggleNodeActive(null); - } - - onToggleNodesDisabled([item.id], checked); - } - }; - - const onItemMouseEnter = (item) => { - if (isTagType(item.type)) { - onToggleTagActive(item.id, true); - } else if (isModularPipelineType(item.type)) { - onToggleModularPipelineActive(item.id, true); - } else if (isElementType(item.type) && item.id === 'parameters') { - // Show parameters highlight when mouse enter parameters filter item - onToggleParametersActive(true); - } else if (item.visible) { - onToggleNodeActive(item.id); - } - }; - - const onItemMouseLeave = (item) => { - if (isTagType(item.type)) { - onToggleTagActive(item.id, false); - } else if (isModularPipelineType(item.type)) { - onToggleModularPipelineActive(item.id, false); - } else if (isElementType(item.type) && item.id === 'parameters') { - // Hide parameters highlight when mouse leave parameters filter item - onToggleParametersActive(false); - } else if (item.visible) { - onToggleNodeActive(null); - } - }; - - // Collapse/expand node group of filters - const onToggleGroupCollapsed = (groupID) => { - const res = { - ...groupCollapsed, - [groupID]: !groupCollapsed[groupID], - }; - - setGroupCollapsed(res); - saveLocalStorage(localStorageName, { groupsCollapsed: res }); - }; - - const onGroupToggleChanged = (groupType) => { - // Enable all items in group if none enabled, otherwise disable all of them - const groupItems = items[groupType] || []; - const groupItemsDisabled = groupItems.every( - (groupItem) => !groupItem.checked - ); - - // Update URL query parameters when a filter group is clicked - handleUrlParamsUpdateOnGroupFilter( - groupType, - groupItems, - groupItemsDisabled - ); - - if (isTagType(groupType)) { - onToggleTagFilter( - groupItems.map((item) => item.id), - groupItemsDisabled - ); - } else if (isElementType(groupType)) { - onToggleTypeDisabled( - groupItems.reduce( - (state, item) => ({ ...state, [item.id]: !groupItemsDisabled }), - {} - ) - ); - } - }; - - const handleToggleModularPipelineExpanded = (expanded) => { - onToggleModularPipelineExpanded(expanded); - }; - - const onGroupItemChange = (item, wasChecked) => { - // Toggle the group - if (isTagType(item.type)) { - onToggleTagFilter(item.id, !wasChecked); - } else if (isElementType(item.type)) { - onToggleTypeDisabled({ [item.id]: wasChecked }); - } - - // Reset node selection - onToggleNodeSelected(null); - onToggleNodeActive(null); - }; - - // Deselect node on Escape key - const handleKeyDown = (event) => { - if (event.keyCode === 27) { - onToggleNodeSelected(null); - } - }; - - // Reset applied filters to default - const onResetFilter = () => { - onToggleTypeDisabled({ task: false, data: false, parameters: true }); - onToggleTagFilter( - tags.map((item) => item.id), - false - ); - - toUpdateUrlParamsOnResetFilter(); - }; - - // Helper function to check if NodeTypes is modified - const hasModifiedNodeTypes = (nodeTypes) => { - return nodeTypes.some( - (item) => NODE_TYPES[item.id]?.defaultState !== item.disabled - ); - }; - - // Updates the reset filter button status based on the node types and tags. - useEffect(() => { - const isNodeTypeModified = hasModifiedNodeTypes(nodeTypes); - const isNodeTagModified = tags.some((tag) => tag.enabled); - setIsResetFilterActive(isNodeTypeModified || isNodeTagModified); - }, [tags, nodeTypes]); - - useEffect(() => { - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }); - - return ( - - ); -}; - -export const mapStateToProps = (state) => ({ - tags: getTagData(state), - tagNodeCounts: getTagNodeCounts(state), - nodes: getGroupedNodes(state), - nodeSelected: getNodeSelected(state), - nodeTypes: getNodeTypes(state), - focusMode: getFocusedModularPipeline(state), - disabledModularPipeline: state.modularPipeline.disabled, - inputOutputDataNodes: getInputOutputNodesForFocusedModularPipeline(state), - modularPipelinesTree: getModularPipelinesTree(state), - isSlicingPipelineApplied: state.slice.apply, -}); - -export const mapDispatchToProps = (dispatch) => ({ - onToggleTagActive: (tagIDs, active) => { - dispatch(toggleTagActive(tagIDs, active)); - }, - onToggleTagFilter: (tagIDs, enabled) => { - dispatch(toggleTagFilter(tagIDs, enabled)); - }, - onToggleModularPipelineActive: (modularPipelineIDs, active) => { - dispatch(toggleModularPipelineActive(modularPipelineIDs, active)); - }, - onToggleModularPipelineDisabled: (modularPipelineIDs, disabled) => { - dispatch(toggleModularPipelineDisabled(modularPipelineIDs, disabled)); - }, - onToggleTypeDisabled: (typeID, disabled) => { - dispatch(toggleTypeDisabled(typeID, disabled)); - }, - onToggleNodeSelected: (nodeID) => { - dispatch(loadNodeData(nodeID)); - }, - onToggleModularPipelineExpanded: (expanded) => { - dispatch(toggleModularPipelinesExpanded(expanded)); - }, - onToggleNodeActive: (nodeID) => { - dispatch(toggleNodeHovered(nodeID)); - }, - onToggleParametersActive: (active) => { - dispatch(toggleParametersHovered(active)); - }, - onToggleNodesDisabled: (nodeIDs, disabled) => { - dispatch(toggleNodesDisabled(nodeIDs, disabled)); - }, - onToggleFocusMode: (modularPipeline) => { - dispatch(toggleFocusMode(modularPipeline)); - }, - onToggleHoveredFocusMode: (active) => { - dispatch(toggleHoveredFocusMode(active)); - }, - onResetSlicePipeline: () => { - dispatch(resetSlicePipeline()); - }, -}); - -export default connect(mapStateToProps, mapDispatchToProps)(NodeListProvider); diff --git a/src/components/node-list/node-list.js b/src/components/node-list/node-list.js deleted file mode 100644 index ad3b7690f5..0000000000 --- a/src/components/node-list/node-list.js +++ /dev/null @@ -1,112 +0,0 @@ -import React from 'react'; -import classnames from 'classnames'; -import { Scrollbars } from 'react-custom-scrollbars-2'; -import SearchList from '../search-list'; -import Filters from '../filters/filters'; -import NodeListTree from './node-list-tree'; -import SplitPanel from '../split-panel'; - -import './styles/node-list.scss'; - -/** - * Scrollable list of toggleable items, with search & filter functionality - */ -const NodeList = ({ - faded, - items, - modularPipelinesTree, - modularPipelinesSearchResult, - groups, - searchValue, - getGroupState, - onUpdateSearchValue, - onGroupToggleChanged, - onToggleGroupCollapsed, - groupCollapsed, - onItemClick, - onItemMouseEnter, - onItemMouseLeave, - onToggleHoveredFocusMode, - onItemChange, - onModularPipelineToggleExpanded, - focusMode, - disabledModularPipeline, - onResetFilter, - isResetFilterActive, -}) => { - return ( -
- - - {({ isResizing, props: { container, panelA, panelB, handle } }) => ( -
-
- -
- -
-
-
-
-
- - - -
-
- )} - -
- ); -}; - -export default NodeList; diff --git a/src/components/nodes-panel/index.js b/src/components/nodes-panel/index.js new file mode 100644 index 0000000000..af6acf42d9 --- /dev/null +++ b/src/components/nodes-panel/index.js @@ -0,0 +1,18 @@ +import React from 'react'; +import NodesPanel from './nodes-panel'; + +import { NodesPanelContextProvider } from './utils/nodes-panel-context'; + +/** + * Acts as a wrapper component that provides the AppContext to the NodesPanel component. + * This ensures that NodesPanel has access to the necessary context values and functions. + */ +const NodesPanelProvider = ({ faded }) => { + return ( + + + + ); +}; + +export default NodesPanelProvider; diff --git a/src/components/nodes-panel/nodes-panel.js b/src/components/nodes-panel/nodes-panel.js new file mode 100644 index 0000000000..8c845108d7 --- /dev/null +++ b/src/components/nodes-panel/nodes-panel.js @@ -0,0 +1,138 @@ +import React, { useContext, useEffect, useState } from 'react'; +import debounce from 'lodash/debounce'; +import classnames from 'classnames'; +import { Scrollbars } from 'react-custom-scrollbars-2'; +import SearchList from '../search-list'; +import Filters from '../filters/filters'; +import NodeListTree from '../node-list-tree/node-list-tree'; +import SplitPanel from '../split-panel'; +import { FiltersContext } from './utils/filters-context'; +import { NodeListContext } from './utils/node-list-context'; +import { getModularPipelinesSearchResult } from '../../selectors/modular-pipelines'; +import { getFiltersSearchResult } from '../../selectors/filtered-node-list-items'; + +/** + * Scrollable list of toggleable items, with search & filter functionality + */ +const NodesPanel = ({ faded }) => { + const [searchValue, updateSearchValue] = useState(''); + + const { + groupCollapsed, + groups, + isResetFilterActive, + items, + handleGroupToggleChanged, + handleResetFilter, + handleToggleGroupCollapsed, + handleFiltersRowClicked, + } = useContext(FiltersContext); + + const { + hoveredNode, + disabledModularPipeline, + expanded, + focusMode, + handleItemMouseEnter, + handleItemMouseLeave, + handleKeyDown, + handleModularPipelineToggleExpanded, + handleNodeListRowChanged, + handleNodeListRowClicked, + handleToggleHoveredFocusMode, + isSlicingPipelineApplied, + modularPipelinesTree, + selectedNodes, + slicedPipeline, + } = useContext(NodeListContext); + + const modularPipelinesSearchResult = searchValue + ? getModularPipelinesSearchResult(modularPipelinesTree, searchValue) + : null; + + const filtersSearchResult = searchValue + ? getFiltersSearchResult(items, searchValue) + : null; + + useEffect(() => { + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }); + + return ( +
+ + + {({ isResizing, props: { container, panelA, panelB, handle } }) => ( +
+
+ +
+ +
+
+
+
+
+ + 0 ? filtersSearchResult : items} + onGroupToggleChanged={handleGroupToggleChanged} + onItemChange={handleFiltersRowClicked} + onResetFilter={handleResetFilter} + onToggleGroupCollapsed={handleToggleGroupCollapsed} + searchValue={searchValue} + /> + +
+
+ )} + +
+ ); +}; + +export default NodesPanel; diff --git a/src/components/node-list/node-list.test.js b/src/components/nodes-panel/nodes-panel.test.js similarity index 91% rename from src/components/node-list/node-list.test.js rename to src/components/nodes-panel/nodes-panel.test.js index be2054c364..a136fbc74e 100644 --- a/src/components/node-list/node-list.test.js +++ b/src/components/nodes-panel/nodes-panel.test.js @@ -12,14 +12,14 @@ import { getTagData } from '../../selectors/tags'; import { mockState, setup } from '../../utils/state.mock'; import IndicatorPartialIcon from '../icons/indicator-partial'; import SplitPanel from '../split-panel'; -import NodeList, { mapStateToProps } from './index'; +import NodesPanel from './index'; jest.mock('lodash/debounce', () => (func) => { func.cancel = jest.fn(); return func; }); -describe('NodeList', () => { +describe('NodesPanel', () => { beforeEach(() => { window.localStorage.clear(); }); @@ -27,7 +27,7 @@ describe('NodeList', () => { it('renders without crashing', () => { const wrapper = setup.mount( - + ); const search = wrapper.find('.pipeline-search-list'); @@ -40,7 +40,7 @@ describe('NodeList', () => { describe('displays nodes matching search value', () => { const wrapper = setup.mount( - + ); @@ -94,7 +94,7 @@ describe('NodeList', () => { it('clears the search input and resets the list when hitting the Escape key', () => { const wrapper = setup.mount( - + ); const searchWrapper = wrapper.find('.pipeline-search-list'); @@ -141,7 +141,7 @@ describe('NodeList', () => { it('displays search results when in focus mode', () => { const wrapper = setup.mount( - + ); const searchWrapper = wrapper.find('.pipeline-search-list'); @@ -198,7 +198,7 @@ describe('NodeList', () => { it('shows full node names when pretty name is turned off', () => { const wrapper = setup.mount( - + , { beforeLayoutActions: [() => toggleIsPrettyName(false)], @@ -215,7 +215,7 @@ describe('NodeList', () => { it('shows formatted node names when pretty name is turned on', () => { const wrapper = setup.mount( - + , { beforeLayoutActions: [() => toggleIsPrettyName(true)], @@ -264,7 +264,7 @@ describe('NodeList', () => { //Parameters are enabled here to override the default behavior const wrapper = setup.mount( - + , { beforeLayoutActions: [() => toggleTypeDisabled('parameters', false)], @@ -284,7 +284,7 @@ describe('NodeList', () => { it('adds a class to tag group item when all tags unchecked', () => { const wrapper = setup.mount( - + ); const uncheckedClass = 'filters-section--all-unchecked'; @@ -299,7 +299,7 @@ describe('NodeList', () => { it('adds a class to the row when a tag row unchecked', () => { const wrapper = setup.mount( - + ); const uncheckedClass = 'toggle-control--icon--unchecked'; @@ -330,7 +330,7 @@ describe('NodeList', () => { it('shows as partially selected when at least one but not all tags selected', () => { const wrapper = setup.mount( - + ); @@ -347,13 +347,13 @@ describe('NodeList', () => { ['Features', 'Preprocessing', 'Split', 'Train'], true ); - expect(partialIcon(wrapper)).toHaveLength(0); + expect(partialIcon(wrapper)).toHaveLength(1); }); it('saves enabled tags in localStorage on selecting a tag on node-list', () => { const wrapper = setup.mount( - + ); changeRows(wrapper, ['Preprocessing'], true); @@ -369,7 +369,7 @@ describe('NodeList', () => { it('renders the correct number of tags in the filter panel', () => { const wrapper = setup.mount( - + ); const nodeList = wrapper.find('.filters-group .node-list-filter-row'); @@ -381,7 +381,7 @@ describe('NodeList', () => { it('renders the correct number of modular pipelines and nodes in the tree sidepanel', () => { const wrapper = setup.mount( - + ); @@ -397,7 +397,7 @@ describe('NodeList', () => { it('renders elements panel, filter panel inside a SplitPanel with a handle', () => { const wrapper = setup.mount( - + ); const split = wrapper.find(SplitPanel); @@ -421,7 +421,7 @@ describe('NodeList', () => { describe('node list element item checkbox', () => { const wrapper = setup.mount( - + ); const checkbox = () => wrapper.find('.node-list-tree-item-row input').at(4); @@ -454,7 +454,7 @@ describe('NodeList', () => { describe('Reset node filters', () => { const wrapper = setup.mount( - + ); @@ -485,30 +485,4 @@ describe('NodeList', () => { expect(window.location.search).not.toContain('tags'); }); }); - - it('maps state to props', () => { - const nodeList = expect.arrayContaining([ - expect.objectContaining({ - disabled: expect.any(Boolean), - disabledNode: expect.any(Boolean), - disabledTag: expect.any(Boolean), - disabledType: expect.any(Boolean), - id: expect.any(String), - name: expect.any(String), - type: expect.any(String), - }), - ]); - const expectedResult = expect.objectContaining({ - tags: expect.any(Object), - nodes: expect.objectContaining({ - data: nodeList, - task: nodeList, - modularPipeline: nodeList, - }), - nodeSelected: expect.any(Object), - nodeTypes: expect.any(Array), - modularPipelinesTree: expect.any(Object), - }); - expect(mapStateToProps(mockState.spaceflights)).toEqual(expectedResult); - }); }); diff --git a/src/components/nodes-panel/utils/filters-context.js b/src/components/nodes-panel/utils/filters-context.js new file mode 100644 index 0000000000..1801552b30 --- /dev/null +++ b/src/components/nodes-panel/utils/filters-context.js @@ -0,0 +1,251 @@ +import React, { useState, useEffect, createContext } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { useGeneratePathname } from '../../../utils/hooks/use-generate-pathname'; +import { loadLocalStorage, saveLocalStorage } from '../../../store/helpers'; + +import { getTagData, getTagNodeCounts } from '../../../selectors/tags'; +import { + getGroupedNodes, + getNodeSelected, + getInputOutputNodesForFocusedModularPipeline, +} from '../../../selectors/nodes'; +import { getNodeTypes } from '../../../selectors/node-types'; +import { getFocusedModularPipeline } from '../../../selectors/modular-pipelines'; + +import { toggleTagFilter } from '../../../actions/tags'; +import { toggleTypeDisabled } from '../../../actions/node-type'; +import { loadNodeData, toggleNodeHovered } from '../../../actions/nodes'; + +import { params, localStorageName, NODE_TYPES } from '../../../config'; +import { + getFilteredItems, + isTagType, + isElementType, + getGroups, +} from '../../../selectors/filtered-node-list-items'; + +// Load the stored state from local storage +const storedState = loadLocalStorage(localStorageName); + +// Custom hook to group useSelector calls +const useFiltersContextSelector = () => { + const dispatch = useDispatch(); + const tags = useSelector(getTagData); + const nodes = useSelector(getGroupedNodes); + const nodeTypes = useSelector(getNodeTypes); + const tagNodeCounts = useSelector(getTagNodeCounts); + const nodeSelected = useSelector(getNodeSelected); + const focusMode = useSelector(getFocusedModularPipeline); + const inputOutputDataNodes = useSelector( + getInputOutputNodesForFocusedModularPipeline + ); + + const onToggleTypeDisabled = (typeID, disabled) => { + dispatch(toggleTypeDisabled(typeID, disabled)); + }; + + const onToggleTagFilter = (tagIDs, enabled) => { + dispatch(toggleTagFilter(tagIDs, enabled)); + }; + + const onToggleNodeSelected = (nodeID) => { + dispatch(loadNodeData(nodeID)); + }; + + const onToggleNodeHovered = (nodeID) => { + dispatch(toggleNodeHovered(nodeID)); + }; + + return { + tags, + nodes, + nodeTypes, + tagNodeCounts, + nodeSelected, + focusMode, + inputOutputDataNodes, + onToggleTypeDisabled, + onToggleTagFilter, + onToggleNodeSelected, + onToggleNodeHovered, + }; +}; + +// Create a context for filters +export const FiltersContext = createContext(); + +export const FiltersContextProvider = ({ children }) => { + const { + tags, + nodes, + nodeTypes, + tagNodeCounts, + nodeSelected, + focusMode, + inputOutputDataNodes, + onToggleTypeDisabled, + onToggleTagFilter, + onToggleNodeSelected, + onToggleNodeHovered, + } = useFiltersContextSelector(); + + const [groupCollapsed, setGroupCollapsed] = useState( + storedState.groupsCollapsed || {} + ); + const [isResetFilterActive, setIsResetFilterActive] = useState(false); + + // Helper function to check if NodeTypes are modified + const hasModifiedNodeTypes = (nodeTypes) => { + return nodeTypes.some( + (item) => NODE_TYPES[item.id]?.defaultState !== item.disabled + ); + }; + + // Effect to update the reset filter button status based on node types and tags + useEffect(() => { + const isNodeTypeModified = hasModifiedNodeTypes(nodeTypes); + const isNodeTagModified = tags.some((tag) => tag.enabled); + setIsResetFilterActive(isNodeTypeModified || isNodeTagModified); + }, [tags, nodeTypes]); + + const { + toUpdateUrlParamsOnResetFilter, + toUpdateUrlParamsOnFilter, + toSetQueryParam, + } = useGeneratePathname(); + + // Function to reset applied filters to default + const handleResetFilter = () => { + onToggleTypeDisabled({ task: false, data: false, parameters: true }); + onToggleTagFilter( + tags.map((item) => item.id), + false + ); + toUpdateUrlParamsOnResetFilter(); + }; + + // Function to collapse/expand node group of filters + const handleToggleGroupCollapsed = (groupID) => { + const updatedGroupCollapsed = { + ...groupCollapsed, + [groupID]: !groupCollapsed[groupID], + }; + setGroupCollapsed(updatedGroupCollapsed); + saveLocalStorage(localStorageName, { + groupsCollapsed: updatedGroupCollapsed, + }); + }; + + const items = getFilteredItems({ + nodes, + tags, + nodeTypes, + tagNodeCounts, + nodeSelected, + searchValue: '', + focusMode, + inputOutputDataNodes, + }); + + const groups = getGroups({ items }); + + // Function to get existing values from URL query parameters + const getExistingValuesFromUrlQueryParams = (paramName, searchParams) => { + const paramValues = searchParams.get(paramName); + return new Set(paramValues ? paramValues.split(',') : []); + }; + + // Function to update URL query parameters when a filter is applied + const handleUrlParamsUpdateOnFilter = (item) => { + const searchParams = new URLSearchParams(window.location.search); + const paramName = isElementType(item.type) ? params.types : params.tags; + const existingValues = getExistingValuesFromUrlQueryParams( + paramName, + searchParams + ); + toUpdateUrlParamsOnFilter(item, paramName, existingValues); + }; + + // Function to update URL query parameters when a filter group is clicked + const handleUrlParamsUpdateOnGroupFilter = ( + groupType, + groupItems, + groupItemsDisabled + ) => { + if (groupItemsDisabled) { + groupItems.forEach((item) => { + handleUrlParamsUpdateOnFilter(item); + }); + } else { + const paramName = isElementType(groupType) ? params.types : params.tags; + toSetQueryParam(paramName, []); + } + }; + + // Function to handle group toggle change + const handleGroupToggleChanged = (groupType) => { + const groupItems = items[groupType] || []; + const groupItemsDisabled = groupItems.every( + (groupItem) => !groupItem.checked + ); + + handleUrlParamsUpdateOnGroupFilter( + groupType, + groupItems, + groupItemsDisabled + ); + + if (isTagType(groupType)) { + onToggleTagFilter( + groupItems.map((item) => item.id), + groupItemsDisabled + ); + } else if (isElementType(groupType)) { + onToggleTypeDisabled( + groupItems.reduce( + (state, item) => ({ ...state, [item.id]: !groupItemsDisabled }), + {} + ) + ); + } + }; + + const onGroupItemChange = (item, wasChecked) => { + // Toggle the group + if (isTagType(item.type)) { + onToggleTagFilter(item.id, !wasChecked); + } else if (isElementType(item.type)) { + onToggleTypeDisabled({ [item.id]: wasChecked }); + } + + // Reset node selection + onToggleNodeSelected(null); + onToggleNodeHovered(null); + }; + + const handleFiltersRowClicked = (event, item) => { + onGroupItemChange(item, item.checked); + handleUrlParamsUpdateOnFilter(item); + + // to prevent page reload on form submission + event.preventDefault(); + }; + + return ( + + {children} + + ); +}; diff --git a/src/components/nodes-panel/utils/node-list-context.js b/src/components/nodes-panel/utils/node-list-context.js new file mode 100644 index 0000000000..f2adc4afd5 --- /dev/null +++ b/src/components/nodes-panel/utils/node-list-context.js @@ -0,0 +1,226 @@ +import React, { createContext } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { useGeneratePathname } from '../../../utils/hooks/use-generate-pathname'; + +import { + getFocusedModularPipeline, + getModularPipelinesTree, +} from '../../../selectors/modular-pipelines'; +import { isModularPipelineType } from '../../../selectors/node-types'; +import { getNodeSelected } from '../../../selectors/nodes'; +import { getSlicedPipeline } from '../../../selectors/sliced-pipeline'; + +import { + toggleModularPipelinesExpanded, + toggleModularPipelineActive, + toggleModularPipelineDisabled, +} from '../../../actions/modular-pipelines'; +import { toggleFocusMode, toggleHoveredFocusMode } from '../../../actions'; +import { + loadNodeData, + toggleNodeHovered, + toggleNodesDisabled, +} from '../../../actions/nodes'; +import { resetSlicePipeline } from '../../../actions/slice'; + +// Custom hook to group useSelector calls +const useNodeListContextSelector = () => { + const dispatch = useDispatch(); + const hoveredNode = useSelector((state) => state.node.hovered); + const selectedNodes = useSelector(getNodeSelected); + const expanded = useSelector((state) => state.modularPipeline.expanded); + const slicedPipeline = useSelector(getSlicedPipeline); + const modularPipelinesTree = useSelector(getModularPipelinesTree); + const isSlicingPipelineApplied = useSelector((state) => state.slice.apply); + const focusMode = useSelector(getFocusedModularPipeline); + const disabledModularPipeline = useSelector( + (state) => state.modularPipeline.disabled + ); + + const onToggleFocusMode = (modularPipeline) => { + dispatch(toggleFocusMode(modularPipeline)); + }; + const onToggleHoveredFocusMode = (active) => { + dispatch(toggleHoveredFocusMode(active)); + }; + const onToggleNodeSelected = (nodeID) => { + dispatch(loadNodeData(nodeID)); + }; + const onToggleNodeHovered = (nodeID) => { + dispatch(toggleNodeHovered(nodeID)); + }; + const onToggleNodesDisabled = (nodeIDs, disabled) => { + dispatch(toggleNodesDisabled(nodeIDs, disabled)); + }; + const onToggleModularPipelineExpanded = (expanded) => { + dispatch(toggleModularPipelinesExpanded(expanded)); + }; + const onToggleModularPipelineDisabled = (modularPipelineIDs, disabled) => { + dispatch(toggleModularPipelineDisabled(modularPipelineIDs, disabled)); + }; + const onToggleModularPipelineActive = (modularPipelineIDs, active) => { + dispatch(toggleModularPipelineActive(modularPipelineIDs, active)); + }; + const onResetSlicePipeline = () => { + dispatch(resetSlicePipeline()); + }; + + return { + disabledModularPipeline, + expanded, + focusMode, + hoveredNode, + isSlicingPipelineApplied, + modularPipelinesTree, + selectedNodes, + slicedPipeline, + onResetSlicePipeline, + onToggleFocusMode, + onToggleHoveredFocusMode, + onToggleModularPipelineActive, + onToggleModularPipelineDisabled, + onToggleModularPipelineExpanded, + onToggleNodeHovered, + onToggleNodesDisabled, + onToggleNodeSelected, + }; +}; + +export const NodeListContext = createContext(); + +export const NodeListContextProvider = ({ children }) => { + const { + disabledModularPipeline, + expanded, + focusMode, + hoveredNode, + isSlicingPipelineApplied, + modularPipelinesTree, + selectedNodes, + slicedPipeline, + onResetSlicePipeline, + onToggleFocusMode, + onToggleHoveredFocusMode, + onToggleModularPipelineActive, + onToggleModularPipelineDisabled, + onToggleModularPipelineExpanded, + onToggleNodeHovered, + onToggleNodesDisabled, + onToggleNodeSelected, + } = useNodeListContextSelector(); + const { toSelectedPipeline, toSelectedNode, toFocusedModularPipeline } = + useGeneratePathname(); + + // Handle row click in the node list + const handleNodeListRowClicked = (event, item) => { + if (isModularPipelineType(item.type)) { + onToggleNodeSelected(null); + } else { + if (item.faded || item.selected) { + onToggleNodeSelected(null); + toSelectedPipeline(); + } else { + onToggleNodeSelected(item.id); + toSelectedNode(item); + // Reset the pipeline slicing filters if no slicing is currently applied + if (!isSlicingPipelineApplied) { + onResetSlicePipeline(); + } + } + } + + // Prevent page reload on form submission + event.preventDefault(); + }; + + // Handle changes in the node list row + const handleNodeListRowChanged = (item, checked, clickedIconType) => { + if (isModularPipelineType(item.type)) { + if (clickedIconType === 'focus') { + if (focusMode === null) { + onToggleFocusMode(item); + toFocusedModularPipeline(item); + + if (disabledModularPipeline[item.id]) { + onToggleModularPipelineDisabled([item.id], checked); + } + } else { + onToggleFocusMode(null); + toSelectedPipeline(); + } + } else { + onToggleModularPipelineDisabled([item.id], checked); + onToggleModularPipelineActive([item.id], false); + } + } else { + if (checked) { + onToggleNodeHovered(null); + } + + onToggleNodesDisabled([item.id], checked); + } + // reset the node data + onToggleNodeSelected(null); + onToggleNodeHovered(null); + }; + + // Handle mouse enter event on an item + const handleItemMouseEnter = (item) => { + if (isModularPipelineType(item.type)) { + onToggleModularPipelineActive(item.id, true); + return; + } + + if (item.visible) { + onToggleNodeHovered(item.id); + } + }; + + // Handle mouse leave event on an item + const handleItemMouseLeave = (item) => { + if (isModularPipelineType(item.type)) { + onToggleModularPipelineActive(item.id, false); + return; + } + if (item.visible) { + onToggleNodeHovered(null); + } + }; + + // Toggle hovered focus mode + const handleToggleHoveredFocusMode = (active) => { + onToggleHoveredFocusMode(active); + }; + + // Deselect node on Escape key + const handleKeyDown = (event) => { + if (event.keyCode === 27) { + onToggleNodeSelected(null); + } + }; + + return ( + + {children} + + ); +}; diff --git a/src/components/nodes-panel/utils/nodes-panel-context.js b/src/components/nodes-panel/utils/nodes-panel-context.js new file mode 100644 index 0000000000..aa32e99d3f --- /dev/null +++ b/src/components/nodes-panel/utils/nodes-panel-context.js @@ -0,0 +1,11 @@ +import React from 'react'; +import { NodeListContextProvider } from './node-list-context'; +import { FiltersContextProvider } from './filters-context'; + +export const NodesPanelContextProvider = ({ children }) => { + return ( + + {children} + + ); +}; diff --git a/src/components/sidebar/sidebar.js b/src/components/sidebar/sidebar.js index 653a104ba7..73b6fbcc14 100644 --- a/src/components/sidebar/sidebar.js +++ b/src/components/sidebar/sidebar.js @@ -5,7 +5,7 @@ import ExperimentPrimaryToolbar from '../experiment-tracking/experiment-primary- import FlowchartPrimaryToolbar from '../flowchart-primary-toolbar'; import MiniMap from '../minimap'; import MiniMapToolbar from '../minimap-toolbar'; -import NodeList from '../node-list'; +import NodesPanel from '../nodes-panel'; import PipelineList from '../pipeline-list'; import RunsList from '../experiment-tracking/runs-list'; @@ -88,7 +88,7 @@ export const Sidebar = ({ >
- +