diff --git a/RELEASE.md b/RELEASE.md
index 8d913321f7..0a15a496ff 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -14,7 +14,6 @@ Please follow the established format:
- Introduce `behaviour` prop object with `reFocus` prop (#2161)
## Bug fixes and other changes
-
- Improve `kedro viz build` usage documentation (#2126)
- Fix unserializable parameters value (#2122)
- Replace `watchgod` library with `watchfiles` and improve autoreload file watching filter (#2134)
@@ -22,6 +21,7 @@ Please follow the established format:
- Enable SQLite WAL mode for Azure ML to fix database locking issues (#2131)
- Replace `flake8`, `isort`, `pylint` and `black` by `ruff` (#2149)
- Refactor `DatasetStatsHook` to avoid showing error when dataset doesn't have file size info (#2174)
+- Refactor `node-list-tree` component. (#2193)
- Fix 404 error when accessing the experiment tracking page on the demo site (#2179)
- Add check for port availability before starting Kedro Viz to prevent unintended browser redirects when the port is already in use (#2176)
- Include Kedro Viz version in telemetry.. (#2194)
diff --git a/cypress/tests/ui/flowchart/flowchart.cy.js b/cypress/tests/ui/flowchart/flowchart.cy.js
index a2eee6fbde..f3f241dcb3 100644
--- a/cypress/tests/ui/flowchart/flowchart.cy.js
+++ b/cypress/tests/ui/flowchart/flowchart.cy.js
@@ -70,7 +70,7 @@ describe('Flowchart DAG', () => {
const nodeToToggleText = 'Parameters';
// Alias
- cy.get(`.pipeline-nodelist__row__checkbox[name=${nodeToToggleText}]`).as(
+ cy.get(`.toggle-control__checkbox[name=${nodeToToggleText}]`).as(
'nodeToToggle'
);
diff --git a/cypress/tests/ui/flowchart/menu.cy.js b/cypress/tests/ui/flowchart/menu.cy.js
index deb6d38f81..5571528339 100644
--- a/cypress/tests/ui/flowchart/menu.cy.js
+++ b/cypress/tests/ui/flowchart/menu.cy.js
@@ -41,7 +41,7 @@ describe('Flowchart Menu', () => {
});
// Pipeline Label in the Menu
- cy.get('.pipeline-nodelist__row__label')
+ cy.get('.row-text__label')
.first()
.invoke('text')
.should((pipelineLabel) => {
@@ -57,7 +57,7 @@ describe('Flowchart Menu', () => {
cy.get('.search-input__field').type(searchInput, { force: true });
// Pipeline Label in the Menu
- cy.get('.pipeline-nodelist__row__label')
+ cy.get('.row-text__label')
.first()
.invoke('text')
.should((pipelineLabel) => {
@@ -72,7 +72,7 @@ describe('Flowchart Menu', () => {
// Action
cy.get(
- `.MuiTreeItem-label > .pipeline-nodelist__row > [data-test=nodelist-data-${nodeToClickText}]`
+ `.MuiTreeItem-label > .node-list-tree-item-row > [data-test=node-list-tree-item--row--${nodeToClickText}]`
)
.should('exist')
.as('nodeToClick');
@@ -91,7 +91,7 @@ describe('Flowchart Menu', () => {
// Action
cy.get(
- `.MuiTreeItem-label > .pipeline-nodelist__row > [data-test=nodelist-data-${nodeToHighlightText}]`
+ `.MuiTreeItem-label > .node-list-tree-item-row > [data-test=node-list-tree-item--row--${nodeToHighlightText}]`
)
.should('exist')
.as('nodeToHighlight');
@@ -108,7 +108,7 @@ describe('Flowchart Menu', () => {
const nodeToToggleText = 'Companies';
// Alias
- cy.get(`.pipeline-nodelist__row__checkbox[name=${nodeToToggleText}]`, {
+ cy.get(`.toggle-control__checkbox[name=${nodeToToggleText}]`, {
timeout: 5000,
}).as('nodeToToggle');
@@ -121,7 +121,7 @@ describe('Flowchart Menu', () => {
// Assert after action
cy.__checkForText__(
- `[data-test=nodelist-data-${nodeToToggleText}] > .pipeline-nodelist__row__label--faded`,
+ `[data-test=node-list-tree-item--row--${nodeToToggleText}] > .row-text__label--faded`,
nodeToToggleText
);
cy.get('.pipeline-node__text').should('not.contain', nodeToToggleText);
@@ -137,7 +137,7 @@ describe('Flowchart Menu', () => {
// Action
cy.get(
- `[for=${nodeToFocusText}-focus] > .pipeline-nodelist__row__icon`
+ `[for=feature_engineering-focus]`
).click();
// Assert after action
@@ -161,34 +161,34 @@ describe('Flowchart Menu', () => {
const visibleRowLabel = 'Companies';
// Alias
- cy.get(`.pipeline-nodelist__row__checkbox[name=${nodeToToggleText}]`).as(
+ cy.get(`.toggle-control__checkbox[name=${nodeToToggleText}]`).as(
'nodeToToggle'
);
// Assert before action
cy.get('@nodeToToggle').should('be.checked');
cy.get(
- `[data-test=nodelist-data-${visibleRowLabel}] > .pipeline-nodelist__row__label`
+ `[data-test=node-list-tree-item--row--${visibleRowLabel}] > .row-text__label`
)
- .should('not.have.class', 'pipeline-nodelist__row__label--faded')
- .should('not.have.class', 'pipeline-nodelist__row__label--disabled');
+ .should('not.have.class', 'row-text__label--faded')
+ .should('not.have.class', 'row-text__label--disabled');
// Action
cy.get('@nodeToToggle').uncheck({ force: true });
// Assert after action
cy.get(
- `[data-test=nodelist-data-${visibleRowLabel}] > .pipeline-nodelist__row__label`
+ `[data-test=node-list-tree-item--row--${visibleRowLabel}] > .row-text__label`
)
- .should('have.class', 'pipeline-nodelist__row__label--faded')
- .should('have.class', 'pipeline-nodelist__row__label--disabled');
+ .should('have.class', 'row-text__label--faded')
+ .should('have.class', 'row-text__label--disabled');
});
it('verifies that after checking node type URL should be updated with correct query params', () => {
const nodeToToggleText = 'Parameters';
// Alias
- cy.get(`.pipeline-nodelist__row__checkbox[name=${nodeToToggleText}]`).as(
+ cy.get(`.toggle-control__checkbox[name=${nodeToToggleText}]`).as(
'nodeToToggle'
);
@@ -207,7 +207,7 @@ describe('Flowchart Menu', () => {
cy.visit(`/?tags=${visibleRowLabel}`);
// Alias
- cy.get(`.pipeline-nodelist__row__checkbox[name=${visibleRowLabel}]`).as(
+ cy.get(`.toggle-control__checkbox[name=${visibleRowLabel}]`).as(
'nodeToToggle'
);
@@ -220,7 +220,7 @@ describe('Flowchart Menu', () => {
cy.visit('/?types=datasets');
// Alias
- cy.get(`.pipeline-nodelist__row__checkbox[name=${visibleRowLabel}]`).as(
+ cy.get(`.toggle-control__checkbox[name=${visibleRowLabel}]`).as(
'nodeToToggle'
);
diff --git a/cypress/tests/ui/toolbar/global-toolbar.cy.js b/cypress/tests/ui/toolbar/global-toolbar.cy.js
index a8f6434968..64971aa1d7 100644
--- a/cypress/tests/ui/toolbar/global-toolbar.cy.js
+++ b/cypress/tests/ui/toolbar/global-toolbar.cy.js
@@ -81,14 +81,14 @@ describe('Global Toolbar', () => {
cy.get('@isPrettyNameCheckbox').should('be.checked');
// Menu
- cy.get(`[data-test="nodelist-modularPipeline-${prettifyName(modularPipelineText)}"]`).click();
- cy.get(`[data-test="nodelist-${nodeNameType}-${prettyNodeNameText}"]`).should('exist');
+ cy.get(`[data-test="node-list-tree-item--row--${prettifyName(modularPipelineText)}"]`).click();
+ cy.get(`[data-test="node-list-tree-item--row--${prettyNodeNameText}"]`).should('exist');
// Flowchart
cy.get('.pipeline-node__text').should('contain', prettyNodeNameText);
// Metadata
- cy.get(`[data-test="nodelist-${nodeNameType}-${prettyNodeNameText}"]`).click({ force: true });
+ cy.get(`[data-test="node-list-tree-item--row--${prettyNodeNameText}"]`).click({ force: true });
cy.get('.pipeline-metadata__title').should(
'have.text',
prettyNodeNameText
@@ -106,7 +106,7 @@ describe('Global Toolbar', () => {
// Assert after action
cy.__waitForPageLoad__(() => {
// Menu
- cy.get(`[data-test="nodelist-${nodeNameType}-${originalNodeNameText}"]`).should('exist');
+ cy.get(`[data-test="node-list-tree-item--row--${originalNodeNameText}"]`).should('exist');
// Flowchart
cy.get('.pipeline-node__text').should('contain', originalNodeNameText);
diff --git a/src/components/filters/filters-group/filters-group.js b/src/components/filters/filters-group/filters-group.js
new file mode 100644
index 0000000000..2edb9af1eb
--- /dev/null
+++ b/src/components/filters/filters-group/filters-group.js
@@ -0,0 +1,49 @@
+import React from 'react';
+import classnames from 'classnames';
+import FiltersRow from '../filters-row/filters-row';
+import { nodeListRowHeight } from '../../../config';
+import LazyList from '../../lazy-list';
+import { getDataTestAttribute } from '../../../utils/get-data-test-attribute';
+
+import './filters-group.scss';
+
+/** A group collection of FiltersRow */
+const FiltersGroup = ({ items = [], group, collapsed, onItemChange }) => (
+ (end - start) * nodeListRowHeight}
+ total={items.length}
+ >
+ {({ start, end, listRef, listStyle }) => (
+
+ {items.slice(start, end).map((item) => (
+ onItemChange(e, item)}
+ onClick={(e) => onItemChange(e, item)}
+ parentClassName={'node-list-filter-row'}
+ visible={item.visible}
+ indicatorIcon={item.visibleIcon}
+ />
+ ))}
+
+ )}
+
+);
+
+export default FiltersGroup;
diff --git a/src/components/filters/filters-group/filters-group.scss b/src/components/filters/filters-group/filters-group.scss
new file mode 100644
index 0000000000..c36a015442
--- /dev/null
+++ b/src/components/filters/filters-group/filters-group.scss
@@ -0,0 +1,15 @@
+@use '../../../styles/variables' as var;
+@use '../../node-list-tree/styles/variables';
+
+.filters-group {
+ list-style: none;
+ padding: 0;
+ margin: 0 0 1.2em;
+
+ // Avoid placeholder fade leaking out for small lists
+ overflow: hidden;
+
+ &--closed {
+ display: none;
+ }
+}
diff --git a/src/components/filters/filters-group/filters-group.test.js b/src/components/filters/filters-group/filters-group.test.js
new file mode 100644
index 0000000000..7f91be5ca0
--- /dev/null
+++ b/src/components/filters/filters-group/filters-group.test.js
@@ -0,0 +1,34 @@
+import React from 'react';
+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 '../../../selectors/filtered-node-list-items';
+
+describe('FiltersGroup Component', () => {
+ const mockProps = () => {
+ const items = getGroupedNodes(mockState.spaceflights);
+ const nodeTypes = getNodeTypes(mockState.spaceflights);
+ const groups = getGroups({ nodeTypes, items });
+ return { group: groups['tags'], items: [] };
+ };
+
+ it('renders without throwing', () => {
+ expect(() => setup.mount()).not.toThrow();
+ });
+ it('adds class when collapsed prop true', () => {
+ const wrapper = setup.mount(
+
+ );
+ const children = wrapper.find('.filters-group');
+ expect(children.hasClass('filters-group--closed')).toBe(true);
+ });
+
+ it('removes class when collapsed prop false', () => {
+ const wrapper = setup.mount(
+
+ );
+ const children = wrapper.find('.filters-group');
+ expect(children.hasClass('filters-group--closed')).toBe(false);
+ });
+});
diff --git a/src/components/filters/filters-row/filters-row.js b/src/components/filters/filters-row/filters-row.js
new file mode 100755
index 0000000000..f854100608
--- /dev/null
+++ b/src/components/filters/filters-row/filters-row.js
@@ -0,0 +1,68 @@
+import React from 'react';
+import classnames from 'classnames';
+import IndicatorIcon from '../../icons/indicator';
+import OffIndicatorIcon from '../../icons/indicator-off';
+import { ToggleControl } from '../../ui/toggle-control/toggle-control';
+import { RowText } from '../../ui/row-text/row-text';
+
+import './filters-row.scss';
+
+const FiltersRow = ({
+ allUnchecked,
+ checked,
+ children,
+ container: ContainerWrapper,
+ count,
+ dataTest,
+ id,
+ indicatorIcon = IndicatorIcon,
+ kind,
+ label,
+ name,
+ offIndicatorIcon = OffIndicatorIcon,
+ onChange,
+ onClick,
+ parentClassName,
+ visible,
+}) => {
+ const Icon = checked ? indicatorIcon : offIndicatorIcon;
+
+ return (
+
+
+
+ {count}
+
+
+ {children}
+
+ );
+};
+
+export default FiltersRow;
diff --git a/src/components/filters/filters-row/filters-row.scss b/src/components/filters/filters-row/filters-row.scss
new file mode 100644
index 0000000000..3f25875237
--- /dev/null
+++ b/src/components/filters/filters-row/filters-row.scss
@@ -0,0 +1,54 @@
+@use '../../../styles/variables' as var;
+@use '../../node-list-tree/styles/variables';
+
+.MuiTreeItem-iconContainer svg {
+ z-index: var.$zindex-MuiTreeItem-icon;
+}
+
+.filter-row {
+ align-items: center;
+ background-color: initial;
+ cursor: default;
+ display: flex;
+ height: 32px;
+ position: relative;
+
+ &--kind-filter {
+ padding: 0 variables.$row-offset-right 0 variables.$row-offset-left;
+ }
+
+ &--visible:hover {
+ background-color: var(--color-nodelist-row-active);
+ }
+}
+
+.filter-row__count {
+ display: inline-block;
+ flex-shrink: 0;
+ width: 2.2em;
+ margin: 0 0.7em 0.1em auto;
+ overflow: hidden;
+ font-size: 1.16em;
+ text-align: right;
+ text-overflow: ellipsis;
+ opacity: 0.75;
+ user-select: none;
+
+ .filter-row--unchecked & {
+ opacity: 0.55;
+ }
+}
+
+.filter-row--unchecked {
+ // Fade row text when unchecked
+ .row-text__label--kind-filter {
+ opacity: 0.55;
+ }
+
+ // Brighter row text when unchecked and hovered
+ &:hover {
+ .row-text__label--kind-filter {
+ opacity: 0.8;
+ }
+ }
+}
diff --git a/src/components/filters/filters-row/filters-row.test.js b/src/components/filters/filters-row/filters-row.test.js
new file mode 100644
index 0000000000..1660b20f14
--- /dev/null
+++ b/src/components/filters/filters-row/filters-row.test.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import FiltersRow from './filters-row';
+
+describe('FiltersRow Component', () => {
+ it('renders without crashing', () => {
+ const wrapper = mount();
+ expect(wrapper.exists()).toBe(true);
+ });
+
+ it('renders correct visible classnames', () => {
+ const wrapper = mount();
+ expect(wrapper.find('.filter-row').hasClass('filter-row--visible')).toBe(
+ true
+ );
+ });
+
+ it('renders correct unchecked classnames', () => {
+ const wrapper = mount();
+ expect(wrapper.find('.filter-row').hasClass('filter-row--unchecked')).toBe(
+ true
+ );
+ });
+});
diff --git a/src/components/filters/filters-section-heading/filters-section-heading.js b/src/components/filters/filters-section-heading/filters-section-heading.js
new file mode 100644
index 0000000000..f63cba95d0
--- /dev/null
+++ b/src/components/filters/filters-section-heading/filters-section-heading.js
@@ -0,0 +1,48 @@
+import React from 'react';
+import classnames from 'classnames';
+import FiltersRow from '../filters-row/filters-row';
+
+import './filters-section-heading.scss';
+
+const FiltersSectionHeading = ({
+ group,
+ collapsed,
+ groupItems,
+ onGroupToggleChanged,
+ onToggleGroupCollapsed,
+}) => {
+ const { id, kind, name, allUnchecked, checked, invisibleIcon, visibleIcon } =
+ group;
+ const disabled = groupItems.length === 0;
+
+ return (
+
+ {
+ onGroupToggleChanged(id, !e.target.checked);
+ }}
+ indicatorIcon={visibleIcon}
+ >
+
+
+ );
+};
+
+export default FiltersSectionHeading;
diff --git a/src/components/filters/filters-section-heading/filters-section-heading.scss b/src/components/filters/filters-section-heading/filters-section-heading.scss
new file mode 100644
index 0000000000..cdd1ea8dc1
--- /dev/null
+++ b/src/components/filters/filters-section-heading/filters-section-heading.scss
@@ -0,0 +1,62 @@
+@use '../../../styles/variables' as var;
+@use '../../node-list-tree/styles/variables';
+
+.filters-section-heading {
+ background: var(--color-nodelist-filter-panel);
+ margin: 0;
+ position: sticky;
+ top: 0;
+
+ // Avoid pixel gap above when scrolling.
+ transform: translateY(-1px);
+ z-index: var.$zindex-nodelist-heading;
+
+ .row-text .row-text__label {
+ font-size: 1.3em;
+ opacity: 0.65;
+ }
+}
+
+.filters-section-heading__toggle-btn {
+ width: variables.$toggle-size;
+ height: variables.$toggle-size;
+ padding: 0;
+ color: var(--color-default-alt);
+ font-size: inherit;
+ font-family: inherit;
+ line-height: 1em;
+ text-align: center;
+ background: none;
+ border: none;
+ border-radius: 50%;
+ box-shadow: none;
+ cursor: pointer;
+ transition: transform ease 0.1s;
+
+ &:focus {
+ outline: none;
+
+ [data-whatintent='keyboard'] & {
+ box-shadow: 0 0 0 3px var.$blue-300 inset;
+ }
+ }
+
+ &::before {
+ font-size: 1.8em;
+ opacity: 0.55;
+ content: '▾';
+ }
+
+ &:hover::before {
+ opacity: 1;
+ }
+
+ &--alt {
+ transform: rotate(90deg);
+ }
+
+ &--disabled {
+ color: var.$black-400;
+ transform: rotate(90deg);
+ }
+}
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
new file mode 100755
index 0000000000..84c57d603d
--- /dev/null
+++ b/src/components/filters/filters-section-heading/filters-section-heading.test.js
@@ -0,0 +1,53 @@
+import React from 'react';
+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 '../../../selectors/filtered-node-list-items';
+
+describe('FiltersSectionHeading', () => {
+ const mockProps = () => {
+ const items = getGroupedNodes(mockState.spaceflights);
+ const nodeTypes = getNodeTypes(mockState.spaceflights);
+ const groups = getGroups({ nodeTypes, items });
+ return { group: groups['elementType'], groupItems: [] };
+ };
+
+ it('renders without throwing', () => {
+ expect(() =>
+ setup.mount()
+ ).not.toThrow();
+ });
+
+ it('handles collapse button click events', () => {
+ const onToggleCollapsed = jest.fn();
+ const wrapper = setup.mount(
+
+ );
+ wrapper.find('.filters-section-heading__toggle-btn').simulate('click');
+ expect(() => onToggleCollapsed.mock.calls.length.toEqual(1)).toThrow();
+ });
+
+ it('adds class when collapsed prop true', () => {
+ const wrapper = setup.mount(
+
+ );
+ const children = wrapper.find('.filters-section-heading__toggle-btn');
+ expect(children.hasClass('filters-section-heading__toggle-btn--alt')).toBe(
+ true
+ );
+ });
+
+ it('adds class when disabled prop true', () => {
+ const wrapper = setup.mount(
+
+ );
+ const children = wrapper.find('.filters-section-heading__toggle-btn');
+ expect(
+ children.hasClass('filters-section-heading__toggle-btn--disabled')
+ ).toBe(true);
+ });
+});
diff --git a/src/components/filters/filters-section/filters-section.js b/src/components/filters/filters-section/filters-section.js
new file mode 100644
index 0000000000..808aee952e
--- /dev/null
+++ b/src/components/filters/filters-section/filters-section.js
@@ -0,0 +1,46 @@
+import React from 'react';
+import classnames from 'classnames';
+import FiltersSectionHeading from '../filters-section-heading/filters-section-heading';
+import FiltersGroup from '../filters-group/filters-group';
+
+import './filters-section.scss';
+
+/** Represents a section within the filters. */
+const FiltersSection = ({
+ group,
+ groupCollapsed,
+ items,
+ onGroupToggleChanged,
+ onItemChange,
+ onToggleGroupCollapsed,
+ searchValue,
+}) => {
+ const { id, allUnchecked } = group;
+ const collapsed = Boolean(searchValue) ? false : groupCollapsed[id];
+ const groupItems = items[id] || [];
+
+ return (
+
+
+
+
+ );
+};
+
+export default FiltersSection;
diff --git a/src/components/filters/filters-section/filters-section.scss b/src/components/filters/filters-section/filters-section.scss
new file mode 100644
index 0000000000..a0f90a9516
--- /dev/null
+++ b/src/components/filters/filters-section/filters-section.scss
@@ -0,0 +1,6 @@
+// Bright row text when the parent groups are all unchecked
+.filters-section--all-unchecked {
+ .row-text__label--kind-filter {
+ opacity: 1;
+ }
+}
diff --git a/src/components/filters/filters-section/filters-section.test.js b/src/components/filters/filters-section/filters-section.test.js
new file mode 100755
index 0000000000..6c476e32cd
--- /dev/null
+++ b/src/components/filters/filters-section/filters-section.test.js
@@ -0,0 +1,26 @@
+import React from 'react';
+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 '../../../selectors/filtered-node-list-items';
+
+describe('FiltersSection Component', () => {
+ const mockProps = () => {
+ const items = getGroupedNodes(mockState.spaceflights);
+ const nodeTypes = getNodeTypes(mockState.spaceflights);
+ const groups = getGroups({ nodeTypes, items });
+ return { items, group: groups['elementType'], groupCollapsed: {} };
+ };
+
+ it('renders without throwing', () => {
+ expect(() =>
+ setup.mount()
+ ).not.toThrow();
+ });
+ it('adds clas all-uncheckes when allUnchecked prop true', () => {
+ const wrapper = setup.mount();
+ const children = wrapper.find('.filters-section');
+ expect(children.hasClass('filters-section--all-unchecked')).toBe(true);
+ });
+});
diff --git a/src/components/filters/filters.js b/src/components/filters/filters.js
new file mode 100644
index 0000000000..2797ebd33c
--- /dev/null
+++ b/src/components/filters/filters.js
@@ -0,0 +1,51 @@
+import React from 'react';
+import FiltersSection from './filters-section/filters-section';
+
+import './filters.scss';
+
+const Filters = ({
+ groupCollapsed,
+ groups,
+ isResetFilterActive,
+ items,
+ onGroupToggleChanged,
+ onItemChange,
+ onResetFilter,
+ onToggleGroupCollapsed,
+ searchValue,
+}) => {
+ return (
+ <>
+
+
+ Filters
+
+
+
+
+ {Object.values(groups).map((group) => {
+ return (
+
+ );
+ })}
+
+ >
+ );
+};
+
+export default Filters;
diff --git a/src/components/node-list/styles/_section.scss b/src/components/filters/filters.scss
similarity index 71%
rename from src/components/node-list/styles/_section.scss
rename to src/components/filters/filters.scss
index a854ce8ee8..03e5922663 100644
--- a/src/components/node-list/styles/_section.scss
+++ b/src/components/filters/filters.scss
@@ -1,6 +1,6 @@
-@use './variables';
-@use '../../../styles/extends';
-@use '../../../styles/variables' as colors;
+@use '../node-list-tree/styles/variables';
+@use '../../styles/extends';
+@use '../../styles/variables' as colors;
.kui-theme--light {
--color-text-reset: #{colors.$black-800};
@@ -10,14 +10,20 @@
--color-text-reset: #{colors.$white-600};
}
-.pipeline-nodelist-section__filters {
+.filters__section-wrapper {
+ margin: 0;
+ padding: 4px 0 28px;
+ list-style: none;
+}
+
+.filters__header {
display: flex;
justify-content: space-between;
align-items: center;
margin: 6px (variables.$section-title-padding-x + 0.92) 12px
(variables.$section-title-padding-x + 1.06);
- .pipeline-nodelist-section__title {
+ .filters__title {
font-weight: normal;
font-size: 1.6em;
opacity: 0.55;
@@ -25,7 +31,7 @@
margin: 0;
}
- .pipeline-nodelist-section__reset-filter {
+ .filters__reset-button {
@extend %button;
font-size: 1.3em;
diff --git a/src/components/filters/filters.test.js b/src/components/filters/filters.test.js
new file mode 100644
index 0000000000..4b1ac0198b
--- /dev/null
+++ b/src/components/filters/filters.test.js
@@ -0,0 +1,61 @@
+import React from 'react';
+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 '../../selectors/filtered-node-list-items';
+
+describe('Filters', () => {
+ const mockProps = () => {
+ const items = getGroupedNodes(mockState.spaceflights);
+ const nodeTypes = getNodeTypes(mockState.spaceflights);
+ const groups = getGroups({ nodeTypes, items });
+ return { items, groups, groupCollapsed: {} };
+ };
+
+ it('renders without throwing', () => {
+ expect(() => setup.mount()).not.toThrow();
+ });
+
+ it('handles collapse button click events', () => {
+ const wrapper = setup.mount();
+ const nodeList = () => wrapper.find('.filters-group').first();
+ const toggle = () =>
+ wrapper.find('.filters-section-heading__toggle-btn').first();
+ expect(nodeList().length).toBe(1);
+ expect(toggle().hasClass('filters-section-heading__toggle-btn--alt')).toBe(
+ false
+ );
+ expect(() => {
+ toggle()
+ .hasClass('filters-section-heading__toggle-btn--disabled')
+ .toBe(false);
+ toggle().simulate('click');
+ expect(nodeList().length).toBe(1);
+ expect(
+ toggle().hasClass('filters-section-heading__toggle-btn--alt')
+ ).toBe(true);
+ }).toThrow();
+ });
+
+ it('handles group checkbox change events', () => {
+ const onGroupToggleChanged = jest.fn();
+ const wrapper = setup.mount(
+
+ );
+ const checkbox = () => wrapper.find('input').first();
+ checkbox().simulate('change', { target: { checked: false } });
+ expect(onGroupToggleChanged.mock.calls.length).toEqual(1);
+ });
+
+ it('calls onResetFilter when reset button is clicked', () => {
+ const onResetFilter = jest.fn();
+ const wrapper = setup.mount(
+
+ );
+ const resetButton = wrapper.find('.filters__reset-button');
+ expect(resetButton.exists()).toBe(true);
+ resetButton.simulate('click');
+ expect(() => onResetFilter.mock.calls.length.toEqual(1)).toThrow();
+ });
+});
diff --git a/src/components/node-list-tree/node-list-row/node-list-row.js b/src/components/node-list-tree/node-list-row/node-list-row.js
new file mode 100755
index 0000000000..619bd301c4
--- /dev/null
+++ b/src/components/node-list-tree/node-list-row/node-list-row.js
@@ -0,0 +1,123 @@
+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 './node-list-row.scss';
+
+const NodeListRow = ({
+ active,
+ checked,
+ children,
+ dataTest,
+ disabled,
+ faded,
+ focused,
+ focusModeIcon = FocusModeIcon,
+ highlight,
+ icon,
+ id,
+ invisibleIcon = InvisibleIcon,
+ isSlicingPipelineApplied,
+ kind,
+ label,
+ name,
+ onChange,
+ onClick,
+ onMouseEnter,
+ onMouseLeave,
+ onToggleHoveredFocusMode,
+ parentClassName,
+ rowType,
+ selected,
+ type,
+ visibleIcon = VisibleIcon,
+}) => {
+ const isModularPipeline = type === 'modularPipeline';
+ const FocusIcon = isModularPipeline ? focusModeIcon : null;
+ const isChecked = isModularPipeline ? checked || focused : checked;
+ const VisibilityIcon = isChecked ? visibleIcon : invisibleIcon;
+
+ return (
+
+ );
+};
+
+export default NodeListRow;
diff --git a/src/components/node-list-tree/node-list-row/node-list-row.scss b/src/components/node-list-tree/node-list-row/node-list-row.scss
new file mode 100755
index 0000000000..346a8f533e
--- /dev/null
+++ b/src/components/node-list-tree/node-list-row/node-list-row.scss
@@ -0,0 +1,87 @@
+@use '../../../styles/variables' as var;
+@use '../../node-list-tree/styles/variables' as variables;
+
+.MuiTreeItem-iconContainer svg {
+ z-index: var.$zindex-MuiTreeItem-icon;
+}
+
+.node-list-row {
+ align-items: center;
+ cursor: default;
+ display: flex;
+ height: 32px;
+ position: relative;
+ transform: translate(0, 0);
+
+ &:hover,
+ &--active {
+ background-color: var(--color-nodelist-row-selected);
+ }
+
+ &--selected {
+ // Additional selector required to increase specificity to override previous rule
+ background-color: var(--color-nodelist-row-selected);
+ border-right: 1px solid var.$blue-300;
+ }
+
+ // to ensure the background of the row covers the full width on hover
+ &::before {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: -100px;
+ width: 100px;
+ background: var(--color-nodelist-row-selected);
+ transform: translate(0, 0);
+ opacity: 0;
+ content: ' ';
+ pointer-events: none;
+ }
+}
+
+.MuiTreeItem-content:hover {
+ .node-list-row__type-icon path {
+ opacity: 1;
+ }
+}
+
+.node-list-row--active::before,
+.node-list-row--selected::before,
+.node-list-row:hover::before {
+ opacity: 1;
+}
+
+.node-list-row__icon {
+ display: block;
+ flex-shrink: 0;
+ width: variables.$row-icon-size;
+ height: variables.$row-icon-size;
+ fill: var(--color-text);
+
+ &--disabled > * {
+ opacity: 0.1;
+ }
+}
+
+.node-list-row__type-icon {
+ &--nested > * {
+ opacity: 0.3;
+ }
+
+ &--faded > * {
+ opacity: 0.2;
+ }
+
+ &--active,
+ &--selected,
+ .node-list-row--visible:hover &,
+ [data-whatintent='keyboard'] .node-list-row__text:focus & {
+ > * {
+ opacity: 1;
+ }
+
+ &--faded > * {
+ opacity: 0.55;
+ }
+ }
+}
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 72%
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 5a08c0ca25..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
@@ -1,8 +1,10 @@
import React from 'react';
+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 NodeListRow from './node-list-row';
+import NodeListRow from '../node-list-row/node-list-row';
+import { getDataTestAttribute } from '../../../utils/get-data-test-attribute';
const arrowIconColor = '#8e8e90';
@@ -12,11 +14,15 @@ const NodeListTreeItem = ({
onItemMouseEnter,
onItemMouseLeave,
onItemChange,
+ onToggleHoveredFocusMode,
children,
isSlicingPipelineApplied,
}) => (
}
label={
onItemClick(data)}
- onMouseEnter={() => onItemMouseEnter(data)}
- onMouseLeave={() => onItemMouseLeave(data)}
+ isSlicingPipelineApplied={isSlicingPipelineApplied}
+ key={data.id}
+ kind="element"
+ label={data.highlightedLabel || data.name}
+ name={data.name}
onChange={(e) =>
onItemChange(data, !e.target.checked, e.target.dataset.iconType)
}
+ onClick={(e) => onItemClick(e, data)}
+ onMouseEnter={() => onItemMouseEnter(data)}
+ onMouseLeave={() => onItemMouseLeave(data)}
+ onToggleHoveredFocusMode={onToggleHoveredFocusMode}
+ parentClassName={'node-list-tree-item-row'}
rowType="tree"
- focused={data.focused}
+ selected={data.selected}
+ type={data.type}
+ visible={data.visible}
+ visibleIcon={data.visibleIcon}
/>
}
>
diff --git a/src/components/node-list/node-list-tree.js b/src/components/node-list-tree/node-list-tree.js
similarity index 77%
rename from src/components/node-list/node-list-tree.js
rename to src/components/node-list-tree/node-list-tree.js
index fa89c3fec8..fdb5df54d3 100644
--- a/src/components/node-list/node-list-tree.js
+++ b/src/components/node-list-tree/node-list-tree.js
@@ -1,5 +1,4 @@
import React from 'react';
-import { connect } from 'react-redux';
import uniqueId from 'lodash/uniqueId';
import { styled } from '@mui/system';
@@ -8,14 +7,13 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import sortBy from 'lodash/sortBy';
-import { loadNodeData } from '../../actions/nodes';
-import { getNodeSelected } from '../../selectors/nodes';
import { isModularPipelineType } from '../../selectors/node-types';
-import NodeListTreeItem from './node-list-tree-item';
+import NodeListTreeItem from './node-list-tree-item/node-list-tree-item';
import VisibleIcon from '../icons/visible';
import InvisibleIcon from '../icons/invisible';
import FocusModeIcon from '../icons/focus-mode';
-import { getSlicedPipeline } from '../../selectors/sliced-pipeline';
+
+import './styles/node-list.scss';
// Display order of node groups
const GROUPED_NODES_DISPLAY_ORDER = {
@@ -36,20 +34,6 @@ const StyledTreeView = styled(TreeView)({
padding: '0 0 0 20px',
});
-/**
- * Return whether the given modular pipeline ID is on focus mode path, i.e.
- * it's not the currently focused pipeline nor one of its children.
- * @param {String} focusModeID The currently focused modular pipeline ID.
- * @param {String} modularPipelineID The modular pipeline ID to check.
- * @return {Boolean} Whether the given modular pipeline ID is on focus mode path.
- */
-const isOnFocusedModePath = (focusModeID, modularPipelineID) => {
- return (
- modularPipelineID === focusModeID ||
- modularPipelineID.startsWith(`${focusModeID}.`)
- );
-};
-
/**
* Return the data of a modular pipeline to display as a row in the node list.
* @param {Object} params
@@ -94,16 +78,17 @@ const getModularPipelineRowData = ({
* @param {Boolean} selected Whether the node is currently disabled
* @param {Boolean} selected Whether the node is currently selected
*/
-const getNodeRowData = (node, disabled, selected, highlight) => {
+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,
+ faded: disabled || !checked,
visible: !disabled && checked,
checked,
disabled,
@@ -111,49 +96,45 @@ const getNodeRowData = (node, disabled, selected, highlight) => {
};
const TreeListProvider = ({
+ hoveredNode,
nodeSelected,
modularPipelinesSearchResult,
modularPipelinesTree,
onItemChange,
onItemMouseEnter,
onItemMouseLeave,
+ onToggleHoveredFocusMode,
onItemClick,
onNodeToggleExpanded,
focusMode,
- disabledModularPipeline,
expanded,
onToggleNodeSelected,
slicedPipeline,
isSlicingPipelineApplied,
+ nodesDisabledViaModularPipeline,
}) => {
// render a leaf node in the modular pipelines tree
const renderLeafNode = (node) => {
// As part of the slicing pipeline logic, child nodes not included in the sliced pipeline are assigned an empty data object.
// Therefore, if a child node has an empty data object, it indicates it's not part of the slicing pipeline and should not be rendered.
- if (Object.keys(node).length === 0) {
+ if (!node || Object.keys(node).length === 0) {
return null;
}
const disabled =
node.disabledTag ||
node.disabledType ||
- (focusMode &&
- !node.modularPipelines
- .map((modularPipelineID) =>
- isOnFocusedModePath(focusMode.id, modularPipelineID)
- )
- .some(Boolean)) ||
- (node.modularPipelines &&
- node.modularPipelines
- .map(
- (modularPipelineID) => disabledModularPipeline[modularPipelineID]
- )
- .some(Boolean));
+ nodesDisabledViaModularPipeline[node.id];
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 58%
rename from src/components/node-list/styles/node-list.scss
rename to src/components/node-list-tree/styles/node-list.scss
index 3d45c4f370..d3ca6ac65c 100644
--- a/src/components/node-list/styles/node-list.scss
+++ b/src/components/node-list-tree/styles/node-list.scss
@@ -1,11 +1,6 @@
@use '../../../styles/mixins' as mixins;
@use '../../../styles/variables' as colors;
-@use './group';
@use './panels';
-@use './row';
-@use './row-label';
-@use './row-toggle';
-@use './section';
@use './variables';
.kui-theme--light {
@@ -84,12 +79,67 @@
}
}
+// Root class for overwriting styles of the pipeline tree item
.pipeline-treeItem__root--overwrite {
+ position: relative;
+
.Mui-selected {
- background-color: transparent !important;
+ background-color: transparent !important; // Override default background color
}
.MuiTreeItem-content {
- padding: 0;
+ padding: 0; // Remove padding
}
+
+ // When hovering over the tree item content
+ .MuiTreeItem-content:hover {
+ background-color: var(--color-nodelist-row-active) !important;
+
+ &::before {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: -100px;
+ width: 100px;
+ background: var(--color-nodelist-row-active);
+ transform: translate(0, 0);
+ opacity: 1;
+ content: ' ';
+ pointer-events: none;
+ }
+
+ // If it represents the modular pipeline node, change the color of the sibling .MuiTreeItem-group
+ ~ .MuiTreeItem-group {
+ background-color: var(--color-nodelist-row-active);
+ position: relative;
+
+ // Ensure all .row__type-icon path elements have opacity 1
+ .node-list-row__type-icon path {
+ opacity: 1;
+ }
+
+ // Apply the after-shadow mixin to ensure the background covers the full width on hover
+ &::after {
+ content: '';
+ position: absolute;
+ left: -40px;
+ top: 0;
+ height: 100%; // Match the height of the parent
+ width: 50px;
+ background-color: var(--color-nodelist-row-active);
+ }
+ }
+ }
+}
+
+// disable mouse events for the overwrite disabled class
+.pipeline-treeItem__root--overwrite--disabled {
+ pointer-events: none;
+}
+
+.pipeline-nodelist__elements-panel .MuiTreeItem-label {
+ // Handle MuiTreeItem icon offset for correct width
+ $icon-offset: 15px + 4px;
+
+ width: calc(100% - #{$icon-offset});
}
diff --git a/src/components/node-list/index.js b/src/components/node-list/index.js
deleted file mode 100644
index 74353d8944..0000000000
--- a/src/components/node-list/index.js
+++ /dev/null
@@ -1,379 +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 } 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 } from '../../config';
-
-/**
- * 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,
- modularPipelinesTree,
- focusMode,
- disabledModularPipeline,
- inputOutputDataNodes,
- onResetSlicePipeline,
- isSlicingPipelineApplied,
-}) => {
- const [searchValue, updateSearchValue] = useState('');
- const [isResetFilterActive, setIsResetFilterActive] = useState(false);
-
- 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 = (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 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);
- }
- };
-
- 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));
- },
- onResetSlicePipeline: () => {
- dispatch(resetSlicePipeline());
- },
-});
-
-export default connect(mapStateToProps, mapDispatchToProps)(NodeListProvider);
diff --git a/src/components/node-list/node-list-group.js b/src/components/node-list/node-list-group.js
deleted file mode 100644
index 9b54a2d72b..0000000000
--- a/src/components/node-list/node-list-group.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import React from 'react';
-import classnames from 'classnames';
-import NodeListRow from './node-list-row';
-import NodeRowList from './node-list-row-list';
-
-export const NodeListGroup = ({
- allUnchecked,
- checked,
- collapsed,
- group,
- id,
- invisibleIcon,
- items,
- kind,
- name,
- onItemChange,
- onItemClick,
- onItemMouseEnter,
- onItemMouseLeave,
- onToggleChecked,
- onToggleCollapsed,
- visibleIcon,
-}) => {
- const disabledGroup = items.length === 0;
-
- return (
-
-
- {
- onToggleChecked(id, !e.target.checked);
- }}
- rowType="filter"
- visibleIcon={visibleIcon}
- >
-
-
-
-
- );
-};
-
-export default NodeListGroup;
diff --git a/src/components/node-list/node-list-group.test.js b/src/components/node-list/node-list-group.test.js
deleted file mode 100644
index e8ca53fb8a..0000000000
--- a/src/components/node-list/node-list-group.test.js
+++ /dev/null
@@ -1,88 +0,0 @@
-import React from 'react';
-import { NodeListGroup } from './node-list-group';
-import { getNodeTypes } from '../../selectors/node-types';
-import { setup, mockState } from '../../utils/state.mock';
-
-describe('NodeListGroup', () => {
- const items = [];
-
- it('renders without throwing', () => {
- const type = getNodeTypes(mockState.spaceflights)[0];
- expect(() =>
- setup.mount()
- ).not.toThrow();
- });
-
- it('handles checkbox change events', () => {
- const type = getNodeTypes(mockState.spaceflights)[0];
- const onToggleChecked = jest.fn();
- const wrapper = setup.mount(
-
- );
- const checkbox = () => wrapper.find('input');
- checkbox().simulate('change', { target: { checked: false } });
- expect(onToggleChecked.mock.calls.length).toEqual(1);
- });
-
- it('handles collapse button click events', () => {
- const type = getNodeTypes(mockState.spaceflights)[0];
- const onToggleCollapsed = jest.fn();
- const wrapper = setup.mount(
-
- );
- wrapper.find('.pipeline-type-group-toggle').simulate('click');
- expect(() => onToggleCollapsed.mock.calls.length.toEqual(1)).toThrow();
- });
-
- it('adds class when collapsed prop true', () => {
- const type = getNodeTypes(mockState.spaceflights)[0];
- const wrapper = setup.mount(
-
- );
- const children = wrapper.find('.pipeline-nodelist__children');
- expect(children.hasClass('pipeline-nodelist__children--closed')).toBe(true);
- });
-
- it('removes class when collapsed prop false', () => {
- const type = getNodeTypes(mockState.spaceflights)[0];
- const wrapper = setup.mount(
-
- );
- const children = wrapper.find('.pipeline-nodelist__children');
- expect(children.hasClass('pipeline-nodelist__children--closed')).toBe(
- false
- );
- });
-
- it('adds disabled class when items list is empty', () => {
- const type = getNodeTypes(mockState.spaceflights)[0];
- const wrapper = setup.mount(
-
- );
- expect(items.length).toBe(0);
- const button = () => wrapper.find('button');
- expect(button().hasClass('pipeline-type-group-toggle--disabled')).toBe(
- true
- );
- });
-});
diff --git a/src/components/node-list/node-list-groups.js b/src/components/node-list/node-list-groups.js
deleted file mode 100644
index a91dd31e94..0000000000
--- a/src/components/node-list/node-list-groups.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import React, { useState } from 'react';
-import { loadLocalStorage, saveLocalStorage } from '../../store/helpers';
-import NodeListGroup from './node-list-group';
-import { localStorageName } from '../../config';
-
-const storedState = loadLocalStorage(localStorageName);
-
-const NodeListGroups = ({
- groups,
- items,
- onGroupToggleChanged,
- onItemChange,
- onItemClick,
- onItemMouseEnter,
- onItemMouseLeave,
- searchValue,
-}) => {
- const [collapsed, setCollapsed] = useState(storedState.groupsCollapsed || {});
-
- // Collapse/expand node group
- const onToggleGroupCollapsed = (groupID) => {
- const groupsCollapsed = {
- ...collapsed,
- [groupID]: !collapsed[groupID],
- };
-
- setCollapsed(groupsCollapsed);
- saveLocalStorage(localStorageName, { groupsCollapsed });
- };
-
- return (
-
- );
-};
-
-export default NodeListGroups;
diff --git a/src/components/node-list/node-list-groups.test.js b/src/components/node-list/node-list-groups.test.js
deleted file mode 100644
index abaa7d4d52..0000000000
--- a/src/components/node-list/node-list-groups.test.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import React from 'react';
-import NodeListGroups from './node-list-groups';
-import { mockState, setup } from '../../utils/state.mock';
-import { getNodeTypes } from '../../selectors/node-types';
-import { getGroupedNodes } from '../../selectors/nodes';
-import { getGroups } from './node-list-items';
-
-describe('NodeListGroups', () => {
- const mockProps = () => {
- const items = getGroupedNodes(mockState.spaceflights);
- const nodeTypes = getNodeTypes(mockState.spaceflights);
- const groups = getGroups({ nodeTypes, items });
- return { items, groups };
- };
-
- it('renders without throwing', () => {
- expect(() =>
- setup.mount()
- ).not.toThrow();
- });
-
- it('handles collapse button click events', () => {
- const wrapper = setup.mount();
- const nodeList = () =>
- wrapper.find('.pipeline-nodelist__list--nested').first();
- const toggle = () => wrapper.find('.pipeline-type-group-toggle').first();
- expect(nodeList().length).toBe(1);
- expect(toggle().hasClass('pipeline-type-group-toggle--alt')).toBe(false);
- expect(() => {
- toggle().hasClass('pipeline-type-group-toggle--disabled').toBe(false);
- toggle().simulate('click');
- expect(nodeList().length).toBe(1);
- expect(toggle().hasClass('pipeline-type-group-toggle--alt')).toBe(true);
- }).toThrow();
- });
-
- it('handles group checkbox change events', () => {
- const onGroupToggleChanged = jest.fn();
- const wrapper = setup.mount(
-
- );
- const checkbox = () => wrapper.find('input').first();
- checkbox().simulate('change', { target: { checked: false } });
- expect(onGroupToggleChanged.mock.calls.length).toEqual(1);
- });
-});
diff --git a/src/components/node-list/node-list-row-list.js b/src/components/node-list/node-list-row-list.js
deleted file mode 100644
index 4566fbaafc..0000000000
--- a/src/components/node-list/node-list-row-list.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import React from 'react';
-import modifiers from '../../utils/modifiers';
-import NodeListRow, { nodeListRowHeight } from './node-list-row';
-import LazyList from '../lazy-list';
-
-const NodeRowList = ({
- items = [],
- group,
- collapsed,
- onItemClick,
- onItemChange,
- onItemMouseEnter,
- onItemMouseLeave,
-}) => (
- (end - start) * nodeListRowHeight}
- total={items.length}
- >
- {({
- start,
- end,
- total,
- listRef,
- upperRef,
- lowerRef,
- listStyle,
- upperStyle,
- lowerStyle,
- }) => (
-
- - 0,
- })}
- ref={upperRef}
- style={upperStyle}
- />
-
- {items.slice(start, end).map((item) => (
- onItemClick(item)}
- onMouseEnter={() => onItemMouseEnter(item)}
- onMouseLeave={() => onItemMouseLeave(item)}
- onChange={(e) => onItemChange(item, !e.target.checked)}
- rowType="filter"
- />
- ))}
-
- )}
-
-);
-
-export default NodeRowList;
diff --git a/src/components/node-list/node-list-row.js b/src/components/node-list/node-list-row.js
deleted file mode 100644
index fdabf9d584..0000000000
--- a/src/components/node-list/node-list-row.js
+++ /dev/null
@@ -1,255 +0,0 @@
-import React, { memo } from 'react';
-import { connect } from 'react-redux';
-import classnames from 'classnames';
-import { changed, replaceAngleBracketMatches } from '../../utils';
-import NodeIcon from '../icons/node-icon';
-import VisibleIcon from '../icons/visible';
-import InvisibleIcon from '../icons/invisible';
-import FocusModeIcon from '../icons/focus-mode';
-import { getNodeActive } from '../../selectors/nodes';
-import { toggleHoveredFocusMode } from '../../actions';
-
-// The exact fixed height of a row as measured by getBoundingClientRect()
-export const nodeListRowHeight = 32;
-
-/**
- * Returns `true` if there are no props changes, therefore the last render can be reused.
- * Performance: Checks only the minimal set of props known to change after first render.
- */
-const shouldMemo = (prevProps, nextProps) =>
- !changed(
- [
- 'active',
- 'checked',
- 'allUnchecked',
- 'disabled',
- 'faded',
- 'focused',
- 'visible',
- 'selected',
- 'highlight',
- 'label',
- 'children',
- 'count',
- ],
- prevProps,
- nextProps
- );
-
-const NodeListRow = memo(
- ({
- container: Container = 'div',
- active,
- checked,
- allUnchecked,
- children,
- disabled,
- faded,
- focused,
- visible,
- id,
- label,
- count,
- name,
- kind,
- onMouseEnter,
- onMouseLeave,
- onChange,
- onClick,
- selected,
- highlight,
- isSlicingPipelineApplied,
- type,
- icon,
- visibleIcon = VisibleIcon,
- invisibleIcon = InvisibleIcon,
- focusModeIcon = FocusModeIcon,
- rowType,
- onToggleHoveredFocusMode,
- }) => {
- const isModularPipeline = type === 'modularPipeline';
- const FocusIcon = isModularPipeline ? focusModeIcon : null;
- const isChecked = isModularPipeline ? checked || focused : checked;
- const VisibilityIcon = isChecked ? visibleIcon : invisibleIcon;
- const isButton = onClick && kind !== 'filter';
- const TextButton = isButton ? 'button' : 'div';
-
- return (
-
- {icon && (
-
- )}
-
-
-
- {typeof count === 'number' && (
-
- {count}
-
- )}
- {VisibilityIcon && (
-
- )}
- {FocusIcon && (
-
- )}
- {children}
-
- );
- },
- shouldMemo
-);
-
-export const mapDispatchToProps = (dispatch) => ({
- onToggleHoveredFocusMode: (active) => {
- dispatch(toggleHoveredFocusMode(active));
- },
-});
-
-export const mapStateToProps = (state, ownProps) => ({
- ...ownProps,
- active:
- typeof ownProps.active !== 'undefined'
- ? ownProps.active
- : getNodeActive(state)[ownProps.id] || false,
-});
-
-export default connect(mapStateToProps, mapDispatchToProps)(NodeListRow);
diff --git a/src/components/node-list/node-list-row.test.js b/src/components/node-list/node-list-row.test.js
deleted file mode 100644
index f4651e200f..0000000000
--- a/src/components/node-list/node-list-row.test.js
+++ /dev/null
@@ -1,191 +0,0 @@
-import React from 'react';
-import NodeListRow, { mapStateToProps } from './node-list-row';
-import { getNodeData } from '../../selectors/nodes';
-import { setup, mockState } from '../../utils/state.mock';
-
-describe('NodeListRow', () => {
- const node = getNodeData(mockState.spaceflights)[0];
- const setupProps = () => {
- const props = {
- active: true,
- checked: true,
- disabled: false,
- faded: false,
- visible: true,
- id: node.id,
- label: node.highlightedLabel,
- name: node.name,
- onClick: jest.fn(),
- onMouseEnter: jest.fn(),
- onMouseLeave: jest.fn(),
- onChange: jest.fn(),
- };
- return { props };
- };
-
- it('renders without throwing', () => {
- expect(() => setup.mount()).not.toThrow();
- });
-
- describe('node list item', () => {
- it('handles mouseenter events', () => {
- const { props } = setupProps();
- const wrapper = setup.mount();
- const nodeRow = () => wrapper.find('.pipeline-nodelist__row');
- nodeRow().simulate('mouseenter');
- expect(props.onMouseEnter.mock.calls.length).toEqual(1);
- });
-
- it('handles mouseleave events', () => {
- const { props } = setupProps();
- const wrapper = setup.mount();
- const nodeRow = () => wrapper.find('.pipeline-nodelist__row');
- nodeRow().simulate('mouseleave');
- expect(props.onMouseLeave.mock.calls.length).toEqual(1);
- });
-
- it('applies the overwrite class if not active', () => {
- const { props } = setupProps();
- const activeNodeWrapper = setup.mount(
-
- );
- expect(
- activeNodeWrapper
- .find('.pipeline-nodelist__row')
- .hasClass('pipeline-nodelist__row--overwrite')
- ).toBe(true);
- });
-
- it('applies the overwrite class if not selected or active', () => {
- const { props } = setupProps();
- const activeNodeWrapper = setup.mount(
-
- );
- expect(
- activeNodeWrapper
- .find('.pipeline-nodelist__row')
- .hasClass('pipeline-nodelist__row--overwrite')
- ).toBe(true);
- });
-
- it('does not applies the overwrite class if not selected', () => {
- const { props } = setupProps();
- const activeNodeWrapper = setup.mount(
-
- );
- expect(
- activeNodeWrapper
- .find('.pipeline-nodelist__row')
- .hasClass('pipeline-nodelist__row--overwrite')
- ).toBe(false);
- });
-
- it('does not applies the overwrite class if active', () => {
- const { props } = setupProps();
- const activeNodeWrapper = setup.mount(
-
- );
- expect(
- activeNodeWrapper
- .find('.pipeline-nodelist__row')
- .hasClass('pipeline-nodelist__row--overwrite')
- ).toBe(false);
- });
-
- it('uses active class if active', () => {
- const { props } = setupProps();
- const activeNodeWrapper = setup.mount(
-
- );
- expect(
- activeNodeWrapper
- .find('.pipeline-nodelist__row')
- .hasClass('pipeline-nodelist__row--active')
- ).toBe(true);
- });
-
- it('uses disabled class if disabled (via type/tag only)', () => {
- const { props } = setupProps();
- const disabledNodeWrapper = setup.mount(
-
- );
- expect(
- disabledNodeWrapper
- .find('.pipeline-nodelist__row')
- .hasClass('pipeline-nodelist__row--disabled')
- ).toBe(true);
- });
-
- it('shows count if count prop set', () => {
- const { props } = setupProps();
- const mockCount = 123;
- const wrapper = setup.mount();
- expect(wrapper.find('.pipeline-nodelist__row__count').text()).toBe(
- mockCount.toString()
- );
- });
-
- it('does not show count if count prop not set', () => {
- const { props } = setupProps();
- const wrapper = setup.mount();
- expect(wrapper.find('.pipeline-nodelist__row__count').exists()).toBe(
- false
- );
- });
-
- describe('focus mode', () => {
- it('sets the focus toggle to the checked mode when the row is selected for focus mode', () => {
- const { props } = setupProps();
- const wrapper = setup.mount(
-
- );
-
- expect(
- wrapper.find('.pipeline-row__toggle-icon--focus-checked').exists()
- ).toBe(true);
- });
-
- it('hides the visibility toggle when the row is selected for focus mode', () => {
- const { props } = setupProps();
- const wrapper = setup.mount(
-
- );
-
- expect(wrapper.find('.pipeline-row__toggle--disabled').exists()).toBe(
- true
- );
- });
-
- it('switches the visibility toggle from hide to show when the row is selected for focus mode', () => {
- const { props } = setupProps();
- const wrapper = setup.mount(
-
- );
- expect(wrapper.find('VisibleIcon')).toHaveLength(1);
- });
- });
- });
-
- describe('node list item checkbox', () => {
- const { props } = setupProps();
- const wrapper = setup.mount();
- const checkbox = () => wrapper.find('input');
-
- it('handles toggle event', () => {
- checkbox().simulate('change', { target: { checked: false } });
- expect(props.onChange.mock.calls.length).toEqual(1);
- });
- });
-
- it('maps state to props', () => {
- const expectedResult = expect.objectContaining({
- active: expect.any(Boolean),
- });
- expect(mapStateToProps(mockState.spaceflights, {})).toEqual(expectedResult);
- });
-});
diff --git a/src/components/node-list/node-list.js b/src/components/node-list/node-list.js
deleted file mode 100644
index 0106c0594c..0000000000
--- a/src/components/node-list/node-list.js
+++ /dev/null
@@ -1,116 +0,0 @@
-import React from 'react';
-import classnames from 'classnames';
-import { Scrollbars } from 'react-custom-scrollbars-2';
-import SearchList from '../search-list';
-import NodeListGroups from './node-list-groups';
-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,
- onItemClick,
- onItemMouseEnter,
- onItemMouseLeave,
- onItemChange,
- onModularPipelineToggleExpanded,
- focusMode,
- disabledModularPipeline,
- onResetFilter,
- isResetFilterActive,
-}) => {
- return (
-
-
-
- {({ isResizing, props: { container, panelA, panelB, handle } }) => (
-
-
-
-
-
-
-
- Filters
-
-
-
-
-
-
-
- )}
-
-
- );
-};
-
-export default NodeList;
diff --git a/src/components/node-list/styles/_group.scss b/src/components/node-list/styles/_group.scss
deleted file mode 100644
index 0d456bd2f5..0000000000
--- a/src/components/node-list/styles/_group.scss
+++ /dev/null
@@ -1,173 +0,0 @@
-@use '../../../styles/variables' as var;
-@use './variables';
-
-%nolist {
- margin: 0;
- padding: 0;
- list-style: none;
-}
-
-.pipeline-nodelist__list {
- @extend %nolist;
-
- &--nested {
- margin: 0 0 1.2em;
- }
-
- .pipeline-nodelist__children {
- // Avoid placeholder fade leaking out for small lists
- overflow: hidden;
-
- &--closed {
- display: none;
- }
- }
-}
-
-$placeholder-fade: 120px;
-
-.pipeline-nodelist__placeholder-upper,
-.pipeline-nodelist__placeholder-lower {
- z-index: var.$zindex-nodelist-placeholder;
- pointer-events: none;
-}
-
-.pipeline-nodelist__placeholder-upper::after,
-.pipeline-nodelist__placeholder-lower::after {
- position: absolute;
- width: 100%;
- height: $placeholder-fade;
- opacity: 0;
- transition: opacity ease 0.3s;
- content: ' ';
- pointer-events: none;
-}
-
-// Add fade overlay at the lazy list boundaries visible during scroll
-.pipeline-nodelist__filter-panel {
- .pipeline-nodelist__placeholder-upper::after {
- bottom: -$placeholder-fade;
- background: linear-gradient(
- 0deg,
- var(--color-nodelist-bg-filter-transparent) 0%,
- var(--color-nodelist-filter-panel) 100%
- );
- }
-
- .pipeline-nodelist__placeholder-lower::after {
- top: -$placeholder-fade;
- background: linear-gradient(
- 0deg,
- var(--color-nodelist-filter-panel) 0%,
- var(--color-nodelist-bg-filter-transparent) 100%
- );
- }
-}
-
-.pipeline-nodelist__placeholder-upper--fade::after,
-.pipeline-nodelist__placeholder-lower--fade::after {
- opacity: 1;
-}
-
-.pipeline-nodelist__heading {
- position: sticky;
- top: 0;
- z-index: var.$zindex-nodelist-heading;
- margin: 0;
-
- // Avoid pixel gap above when scrolling.
- transform: translateY(-1px);
-
- .pipeline-nodelist__row__text {
- position: relative;
- opacity: 0.65;
- }
-
- .pipeline-nodelist__row__text .pipeline-nodelist__row__label {
- font-size: 1.3em;
- }
-}
-
-.pipeline-nodelist__elements-panel .pipeline-nodelist__heading {
- background: var(--color-nodelist-element-panel);
-
- &::after {
- position: absolute;
- bottom: -19px;
- z-index: var.$zindex-group-background-fade;
- width: 100%;
- height: 20px;
- background: linear-gradient(
- 0deg,
- var(--color-nodelist-bg-transparent) 0%,
- var(--color-nodelist-element-panel) 100%
- );
- transition: opacity ease 0.3s;
- content: ' ';
- pointer-events: none;
- }
-}
-
-.pipeline-nodelist__filter-panel .pipeline-nodelist__heading {
- background: var(--color-nodelist-filter-panel);
-
- &::after {
- position: absolute;
- bottom: -19px;
- z-index: var.$zindex-group-background-fade;
- width: 100%;
- height: 20px;
- background: linear-gradient(
- 0deg,
- var(--color-nodelist-bg-transparent) 0%,
- var(--color-nodelist-filter-panel) 100%
- );
- transition: opacity ease 0.3s;
- content: ' ';
- pointer-events: none;
- }
-}
-
-.pipeline-type-group-toggle {
- width: variables.$toggle-size;
- height: variables.$toggle-size;
- padding: 0;
- color: var(--color-default-alt);
- font-size: inherit;
- font-family: inherit;
- line-height: 1em;
- text-align: center;
- background: none;
- border: none;
- border-radius: 50%;
- box-shadow: none;
- cursor: pointer;
- transition: transform ease 0.1s;
-
- &:focus {
- outline: none;
-
- [data-whatintent='keyboard'] & {
- box-shadow: 0 0 0 3px var.$blue-300 inset;
- }
- }
-
- &::before {
- font-size: 1.8em;
- opacity: 0.55;
- content: '▾';
- }
-
- &:hover::before {
- opacity: 1;
- }
-
- &--alt {
- transform: rotate(90deg);
- }
-
- &--disabled {
- color: var.$black-400;
- transform: rotate(90deg);
- }
-}
diff --git a/src/components/node-list/styles/_row-label.scss b/src/components/node-list/styles/_row-label.scss
deleted file mode 100644
index 72fc48a6c8..0000000000
--- a/src/components/node-list/styles/_row-label.scss
+++ /dev/null
@@ -1,102 +0,0 @@
-@use '../../../styles/variables' as colors;
-@use './variables';
-
-.pipeline-nodelist__elements-panel .MuiTreeItem-label {
- // Handle MuiTreeItem icon offset for correct width
- $icon-offset: 15px + 4px;
-
- width: calc(100% - #{$icon-offset});
-}
-
-.pipeline-nodelist__row__text {
- display: flex;
- align-items: center;
-
- // Fixed with required for overflow elipsis
- width: calc(100% - 7em);
- margin-right: auto;
- padding: variables.$row-padding-y 0 variables.$row-padding-y 0;
- color: inherit;
- font-size: inherit;
- font-family: inherit;
- line-height: 1.6;
- letter-spacing: inherit;
- text-align: inherit;
- background: none;
- border: none;
- border-radius: 0;
- box-shadow: none;
- cursor: default;
- user-select: none;
-
- &--tree {
- padding: variables.$row-padding-y 0 variables.$row-padding-y 1em;
- }
-
- &:focus {
- outline: none;
- box-shadow: 0 0 0 4px colors.$blue-300 inset;
-
- [data-whatintent='mouse'] & {
- box-shadow: none;
- }
- }
-}
-
-.pipeline-nodelist__row__label {
- overflow: hidden;
- font-size: 1.4em;
- white-space: nowrap;
- text-overflow: ellipsis;
-
- &--faded {
- opacity: 0.65;
- }
-
- &--disabled {
- opacity: 0.3 !important;
- }
-
- b {
- color: var(--color-nodelist-highlight);
- font-weight: normal;
- }
-}
-
-.pipeline-nodelist__row__count {
- display: inline-block;
- flex-shrink: 0;
- width: 2.2em;
- margin: 0 0.7em 0.1em auto;
- overflow: hidden;
- font-size: 1.16em;
- text-align: right;
- text-overflow: ellipsis;
- opacity: 0.75;
- user-select: none;
-
- .pipeline-nodelist__row--unchecked & {
- opacity: 0.55;
- }
-}
-
-.pipeline-nodelist__row--unchecked {
- // Fade row text when unchecked
- .pipeline-nodelist__row__label--kind-filter {
- opacity: 0.55;
- }
-
- // Brighter row text when unchecked and hovered
- &:hover {
- .pipeline-nodelist__row__label--kind-filter {
- opacity: 0.8;
- }
- }
-
- // Bright row text when all unchecked
- .pipeline-nodelist__group--all-unchecked & {
- .pipeline-nodelist__row__label--kind-filter {
- opacity: 1;
- }
- }
-}
diff --git a/src/components/node-list/styles/_row.scss b/src/components/node-list/styles/_row.scss
deleted file mode 100644
index 409be89666..0000000000
--- a/src/components/node-list/styles/_row.scss
+++ /dev/null
@@ -1,116 +0,0 @@
-@use '../../../styles/variables' as var;
-@use './variables';
-
-.MuiTreeItem-iconContainer svg {
- z-index: var.$zindex-MuiTreeItem-icon;
-}
-
-.pipeline-nodelist__row {
- position: relative;
- display: flex;
- align-items: center;
- height: 32px; // Fixed row height required for lazy list, apply any changes to node-list-row.js.
- transform: translate(
- 0,
- 0
- ); // Force GPU layers to avoid drawing lag on scroll.
-
- background-color: initial;
- cursor: default;
-
- &--overwrite {
- .Mui-selected & {
- .kui-theme--dark & {
- background-color: var.$slate-200;
- }
-
- .kui-theme--light & {
- background-color: var.$white-0;
- }
- }
- }
-
- &--kind-filter {
- padding: 0 variables.$row-offset-right 0 variables.$row-offset-left;
- }
-
- &--active,
- &--visible:hover {
- background-color: var(--color-nodelist-row-active);
- }
-
- &--selected,
- &--visible#{&}--selected {
- // Additional selector required to increase specificity to override previous rule
- background-color: var(--color-nodelist-row-selected);
- border-right: 1px solid var.$blue-300;
- }
-
- &--disabled {
- pointer-events: none;
- }
-
- &::before {
- position: absolute;
- top: 0;
- bottom: 0;
- left: -100px;
- width: 100px;
- background: var(--color-nodelist-row-selected);
- transform: translate(0, 0);
- opacity: 0;
- content: ' ';
- pointer-events: none;
- }
-}
-
-.pipeline-nodelist__row--active::before,
-.pipeline-nodelist__row--selected::before,
-.pipeline-nodelist__row:hover::before {
- opacity: 1;
-}
-
-.pipeline-nodelist__row--overwrite::before {
- .Mui-selected & {
- opacity: 1;
- }
-}
-
-.pipeline-nodelist__row__icon {
- display: block;
- flex-shrink: 0;
- width: variables.$row-icon-size;
- height: variables.$row-icon-size;
- fill: var(--color-text);
-
- &.pipeline-row__toggle-icon--focus-checked {
- fill: var.$blue-300;
- }
-
- &--disabled > * {
- opacity: 0.1;
- }
-}
-
-.pipeline-nodelist__row__type-icon {
- &--nested > * {
- opacity: 0.3;
- }
-
- &--faded > * {
- opacity: 0.2;
- }
-
- &--active,
- &--selected,
- .pipeline-nodelist__row--visible:hover &,
- [data-whatintent='keyboard'] .pipeline-nodelist__row__text:focus & {
- > * {
- opacity: 1;
- }
-
- &--faded > * {
- opacity: 0.55;
- }
- }
-}
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..8a8957cf61
--- /dev/null
+++ b/src/components/nodes-panel/nodes-panel.js
@@ -0,0 +1,140 @@
+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,
+ expanded,
+ focusMode,
+ handleItemMouseEnter,
+ handleItemMouseLeave,
+ handleKeyDown,
+ handleModularPipelineToggleExpanded,
+ handleNodeListRowChanged,
+ handleNodeListRowClicked,
+ handleToggleHoveredFocusMode,
+ isSlicingPipelineApplied,
+ modularPipelinesTree,
+ selectedNodes,
+ slicedPipeline,
+ nodesDisabledViaModularPipeline,
+ } = 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 74%
rename from src/components/node-list/node-list.test.js
rename to src/components/nodes-panel/nodes-panel.test.js
index edceb82879..8d56c56adc 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,11 +27,11 @@ describe('NodeList', () => {
it('renders without crashing', () => {
const wrapper = setup.mount(
-
+
);
const search = wrapper.find('.pipeline-search-list');
- const nodeList = wrapper.find('.pipeline-nodelist__list');
+ const nodeList = wrapper.find('.filters__section-wrapper');
expect(search.length).toBe(1);
expect(nodeList.length).toBeGreaterThan(0);
});
@@ -40,7 +40,7 @@ describe('NodeList', () => {
describe('displays nodes matching search value', () => {
const wrapper = setup.mount(
-
+
);
@@ -59,7 +59,7 @@ describe('NodeList', () => {
const search = () => wrapper.find('.search-input__field');
search().simulate('change', { target: { value: searchText } });
const nodeList = wrapper.find(
- '.pipeline-nodelist__elements-panel .pipeline-nodelist__row'
+ '.pipeline-nodelist__elements-panel .node-list-tree-item-row'
);
const nodes = getNodeData(mockState.spaceflights);
const tags = getTagData(mockState.spaceflights);
@@ -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');
@@ -102,7 +102,7 @@ describe('NodeList', () => {
const search = () => wrapper.find('.search-input__field');
const nodeList = () =>
wrapper.find(
- '.pipeline-nodelist__elements-panel .pipeline-nodelist__row'
+ '.pipeline-nodelist__elements-panel .node-list-tree-item-row'
);
const nodes = getNodeData(mockState.spaceflights);
@@ -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');
@@ -149,7 +149,7 @@ describe('NodeList', () => {
const search = () => wrapper.find('.search-input__field');
const nodeList = () =>
wrapper.find(
- '.pipeline-nodelist__elements-panel .pipeline-nodelist__row'
+ '.pipeline-nodelist__elements-panel .node-list-tree-item-row'
);
const nodes = getNodeData(mockState.spaceflights);
@@ -192,13 +192,13 @@ describe('NodeList', () => {
const elements = (wrapper) =>
wrapper
.find('.MuiTreeItem-label')
- .find('.pipeline-nodelist__row')
+ .find('.node-list-tree-item-row')
.map((row) => [row.prop('title')]);
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)],
@@ -233,10 +233,10 @@ describe('NodeList', () => {
describe('checkboxes on tag filter items', () => {
const checkboxByName = (wrapper, text) =>
- wrapper.find(`.pipeline-nodelist__row__checkbox[name="${text}"]`);
+ wrapper.find(`.toggle-control__checkbox[name="${text}"]`);
- const rowByName = (wrapper, text) =>
- wrapper.find(`.pipeline-nodelist__row[title="${text}"]`);
+ const filterRowByName = (wrapper, text) =>
+ wrapper.find(`.node-list-filter-row[title="${text}"]`);
const changeRows = (wrapper, names, checked) =>
names.forEach((name) =>
@@ -248,52 +248,19 @@ describe('NodeList', () => {
const elements = (wrapper) =>
wrapper
.find('.MuiTreeItem-label')
- .find('.pipeline-nodelist__row')
- .map((row) => [
- row.prop('title'),
- !row.hasClass('pipeline-nodelist__row--disabled'),
- ]);
+ .find('.node-list-tree-item-row')
+ .map((row) => [row.prop('title'), !row.hasClass('row--disabled')]);
- const elementsEnabled = (wrapper) => {
- return elements(wrapper).filter(([_, enabled]) => enabled);
- };
-
- const tagItem = (wrapper) =>
- wrapper.find('.pipeline-nodelist__group--type-tag');
+ const tagItem = (wrapper) => wrapper.find('.filters-section--type-tag');
const partialIcon = (wrapper) =>
tagItem(wrapper).find(IndicatorPartialIcon);
- it('selecting tags enables only elements with given tags and modular pipelines', () => {
- //Parameters are enabled here to override the default behavior
- const wrapper = setup.mount(
-
-
- ,
- {
- beforeLayoutActions: [() => toggleTypeDisabled('parameters', false)],
- }
- );
-
- changeRows(wrapper, ['Preprocessing'], true);
- expect(elementsEnabled(wrapper)).toEqual([
- ['data_processing', true],
- ['data_science', true],
- ]);
-
- changeRows(wrapper, ['Preprocessing', 'Features'], true);
- expect(elementsEnabled(wrapper)).toEqual([
- ['data_processing', true],
- ['data_science', true],
- ['model_input_table', true],
- ]);
- });
-
it('selecting a tag sorts elements by modular pipelines first then by task, data and parameter nodes ', () => {
//Parameters are enabled here to override the default behavior
const wrapper = setup.mount(
-
+
,
{
beforeLayoutActions: [() => toggleTypeDisabled('parameters', false)],
@@ -313,10 +280,10 @@ describe('NodeList', () => {
it('adds a class to tag group item when all tags unchecked', () => {
const wrapper = setup.mount(
-
+
);
- const uncheckedClass = 'pipeline-nodelist__group--all-unchecked';
+ const uncheckedClass = 'filters-section--all-unchecked';
expect(tagItem(wrapper).hasClass(uncheckedClass)).toBe(true);
changeRows(wrapper, ['Preprocessing'], true);
@@ -328,28 +295,38 @@ describe('NodeList', () => {
it('adds a class to the row when a tag row unchecked', () => {
const wrapper = setup.mount(
-
+
);
- const uncheckedClass = 'pipeline-nodelist__row--unchecked';
+ const uncheckedClass = 'toggle-control--icon--unchecked';
+
+ const filterRow = filterRowByName(wrapper, 'Preprocessing');
+ const hasUncheckedClass = filterRow.find(`.${uncheckedClass}`).exists();
+ expect(hasUncheckedClass).toBe(true);
- expect(rowByName(wrapper, 'Preprocessing').hasClass(uncheckedClass)).toBe(
- true
- );
changeRows(wrapper, ['Preprocessing'], true);
- expect(rowByName(wrapper, 'Preprocessing').hasClass(uncheckedClass)).toBe(
- false
- );
+ const hasUncheckedClassAfterChangeTrue = filterRowByName(
+ wrapper,
+ 'Preprocessing'
+ )
+ .find(`.${uncheckedClass}`)
+ .exists();
+ expect(hasUncheckedClassAfterChangeTrue).toBe(false);
+
changeRows(wrapper, ['Preprocessing'], false);
- expect(rowByName(wrapper, 'Preprocessing').hasClass(uncheckedClass)).toBe(
- true
- );
+ const hasUncheckedClassAfterChangeFalse = filterRowByName(
+ wrapper,
+ 'Preprocessing'
+ )
+ .find(`.${uncheckedClass}`)
+ .exists();
+ expect(hasUncheckedClassAfterChangeFalse).toBe(true);
});
it('shows as partially selected when at least one but not all tags selected', () => {
const wrapper = setup.mount(
-
+
);
@@ -366,13 +343,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);
@@ -383,28 +360,28 @@ describe('NodeList', () => {
});
});
+ // FILTER GROUP
describe('node list', () => {
it('renders the correct number of tags in the filter panel', () => {
const wrapper = setup.mount(
-
+
);
- const nodeList = wrapper.find(
- '.pipeline-nodelist__list--nested .pipeline-nodelist__row'
- );
- // const nodes = getNodeData(mockState.spaceflights);
+ const nodeList = wrapper.find('.filters-group .node-list-filter-row');
const tags = getTagData(mockState.spaceflights);
const elementTypes = Object.keys(sidebarElementTypes);
expect(nodeList.length).toBe(tags.length + elementTypes.length);
});
+
it('renders the correct number of modular pipelines and nodes in the tree sidepanel', () => {
const wrapper = setup.mount(
-
+
);
- const nodeList = wrapper.find('.pipeline-nodelist__row__text--tree');
+
+ const nodeList = wrapper.find('.row-text--tree');
const modularPipelinesTree = getModularPipelinesTree(
mockState.spaceflights
);
@@ -416,7 +393,7 @@ describe('NodeList', () => {
it('renders elements panel, filter panel inside a SplitPanel with a handle', () => {
const wrapper = setup.mount(
-
+
);
const split = wrapper.find(SplitPanel);
@@ -437,33 +414,13 @@ describe('NodeList', () => {
});
});
- describe('node list element item', () => {
- const wrapper = setup.mount(
-
-
-
- );
- // this needs to be the 3rd element as the first 2 elements are modular pipelines rows which does not apply the '--active' class
- const nodeRow = () => wrapper.find('.pipeline-nodelist__row').at(3);
-
- it('handles mouseenter events', () => {
- nodeRow().simulate('mouseenter');
- expect(nodeRow().hasClass('pipeline-nodelist__row--active')).toBe(true);
- });
-
- it('handles mouseleave events', () => {
- nodeRow().simulate('mouseleave');
- expect(nodeRow().hasClass('pipeline-nodelist__row--active')).toBe(false);
- });
- });
-
describe('node list element item checkbox', () => {
const wrapper = setup.mount(
-
+
);
- const checkbox = () => wrapper.find('.pipeline-nodelist__row input').at(4);
+ const checkbox = () => wrapper.find('.node-list-tree-item-row input').at(4);
it('handles toggle off event', () => {
checkbox().simulate('change', {
@@ -493,13 +450,11 @@ describe('NodeList', () => {
describe('Reset node filters', () => {
const wrapper = setup.mount(
-
+
);
- const resetFilterButton = wrapper.find(
- '.pipeline-nodelist-section__reset-filter'
- );
+ const resetFilterButton = wrapper.find('.filters__reset-button');
it('On first load before applying filter button should be disabled', () => {
expect(resetFilterButton.prop('disabled')).toBe(true);
@@ -507,7 +462,7 @@ describe('NodeList', () => {
it('After applying any filter filter button should not be disabled', () => {
const nodeTypeFilter = wrapper.find(
- `.pipeline-nodelist__row__checkbox[name="Datasets"]`
+ `.toggle-control__checkbox[name="Datasets"]`
);
nodeTypeFilter.simulate('click');
@@ -526,30 +481,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..a7c4cde3fc
--- /dev/null
+++ b/src/components/nodes-panel/utils/node-list-context.js
@@ -0,0 +1,232 @@
+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';
+import { getnodesDisabledViaModularPipeline } from '../../../selectors/disabled';
+
+// Custom hook to group useSelector calls
+const useNodeListContextSelector = () => {
+ const dispatch = useDispatch();
+ const hoveredNode = useSelector((state) => state.node.hovered);
+ const selectedNodes = useSelector(getNodeSelected);
+ const nodesDisabledViaModularPipeline = useSelector(
+ getnodesDisabledViaModularPipeline
+ );
+ 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,
+ nodesDisabledViaModularPipeline,
+ 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,
+ nodesDisabledViaModularPipeline,
+ 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 = ({
>