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/filter-row/filter-row.js b/src/components/filter-row/filter-row.js
new file mode 100755
index 0000000000..baa8cc772c
--- /dev/null
+++ b/src/components/filter-row/filter-row.js
@@ -0,0 +1,66 @@
+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 './filter-row.scss';
+
+export const FilterRow = ({
+ 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}
+
+ );
+};
diff --git a/src/components/filter-row/filter-row.scss b/src/components/filter-row/filter-row.scss
new file mode 100644
index 0000000000..ff23a5d05e
--- /dev/null
+++ b/src/components/filter-row/filter-row.scss
@@ -0,0 +1,54 @@
+@use '../../styles/variables' as var;
+@use '../node-list/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/filter-row/filter-row.test.js b/src/components/filter-row/filter-row.test.js
new file mode 100644
index 0000000000..3e1a31a203
--- /dev/null
+++ b/src/components/filter-row/filter-row.test.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import { FilterRow } from './filter-row';
+
+describe('FilterRow 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/node-list/components/row/row.js b/src/components/node-list/components/row/row.js
new file mode 100755
index 0000000000..416bcb4947
--- /dev/null
+++ b/src/components/node-list/components/row/row.js
@@ -0,0 +1,113 @@
+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 './row.scss';
+
+const Row = ({
+ 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 Row;
diff --git a/src/components/node-list/components/row/row.scss b/src/components/node-list/components/row/row.scss
new file mode 100755
index 0000000000..99606506ca
--- /dev/null
+++ b/src/components/node-list/components/row/row.scss
@@ -0,0 +1,83 @@
+@use '../../../../styles/variables' as var;
+@use '../../styles/variables';
+
+.MuiTreeItem-iconContainer svg {
+ z-index: var.$zindex-MuiTreeItem-icon;
+}
+
+.row {
+ align-items: center;
+ cursor: default;
+ display: flex;
+ height: 32px;
+ position: relative;
+ transform: translate(0, 0);
+
+ &:hover,
+ &--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 {
+ .row__type-icon path {
+ opacity: 1;
+ }
+}
+
+.row--active::before,
+.row--selected::before,
+.row:hover::before {
+ opacity: 1;
+}
+
+.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;
+ }
+}
+
+.row__type-icon {
+ &--nested > * {
+ opacity: 0.3;
+ }
+
+ &--faded > * {
+ opacity: 0.2;
+ }
+
+ &--active,
+ &--selected,
+ .row--visible:hover &,
+ [data-whatintent='keyboard'] .row__text:focus & {
+ > * {
+ opacity: 1;
+ }
+
+ &--faded > * {
+ opacity: 0.55;
+ }
+ }
+}
diff --git a/src/components/node-list/components/row/row.test.js b/src/components/node-list/components/row/row.test.js
new file mode 100644
index 0000000000..42294ab8dd
--- /dev/null
+++ b/src/components/node-list/components/row/row.test.js
@@ -0,0 +1,66 @@
+import React from 'react';
+import Row from './row';
+import { setup } from '../../../../utils/state.mock';
+
+// Mock props
+const mockProps = {
+ name: 'Test Row',
+ kind: 'modular-pipeline',
+ active: false,
+ disabled: false,
+ selected: false,
+ visible: true,
+ onMouseEnter: jest.fn(),
+ onMouseLeave: jest.fn(),
+ onClick: jest.fn(),
+ icon: null,
+ type: 'modularPipeline',
+ checked: true,
+ focused: false,
+};
+
+describe('Row Component', () => {
+ it('renders without crashing', () => {
+ expect(() => setup.mount(
)).not.toThrow();
+ });
+
+ it('handles mouseenter events', () => {
+ const wrapper = setup.mount(
);
+ const nodeRow = () => wrapper.find('.row');
+ nodeRow().simulate('mouseenter');
+ expect(mockProps.onMouseEnter.mock.calls.length).toEqual(1);
+ });
+
+ it('handles mouseleave events', () => {
+ const wrapper = setup.mount(
);
+ const nodeRow = () => wrapper.find('.row');
+ nodeRow().simulate('mouseleave');
+ expect(mockProps.onMouseLeave.mock.calls.length).toEqual(1);
+ });
+
+ it('applies the row--active class when active is true', () => {
+ const wrapper = setup.mount(
);
+ expect(wrapper.find('.row').hasClass('row--active')).toBe(true);
+ });
+
+ it('applies the row--selected class when selected is true', () => {
+ const wrapper = setup.mount(
);
+ expect(wrapper.find('.row').hasClass('row--selected')).toBe(true);
+ });
+
+ it('applies the row--selected class when highlight is true and isSlicingPipelineApplied is false', () => {
+ const wrapper = setup.mount(
+
+ );
+ expect(wrapper.find('.row').hasClass('row--selected')).toBe(true);
+ });
+
+ it('applies the overwrite class if not selected or active', () => {
+ const activeNodeWrapper = setup.mount(
+
+ );
+ expect(activeNodeWrapper.find('.row').hasClass('row--overwrite')).toBe(
+ true
+ );
+ });
+});
diff --git a/src/components/node-list/index.js b/src/components/node-list/index.js
index 74353d8944..da2ea0984a 100644
--- a/src/components/node-list/index.js
+++ b/src/components/node-list/index.js
@@ -26,7 +26,11 @@ import {
} from '../../selectors/nodes';
import { toggleTagActive, toggleTagFilter } from '../../actions/tags';
import { toggleTypeDisabled } from '../../actions/node-type';
-import { toggleParametersHovered, toggleFocusMode } from '../../actions';
+import {
+ toggleParametersHovered,
+ toggleFocusMode,
+ toggleHoveredFocusMode,
+} from '../../actions';
import {
toggleModularPipelineActive,
toggleModularPipelineDisabled,
@@ -64,6 +68,7 @@ const NodeListProvider = ({
onToggleModularPipelineExpanded,
onToggleTypeDisabled,
onToggleFocusMode,
+ onToggleHoveredFocusMode,
modularPipelinesTree,
focusMode,
disabledModularPipeline,
@@ -100,7 +105,7 @@ const NodeListProvider = ({
const groups = getGroups({ items });
- const onItemClick = (item) => {
+ const onItemClick = (event, item) => {
if (isGroupType(item.type)) {
onGroupItemChange(item, item.checked);
} else if (isModularPipelineType(item.type)) {
@@ -118,6 +123,9 @@ const NodeListProvider = ({
}
}
}
+
+ // to prevent page reload on form submission
+ event.preventDefault();
};
// To get existing values from URL query parameters
@@ -315,6 +323,7 @@ const NodeListProvider = ({
onItemClick={onItemClick}
onItemMouseEnter={onItemMouseEnter}
onItemMouseLeave={onItemMouseLeave}
+ onToggleHoveredFocusMode={onToggleHoveredFocusMode}
onItemChange={onItemChange}
focusMode={focusMode}
disabledModularPipeline={disabledModularPipeline}
@@ -371,6 +380,9 @@ export const mapDispatchToProps = (dispatch) => ({
onToggleFocusMode: (modularPipeline) => {
dispatch(toggleFocusMode(modularPipeline));
},
+ onToggleHoveredFocusMode: (active) => {
+ dispatch(toggleHoveredFocusMode(active));
+ },
onResetSlicePipeline: () => {
dispatch(resetSlicePipeline());
},
diff --git a/src/components/node-list/node-list-group.js b/src/components/node-list/node-list-group.js
index 9b54a2d72b..4d68df9b19 100644
--- a/src/components/node-list/node-list-group.js
+++ b/src/components/node-list/node-list-group.js
@@ -1,6 +1,6 @@
import React from 'react';
import classnames from 'classnames';
-import NodeListRow from './node-list-row';
+import { FilterRow } from '../filter-row/filter-row';
import NodeRowList from './node-list-row-list';
export const NodeListGroup = ({
@@ -35,12 +35,12 @@ export const NodeListGroup = ({
)}
>
-
+
{
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-row-list.js b/src/components/node-list/node-list-row-list.js
index 4566fbaafc..fac2b346e6 100644
--- a/src/components/node-list/node-list-row-list.js
+++ b/src/components/node-list/node-list-row-list.js
@@ -1,7 +1,9 @@
import React from 'react';
import modifiers from '../../utils/modifiers';
-import NodeListRow, { nodeListRowHeight } from './node-list-row';
+import { FilterRow } from '../filter-row/filter-row';
+import { nodeListRowHeight } from '../../config';
import LazyList from '../lazy-list';
+import { getDataTestAttribute } from '../../utils/get-data-test-attribute';
const NodeRowList = ({
items = [],
@@ -9,24 +11,12 @@ const NodeRowList = ({
collapsed,
onItemClick,
onItemChange,
- onItemMouseEnter,
- onItemMouseLeave,
}) => (
(end - start) * nodeListRowHeight}
total={items.length}
>
- {({
- start,
- end,
- total,
- listRef,
- upperRef,
- lowerRef,
- listStyle,
- upperStyle,
- lowerStyle,
- }) => (
+ {({ start, end, listRef, listStyle }) => (
- - 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"
+ onClick={() => onItemClick(item)}
+ parentClassName={'node-list-filter-row'}
+ visible={item.visible}
+ indicatorIcon={item.visibleIcon}
/>
))}
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-tree-item.js b/src/components/node-list/node-list-tree-item.js
index 5a08c0ca25..81c5cebfa3 100644
--- a/src/components/node-list/node-list-tree-item.js
+++ b/src/components/node-list/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 Row from './components/row/row';
+import { getDataTestAttribute } from '../../utils/get-data-test-attribute';
const arrowIconColor = '#8e8e90';
@@ -12,46 +14,51 @@ const NodeListTreeItem = ({
onItemMouseEnter,
onItemMouseLeave,
onItemChange,
+ onToggleHoveredFocusMode,
children,
isSlicingPipelineApplied,
}) => (
}
expandIcon={}
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/node-list-tree.js
index fa89c3fec8..df9dd33a68 100644
--- a/src/components/node-list/node-list-tree.js
+++ b/src/components/node-list/node-list-tree.js
@@ -117,6 +117,7 @@ const TreeListProvider = ({
onItemChange,
onItemMouseEnter,
onItemMouseLeave,
+ onToggleHoveredFocusMode,
onItemClick,
onNodeToggleExpanded,
focusMode,
@@ -161,6 +162,7 @@ const TreeListProvider = ({
onItemMouseEnter={onItemMouseEnter}
onItemMouseLeave={onItemMouseLeave}
onItemChange={onItemChange}
+ onToggleHoveredFocusMode={onToggleHoveredFocusMode}
onItemClick={onItemClick}
key={uniqueId(node.id)}
isSlicingPipelineApplied={isSlicingPipelineApplied}
@@ -231,6 +233,7 @@ const TreeListProvider = ({
onItemMouseEnter={onItemMouseEnter}
onItemMouseLeave={onItemMouseLeave}
onItemChange={onItemChange}
+ onToggleHoveredFocusMode={onToggleHoveredFocusMode}
onItemClick={onItemClick}
key={uniqueId(node.id)}
isSlicingPipelineApplied={isSlicingPipelineApplied}
diff --git a/src/components/node-list/node-list.js b/src/components/node-list/node-list.js
index 0106c0594c..08f415b4fb 100644
--- a/src/components/node-list/node-list.js
+++ b/src/components/node-list/node-list.js
@@ -24,6 +24,7 @@ const NodeList = ({
onItemClick,
onItemMouseEnter,
onItemMouseLeave,
+ onToggleHoveredFocusMode,
onItemChange,
onModularPipelineToggleExpanded,
focusMode,
@@ -65,6 +66,7 @@ const NodeList = ({
onItemClick={onItemClick}
onItemMouseEnter={onItemMouseEnter}
onItemMouseLeave={onItemMouseLeave}
+ onToggleHoveredFocusMode={onToggleHoveredFocusMode}
onItemChange={onItemChange}
onNodeToggleExpanded={onModularPipelineToggleExpanded}
focusMode={focusMode}
diff --git a/src/components/node-list/node-list.test.js b/src/components/node-list/node-list.test.js
index edceb82879..83305c5af9 100644
--- a/src/components/node-list/node-list.test.js
+++ b/src/components/node-list/node-list.test.js
@@ -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);
@@ -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);
@@ -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,7 +192,7 @@ 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', () => {
@@ -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,11 +248,8 @@ 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);
@@ -264,31 +261,6 @@ describe('NodeList', () => {
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(
@@ -331,19 +303,29 @@ describe('NodeList', () => {
);
- 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', () => {
@@ -383,6 +365,7 @@ describe('NodeList', () => {
});
});
+ // FILTER GROUP
describe('node list', () => {
it('renders the correct number of tags in the filter panel', () => {
const wrapper = setup.mount(
@@ -391,20 +374,22 @@ describe('NodeList', () => {
);
const nodeList = wrapper.find(
- '.pipeline-nodelist__list--nested .pipeline-nodelist__row'
+ '.pipeline-nodelist__list--nested .node-list-filter-row'
);
// const nodes = getNodeData(mockState.spaceflights);
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
);
@@ -437,33 +422,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', {
@@ -507,7 +472,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');
diff --git a/src/components/node-list/styles/_group.scss b/src/components/node-list/styles/_group.scss
index 0d456bd2f5..f9eb5d66f8 100644
--- a/src/components/node-list/styles/_group.scss
+++ b/src/components/node-list/styles/_group.scss
@@ -26,49 +26,6 @@
$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;
@@ -78,12 +35,13 @@ $placeholder-fade: 120px;
// Avoid pixel gap above when scrolling.
transform: translateY(-1px);
- .pipeline-nodelist__row__text {
+ .pipeline-nodelist__row__text,
+ .row-text {
position: relative;
opacity: 0.65;
}
- .pipeline-nodelist__row__text .pipeline-nodelist__row__label {
+ .row-text .row-text__label {
font-size: 1.3em;
}
}
@@ -171,3 +129,10 @@ $placeholder-fade: 120px;
transform: rotate(90deg);
}
}
+
+// Bright row text when the parent groups are all unchecked
+.pipeline-nodelist__group--all-unchecked {
+ .row-text__label--kind-filter {
+ opacity: 1;
+ }
+}
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/node-list/styles/node-list.scss b/src/components/node-list/styles/node-list.scss
index 3d45c4f370..7cffa2b13b 100644
--- a/src/components/node-list/styles/node-list.scss
+++ b/src/components/node-list/styles/node-list.scss
@@ -2,9 +2,6 @@
@use '../../../styles/variables' as colors;
@use './group';
@use './panels';
-@use './row';
-@use './row-label';
-@use './row-toggle';
@use './section';
@use './variables';
@@ -84,12 +81,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
+ .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/ui/row-text/row-text.js b/src/components/ui/row-text/row-text.js
new file mode 100644
index 0000000000..5ee6ba33ea
--- /dev/null
+++ b/src/components/ui/row-text/row-text.js
@@ -0,0 +1,51 @@
+import React from 'react';
+import classnames from 'classnames';
+import { replaceAngleBracketMatches } from '../../../utils';
+import { getDataTestAttribute } from '../../../utils/get-data-test-attribute';
+
+import './row-text.scss';
+
+export const RowText = ({
+ dataTest,
+ disabled,
+ faded,
+ kind,
+ label,
+ name,
+ onClick,
+ onMouseEnter,
+ onMouseLeave,
+ rowType,
+}) => {
+ return (
+
+ );
+};
diff --git a/src/components/ui/row-text/row-text.scss b/src/components/ui/row-text/row-text.scss
new file mode 100644
index 0000000000..11e8c1c8eb
--- /dev/null
+++ b/src/components/ui/row-text/row-text.scss
@@ -0,0 +1,62 @@
+@use '../../../styles/variables' as var;
+@use '../../node-list/styles/variables';
+
+.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;
+
+ // add padding between icon and text
+ &--tree {
+ padding: variables.$row-padding-y 0 variables.$row-padding-y 1em;
+ }
+
+ &--faded {
+ pointer-events: none;
+ }
+
+ &:focus {
+ outline: none;
+ box-shadow: 0 0 0 4px var.$blue-300 inset;
+
+ [data-whatintent='mouse'] & {
+ box-shadow: none;
+ }
+ }
+}
+
+.row-text__label {
+ overflow: hidden;
+ font-size: 1.4em;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+
+ &--faded {
+ opacity: 0.65;
+ }
+
+ &--disabled {
+ opacity: 0.3;
+ }
+
+ b {
+ color: var(--color-nodelist-highlight);
+ font-weight: normal;
+ }
+}
diff --git a/src/components/ui/toggle-control/toggle-control.js b/src/components/ui/toggle-control/toggle-control.js
new file mode 100755
index 0000000000..968b717daf
--- /dev/null
+++ b/src/components/ui/toggle-control/toggle-control.js
@@ -0,0 +1,75 @@
+import React from 'react';
+import classnames from 'classnames';
+import { getDataTestAttribute } from '../../../utils/get-data-test-attribute';
+
+import './toggle-control.scss';
+
+export const ToggleControl = ({
+ className,
+ focusChecked,
+ IconComponent,
+ disabled,
+ id,
+ isChecked,
+ kind,
+ name,
+ onChange,
+ onToggleHoveredFocusMode,
+ selected,
+ dataIconType,
+}) => {
+ const handleMouseHover = (isEntering) =>
+ onToggleHoveredFocusMode && onToggleHoveredFocusMode(isEntering);
+
+ const iconClassNames = classnames(
+ className,
+ 'toggle-control--icon',
+ `toggle-control--icon--kind-${kind}`,
+ {
+ 'toggle-control--icon--checked': isChecked,
+ 'toggle-control--icon--unchecked': !isChecked,
+ 'toggle-control--icon--focus-checked': focusChecked,
+ 'toggle-control--icon--disabled': disabled,
+ }
+ );
+
+ const labelClassNames = classnames(
+ 'toggle-control',
+ `toggle-control--kind-${kind}`,
+ {
+ 'toggle-control--selected': selected,
+ }
+ );
+
+ const dataTestValue = getDataTestAttribute(
+ 'toggle-control',
+ kind === 'focus' ? 'focusMode' : 'visible',
+ name
+ );
+
+ return (
+
+ );
+};
diff --git a/src/components/node-list/styles/_row-toggle.scss b/src/components/ui/toggle-control/toggle-control.scss
similarity index 62%
rename from src/components/node-list/styles/_row-toggle.scss
rename to src/components/ui/toggle-control/toggle-control.scss
index d9220bad90..181770d3ad 100644
--- a/src/components/node-list/styles/_row-toggle.scss
+++ b/src/components/ui/toggle-control/toggle-control.scss
@@ -1,10 +1,8 @@
@use '../../../styles/mixins' as mixins;
@use '../../../styles/variables' as colors;
-@use './variables';
+@use '../../node-list/styles/variables';
-// --- Toggle ---//
-
-.pipeline-row__toggle {
+.toggle-control {
cursor: pointer;
&--kind-element {
@@ -14,10 +12,6 @@
&--kind-element:nth-of-type(2) {
margin: 0 8px 0 -8px;
}
-
- &--disabled {
- display: none;
- }
}
@include mixins.transparentColour(
@@ -26,29 +20,35 @@
variables.$row-selected-dark
);
-.pipeline-row__toggle--selected::before {
+.toggle-control--selected::before {
opacity: 1;
}
-.pipeline-nodelist__row__checkbox {
+.toggle-control__checkbox {
@include mixins.screenReaderOnly;
}
-// --- Toggle icon ---//
-
-.pipeline-row__toggle-icon {
- width: variables.$toggle-icon-size;
- height: variables.$toggle-icon-size;
+.toggle-control--icon {
+ width: variables.$toggle-icon-size !important;
+ height: variables.$toggle-icon-size !important;
padding: variables.$toggle-icon-padding;
border-radius: 50%;
- .pipeline-nodelist__row__checkbox:focus + & {
+ &--disabled {
+ display: none !important;
+ }
+
+ .toggle-control__checkbox:focus + & {
outline: none;
[data-whatintent='keyboard'] & {
box-shadow: 0 0 0 3px colors.$blue-300 inset;
}
}
+
+ &.toggle-control--icon--focus-checked {
+ fill: colors.$blue-300;
+ }
}
// There are two kinds of toggle icon, with different styling:
@@ -71,26 +71,26 @@ $element-icon-opacity-0: 0;
$element-icon-opacity-1: 0.55;
$element-icon-opacity-2: 1;
-.pipeline-row__toggle-icon--kind-element {
+.toggle-control--icon--kind-element {
// Change opacity on the SVG's child elements instead, in order to
// maintain 100% opacity outline on parent SVG on keyboard focus
> * {
opacity: $element-icon-opacity-0;
}
- .pipeline-nodelist__row:hover & {
+ .node-list-tree-item-row:hover & {
> * {
opacity: $element-icon-opacity-1;
}
- &.pipeline-row__toggle-icon--focus-checked {
+ &.toggle-control--icon--focus-checked {
> * {
opacity: $element-icon-opacity-2;
}
}
}
- .pipeline-nodelist__row &:hover {
+ .node-list-tree-item-row &:hover {
> * {
opacity: $element-icon-opacity-2;
}
@@ -101,14 +101,14 @@ $element-icon-opacity-2: 1;
opacity: $element-icon-opacity-1;
}
- &.pipeline-row__toggle-icon--checked {
+ &.toggle-control--icon--checked {
> * {
opacity: $element-icon-opacity-2;
}
}
}
- &.pipeline-row__toggle-icon--focus-checked {
+ &.toggle-control--icon--focus-checked {
> * {
opacity: $element-icon-opacity-2;
}
@@ -139,93 +139,58 @@ $filter-icon-opacity-1: 0.55;
$filter-icon-opacity-2: 0.9;
$filter-icon-opacity-3: 1;
-.pipeline-row__toggle-icon--kind-filter {
+.toggle-control--icon--kind-filter {
// Change opacity on the SVG's child elements instead, in order to
// maintain 100% opacity outline on parent SVG on keyboard focus
> * {
opacity: $filter-icon-opacity-1;
}
- .pipeline-nodelist__heading &.pipeline-row__toggle-icon--all-unchecked > * {
- opacity: $filter-icon-opacity-0;
- }
-
- &.pipeline-row__toggle-icon--all-unchecked {
+ &.toggle-control--icon--all-unchecked,
+ .pipeline-nodelist__heading &.toggle-control--icon--all-unchecked > * {
> * {
- opacity: $filter-icon-opacity-1;
+ opacity: $filter-icon-opacity-0;
}
}
- .pipeline-nodelist__row:hover & {
+ .node-list-tree-item-row:hover & {
> * {
opacity: $filter-icon-opacity-1;
}
- }
-
- .pipeline-nodelist__row:hover & {
- &.pipeline-row__toggle-icon--parent:hover {
- > * {
- opacity: $filter-icon-opacity-2;
- }
- }
- }
- .pipeline-nodelist__row & {
- &.pipeline-row__toggle-icon--checked {
+ &.toggle-control--icon--parent:hover,
+ &.toggle-control--icon--checked,
+ &.toggle-control--icon--child.toggle-control--icon--checked {
> * {
- opacity: $filter-icon-opacity-2;
- }
- }
- }
-
- .pipeline-nodelist__row:hover & {
- &.pipeline-row__toggle-icon--child {
- &.pipeline-row__toggle-icon--checked {
- > * {
- opacity: $filter-icon-opacity-3;
- }
- }
- }
- }
-
- .pipeline-nodelist__row & {
- &.pipeline-row__toggle-icon--parent:hover {
- &.pipeline-row__toggle-icon--checked {
- > * {
- opacity: $filter-icon-opacity-3;
- }
+ opacity: $filter-icon-opacity-2; // Increase opacity for checked or parent hover
}
}
}
[data-whatintent='keyboard'] input:focus + & {
> * {
- opacity: $filter-icon-opacity-2;
+ opacity: $filter-icon-opacity-2; // Increase opacity on keyboard focus
}
- &.pipeline-row__toggle-icon--checked {
+ &.toggle-control--icon--checked {
> * {
- opacity: $filter-icon-opacity-3;
+ opacity: $filter-icon-opacity-3; // Further increase for checked on focus
}
}
}
-}
-
-// --- Toggle (kind=filter) icon fills and strokes ---//
-.pipeline-row__toggle-icon--kind-filter {
- &.pipeline-row__toggle-icon--checked {
+ &.toggle-control--icon--checked {
fill: var(--color-nodelist-filter-indicator-on);
stroke: var(--color-nodelist-filter-indicator-on);
}
- &.pipeline-row__toggle-icon--unchecked {
+ &.toggle-control--icon--unchecked {
fill: none;
stroke: var(--color-nodelist-filter-indicator-off);
}
- .pipeline-nodelist__row:hover &.pipeline-row__toggle-icon--all-unchecked,
- &.pipeline-row__toggle-icon--parent {
+ .node-list-tree-item-row:hover &.toggle-control--icon--all-unchecked,
+ &.toggle-control--icon--parent {
fill: colors.$blue-300;
stroke: colors.$blue-300;
}
diff --git a/src/components/ui/toggle-control/toggle-control.test.js b/src/components/ui/toggle-control/toggle-control.test.js
new file mode 100644
index 0000000000..e99cccff0b
--- /dev/null
+++ b/src/components/ui/toggle-control/toggle-control.test.js
@@ -0,0 +1,58 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { ToggleControl } from './toggle-control';
+
+describe('ToggleControl', () => {
+ const baseProps = {
+ name: 'Test Node',
+ onChange: jest.fn(),
+ onToggleHoveredFocusMode: jest.fn(),
+ };
+
+ it('applies correct class for kind prop', () => {
+ const kinds = ['modularPipeline', 'data', 'task'];
+ kinds.forEach((kind) => {
+ const props = { ...baseProps, kind };
+ const wrapper = shallow();
+ expect(wrapper.hasClass(`toggle-control--kind-${kind}`)).toBe(true);
+ });
+ });
+
+ it('does not apply "all-unchecked" class when allUnchecked is false', () => {
+ const props = { ...baseProps, allUnchecked: false };
+ const wrapper = shallow();
+ expect(wrapper.hasClass('toggle-control--icon--all-unchecked')).toBe(false);
+ });
+
+ it('does not apply "disabled" class when disabled is false', () => {
+ const props = { ...baseProps, disabled: false };
+ const wrapper = shallow();
+ expect(wrapper.hasClass('toggle-control--disabled')).toBe(false);
+ });
+
+ it('does not apply "checked" class when isChecked is false', () => {
+ const props = { ...baseProps, isChecked: false };
+ const wrapper = shallow();
+ expect(wrapper.hasClass('toggle-control--icon--checked')).toBe(false);
+ });
+
+ it('does not apply "parent" class when isParent is false', () => {
+ const props = { ...baseProps, isParent: false };
+ const wrapper = shallow();
+ expect(wrapper.hasClass('toggle-control--icon--parent')).toBe(false);
+ });
+
+ it('does not trigger onToggleHoveredFocusMode when not provided', () => {
+ const props = { ...baseProps, onToggleHoveredFocusMode: undefined };
+ const wrapper = shallow();
+ wrapper.simulate('mouseenter');
+ expect(() => wrapper.simulate('mouseenter')).not.toThrow();
+ });
+
+ it('triggers onToggleHoveredFocusMode when provided', () => {
+ const props = { ...baseProps };
+ const wrapper = shallow();
+ wrapper.simulate('mouseenter');
+ expect(props.onToggleHoveredFocusMode).toHaveBeenCalled();
+ });
+});
diff --git a/src/config.js b/src/config.js
index 6bdaa8a1a2..daa602385a 100644
--- a/src/config.js
+++ b/src/config.js
@@ -35,6 +35,9 @@ export const codeSidebarWidth = {
open: 480,
};
+// The exact fixed height of a row as measured by getBoundingClientRect()
+export const nodeListRowHeight = 32;
+
// These colours variables come from styles/variables
const slate600 = '#0e222d';
const slate200 = '#21333e';
diff --git a/tools/test-lib/react-app/app.test.js b/tools/test-lib/react-app/app.test.js
index 07354db84b..d7afff30e9 100644
--- a/tools/test-lib/react-app/app.test.js
+++ b/tools/test-lib/react-app/app.test.js
@@ -17,9 +17,8 @@ describe('lib-test', () => {
*/
const testFirstNodeNameMatch = (container, key) => {
const firstNodeName = container
- .querySelector('.pipeline-nodelist__row')
- .querySelector('.pipeline-nodelist__row__text--tree')
- .querySelector('.pipeline-nodelist__row__label')
+ .querySelector('.node-list-tree-item-row')
+ .querySelector('.row-text__label')
.textContent.trim();
const modularPipelinesTree = dataSources[key]().modular_pipelines;