Skip to content

Commit 6bbe8ef

Browse files
[Ingest Pipelines] Add structure tree to details flyout (elastic#228792)
Addresses elastic#227405 https://github.com/user-attachments/assets/1473ade0-19cb-4892-a64a-2ed4374e3879 ## Summary This PR adds the Ingest pipeline structure tree to the ingest pipelines details flyout. **How to test:** 1. Run the following requests in Console (you can run them all at once) to create a sample ingest pipeline tree: <details> <summary>Console requests:</summary> ``` PUT _ingest/pipeline/level-8-1 { "processors": [] } PUT _ingest/pipeline/level-8-2 { "processors": [] } PUT _ingest/pipeline/level-7-1 { "processors": [ { "pipeline": { "name": "level-8-1" } }, { "pipeline": { "name": "level-8-2" } } ] } PUT _ingest/pipeline/level-7-2 { "processors": [ { "pipeline": { "name": "level-8-1" } }, { "pipeline": { "name": "level-8-2" } } ] } PUT _ingest/pipeline/level-6-1 { "processors": [ { "pipeline": { "name": "level-7-1" } }, { "pipeline": { "name": "level-7-2" } } ] } PUT _ingest/pipeline/level-6-2 { "processors": [ { "pipeline": { "name": "level-7-1" } }, { "pipeline": { "name": "level-7-2" } } ] } PUT _ingest/pipeline/level-5-1 { "processors": [ { "pipeline": { "name": "level-6-1" } }, { "pipeline": { "name": "level-6-2" } } ] } PUT _ingest/pipeline/level-5-2 { "processors": [ { "pipeline": { "name": "level-6-1" } }, { "pipeline": { "name": "level-6-2" } } ] } PUT _ingest/pipeline/level-4-1 { "processors": [ { "pipeline": { "name": "level-5-1" } }, { "pipeline": { "name": "level-5-2" } } ] } PUT _ingest/pipeline/level-4-2 { "processors": [ { "pipeline": { "name": "level-5-1" } }, { "pipeline": { "name": "level-5-2" } } ] } PUT _ingest/pipeline/level-3-1 { "processors": [ { "pipeline": { "name": "level-4-1" } }, { "pipeline": { "name": "level-4-2" } } ] } PUT _ingest/pipeline/level-3-2 { "processors": [ { "pipeline": { "name": "level-4-1" } }, { "pipeline": { "name": "level-4-2" } } ] } PUT _ingest/pipeline/level-2-1 { "processors": [ { "pipeline": { "name": "level-3-1" } }, { "pipeline": { "name": "level-3-2" } } ] } PUT _ingest/pipeline/level-2-2 { "processors": [ { "pipeline": { "name": "level-3-1" } }, { "pipeline": { "name": "level-3-2" } } ] } PUT _ingest/pipeline/level-1 { "processors": [ { "pipeline": { "name": "level-2-1" } }, { "pipeline": { "name": "level-2-2" } }, { "pipeline": { "name": "non-existing" } } ] } ``` </details> 2. Go to Ingest pipelines and click on any of the created pipelines. Verify that the tree is working as expected. Verify that the footer action buttons also work as expected (edit/clone/delete) 3. Verify that tree view is only displayed if the root has children. 4. Verify that navigating to a non-existing pipeline through the URL opens the flyout with an error banner and without the tree. 5. Verify that when you open a non-exisisting pipeline by clicking on a tree node, a warning banner is displayed. 6. Verify that in smaller screens only one panel is displayed at a time. 7. To test in Streams, enable streams with `POST kbn:/api/streams/_enable` and create sample date with `node scripts/synthtrace.js sample_logs --live --kibana=http://elastic:changeme@localhost:5601 --target=http://elastic:changeme@localhost:9200 --liveBucketSize=1000` --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
1 parent a00149d commit 6bbe8ef

32 files changed

Lines changed: 1060 additions & 658 deletions

File tree

x-pack/platform/packages/shared/ingest-pipelines/src/components/pipeline_structure_tree/create_tree_nodes/create_tree_nodes.test.tsx

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,68 +18,78 @@ const renderTreeNode = (node: ReturnType<typeof createTreeNodesFromPipelines>) =
1818

1919
describe('createTreeNodesFromPipelines', () => {
2020
it('renders a basic pipeline node label', () => {
21-
const setSelectedPipeline = jest.fn();
21+
const clickTreeNode = jest.fn();
22+
const clickMorePipelines = jest.fn();
2223
const pipeline: PipelineTreeNode = {
2324
pipelineName: 'root-pipeline',
2425
isManaged: false,
2526
isDeprecated: false,
2627
children: [],
2728
};
2829

29-
const node = createTreeNodesFromPipelines(pipeline, '', setSelectedPipeline);
30+
const node = createTreeNodesFromPipelines(pipeline, '', clickTreeNode, clickMorePipelines);
3031
const { getByTestId } = renderTreeNode(node);
3132

3233
expect(getByTestId('pipelineTreeNode-root-pipeline')).toBeInTheDocument();
3334
});
3435

3536
it('renders managed and deprecated icons', () => {
36-
const setSelectedPipeline = jest.fn();
37+
const clickTreeNode = jest.fn();
38+
const clickMorePipelines = jest.fn();
3739
const pipeline: PipelineTreeNode = {
3840
pipelineName: 'managed-deprecated',
3941
isManaged: true,
4042
isDeprecated: true,
4143
children: [],
4244
};
4345

44-
const node = createTreeNodesFromPipelines(pipeline, '', setSelectedPipeline);
46+
const node = createTreeNodesFromPipelines(pipeline, '', clickTreeNode, clickMorePipelines);
4547
const { getByTestId } = renderTreeNode(node);
4648

4749
expect(getByTestId('pipelineTreeNode-managed-deprecated-managedIcon')).toBeInTheDocument();
4850
expect(getByTestId('pipelineTreeNode-managed-deprecated-deprecatedIcon')).toBeInTheDocument();
4951
});
5052

51-
it('calls setSelectedPipeline when node label is clicked', () => {
52-
const setSelectedPipeline = jest.fn();
53+
it('calls clickTreeNode when node label is clicked', () => {
54+
const clickTreeNode = jest.fn();
55+
const clickMorePipelines = jest.fn();
5356
const pipeline: PipelineTreeNode = {
5457
pipelineName: 'test-pipeline',
5558
isManaged: false,
5659
isDeprecated: false,
5760
children: [],
5861
};
5962

60-
const node = createTreeNodesFromPipelines(pipeline, '', setSelectedPipeline);
63+
const node = createTreeNodesFromPipelines(pipeline, '', clickTreeNode, clickMorePipelines);
6164
const { getByTestId } = renderTreeNode(node);
6265

63-
fireEvent.click(getByTestId('pipelineTreeNodeLink-test-pipeline'));
64-
expect(setSelectedPipeline).toHaveBeenCalledWith('test-pipeline');
66+
fireEvent.click(getByTestId('pipelineTreeNode-test-pipeline'));
67+
expect(clickTreeNode).toHaveBeenCalledWith('test-pipeline');
6568
});
6669

6770
it('adds active class when selectedPipeline matches', () => {
68-
const setSelectedPipeline = jest.fn();
71+
const clickTreeNode = jest.fn();
72+
const clickMorePipelines = jest.fn();
6973
const pipeline: PipelineTreeNode = {
7074
pipelineName: 'selected-one',
7175
isManaged: false,
7276
isDeprecated: false,
7377
children: [],
7478
};
7579

76-
const node = createTreeNodesFromPipelines(pipeline, 'selected-one', setSelectedPipeline);
80+
const node = createTreeNodesFromPipelines(
81+
pipeline,
82+
'selected-one',
83+
clickTreeNode,
84+
clickMorePipelines
85+
);
7786

7887
expect(node.className).toContain('--active');
7988
});
8089

8190
it('adds a "+ more pipelines" label when max depth is reached', () => {
82-
const setSelectedPipeline = jest.fn();
91+
const clickTreeNode = jest.fn();
92+
const clickMorePipelines = jest.fn();
8393

8494
// Create a deeply nested tree exceeding MAX_TREE_LEVEL
8595
const deepTree = (level: number): PipelineTreeNode => {
@@ -100,7 +110,7 @@ describe('createTreeNodesFromPipelines', () => {
100110
};
101111

102112
const root = deepTree(MAX_TREE_LEVEL + 1);
103-
const node = createTreeNodesFromPipelines(root, '', setSelectedPipeline);
113+
const node = createTreeNodesFromPipelines(root, '', clickTreeNode, clickMorePipelines);
104114

105115
// Traverse to the level that contains the "more pipelines" node
106116
let current = node;
@@ -112,4 +122,29 @@ describe('createTreeNodesFromPipelines', () => {
112122
expect(finalLevelChildren).toHaveLength(1);
113123
expect(finalLevelChildren[0].id).toMatch(/-moreChildrenPipelines$/);
114124
});
125+
126+
it('calls clickMorePipelines when "+ more pipelines" is clicked', () => {
127+
const clickTreeNode = jest.fn();
128+
const clickMorePipelines = jest.fn();
129+
const pipeline: PipelineTreeNode = {
130+
pipelineName: 'test-pipeline',
131+
isManaged: false,
132+
isDeprecated: false,
133+
children: [
134+
{
135+
pipelineName: 'child',
136+
isManaged: true,
137+
isDeprecated: true,
138+
children: [],
139+
},
140+
],
141+
};
142+
143+
const node = createTreeNodesFromPipelines(pipeline, '', clickTreeNode, clickMorePipelines, 5);
144+
const { getByTestId } = renderTreeNode(node);
145+
146+
// Expand root to display the "More pipelines" node
147+
fireEvent.click(getByTestId('pipelineTreeNode-test-pipeline'));
148+
fireEvent.click(getByTestId('morePipelinesNodeLabel'));
149+
});
115150
});

x-pack/platform/packages/shared/ingest-pipelines/src/components/pipeline_structure_tree/create_tree_nodes/create_tree_nodes.tsx

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ import { PipelineTreeNodeLabel, MorePipelinesLabel } from '../tree_node_labels';
1717
*/
1818
export const createTreeNodesFromPipelines = (
1919
treeNode: PipelineTreeNode,
20-
selectedPipeline: string,
21-
setSelectedPipeline: (pipelineName: string) => void,
20+
selectedPipeline: string | undefined,
21+
clickTreeNode: (pipelineName: string) => void,
22+
clickMorePipelines: (name: string) => void,
2223
level: number = 1
2324
): Node => {
2425
const currentNode = {
@@ -28,34 +29,39 @@ export const createTreeNodesFromPipelines = (
2829
pipelineName={treeNode.pipelineName}
2930
isManaged={treeNode.isManaged}
3031
isDeprecated={treeNode.isDeprecated}
31-
setSelected={() => setSelectedPipeline(treeNode.pipelineName)}
3232
/>
3333
),
34+
'data-test-subj': `pipelineTreeNode-${treeNode.pipelineName}-moreChildrenPipelines`,
3435
className:
3536
(level === 1 ? 'cssTreeNode-root' : 'cssTreeNode-children') +
3637
(treeNode.pipelineName === selectedPipeline ? '--active' : ''),
37-
children: treeNode.children.length ? ([] as Node[]) : undefined,
38+
children: treeNode.children.length ? [] : undefined,
3839
isExpanded: level === 1,
39-
// Disable EUI's logic for activating tree node when expanding/collapsing them
40-
// We should only activate a tree node when we click on the pipeline name
41-
isActive: false,
42-
};
40+
callback: () => clickTreeNode(treeNode.pipelineName),
41+
} as unknown as Node;
4342

4443
if (level === MAX_TREE_LEVEL) {
45-
if (treeNode.children) {
44+
if (treeNode.children.length > 0) {
4645
const morePipelinesNode = {
4746
id: `${treeNode.pipelineName}-moreChildrenPipelines`,
4847
label: <MorePipelinesLabel count={treeNode.children.length} />,
4948
'data-test-subj': `pipelineTreeNode-${treeNode.pipelineName}-moreChildrenPipelines`,
5049
className: 'cssTreeNode-morePipelines',
51-
};
50+
callback: () => clickMorePipelines(treeNode.pipelineName),
51+
} as unknown as Node;
5252
currentNode.children!.push(morePipelinesNode);
5353
}
5454
return currentNode;
5555
}
5656
treeNode.children.forEach((node) => {
5757
currentNode.children!.push(
58-
createTreeNodesFromPipelines(node, selectedPipeline, setSelectedPipeline, level + 1)
58+
createTreeNodesFromPipelines(
59+
node,
60+
selectedPipeline,
61+
clickTreeNode,
62+
clickMorePipelines,
63+
level + 1
64+
)
5965
);
6066
});
6167
return currentNode;

x-pack/platform/packages/shared/ingest-pipelines/src/components/pipeline_structure_tree/pipeline_structure_tree.tsx

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* 2.0.
66
*/
77

8-
import React, { useState } from 'react';
8+
import React from 'react';
99
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiTreeView, useEuiTheme } from '@elastic/eui';
1010
import { FormattedMessage } from '@kbn/i18n-react';
1111
import type { PipelineTreeNode } from './types';
@@ -14,11 +14,15 @@ import { getStyles } from './styles';
1414

1515
export interface PipelineStructureTreeProps {
1616
pipelineTree: PipelineTreeNode;
17+
selectedPipeline: string | undefined;
1718
/**
1819
* Specifies whether the tree is an extension of the main tree; i.e. displayed
1920
* when the user clicks on the last "+X more pipelines" tree node.
2021
*/
2122
isExtension: boolean;
23+
clickTreeNode: (name: string) => void;
24+
clickMorePipelines: (name: string) => void;
25+
goBack: () => void;
2226
}
2327

2428
/**
@@ -27,36 +31,41 @@ export interface PipelineStructureTreeProps {
2731
* corresponding pipelines from the children node.
2832
* See more at https://www.elastic.co/docs/reference/enrich-processor/pipeline-processor
2933
*/
30-
export const PipelineStructureTree = ({
31-
pipelineTree,
32-
isExtension,
33-
}: PipelineStructureTreeProps) => {
34-
const { euiTheme } = useEuiTheme();
35-
const styles = getStyles(euiTheme, isExtension);
36-
37-
const [selectedPipeline, setSelectedPipeline] = useState(pipelineTree.pipelineName);
38-
39-
const treeNode = createTreeNodesFromPipelines(
34+
export const PipelineStructureTree = React.memo(
35+
({
4036
pipelineTree,
4137
selectedPipeline,
42-
setSelectedPipeline
43-
);
38+
isExtension,
39+
clickTreeNode,
40+
clickMorePipelines,
41+
goBack,
42+
}: PipelineStructureTreeProps) => {
43+
const { euiTheme } = useEuiTheme();
44+
const styles = getStyles(euiTheme, isExtension);
45+
46+
const treeNode = createTreeNodesFromPipelines(
47+
pipelineTree,
48+
selectedPipeline,
49+
clickTreeNode,
50+
clickMorePipelines
51+
);
4452

45-
return (
46-
<EuiFlexGroup direction="column" gutterSize="none" alignItems="flexStart">
47-
{isExtension && (
48-
<EuiFlexItem>
49-
<EuiButtonEmpty iconType="arrowLeft" onClick={() => {}}>
50-
<FormattedMessage
51-
id="ingestPipelines.pipelineStructureTree.backToMainTreeNodeLabel"
52-
defaultMessage="Back to previous pipelines"
53-
/>
54-
</EuiButtonEmpty>
53+
return (
54+
<EuiFlexGroup direction="column" gutterSize="none" alignItems="flexStart">
55+
{isExtension && (
56+
<EuiFlexItem grow={false}>
57+
<EuiButtonEmpty iconType="arrowLeft" onClick={goBack}>
58+
<FormattedMessage
59+
id="ingestPipelines.pipelineStructureTree.backToMainTreeNodeLabel"
60+
defaultMessage="Back to previous pipelines"
61+
/>
62+
</EuiButtonEmpty>
63+
</EuiFlexItem>
64+
)}
65+
<EuiFlexItem css={{ marginLeft: isExtension ? euiTheme.size.l : '0' }}>
66+
<EuiTreeView items={[treeNode]} showExpansionArrows={true} css={styles} />
5567
</EuiFlexItem>
56-
)}
57-
<EuiFlexItem css={{ marginLeft: isExtension ? euiTheme.size.l : '0' }}>
58-
<EuiTreeView items={[treeNode]} showExpansionArrows={true} css={styles} />
59-
</EuiFlexItem>
60-
</EuiFlexGroup>
61-
);
62-
};
68+
</EuiFlexGroup>
69+
);
70+
}
71+
);

x-pack/platform/packages/shared/ingest-pipelines/src/components/pipeline_structure_tree/styles.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export const getStyles = (euiTheme: EuiThemeComputed, isExtension: boolean) => c
4040
.cssTreeNode-morePipelines {
4141
margin-left: ${euiTheme.size.base};
4242
background-color: ${euiTheme.colors.backgroundLightPrimary};
43+
color: ${euiTheme.colors.textPrimary} !important;
4344
border: none;
4445
}
4546
`;

x-pack/platform/packages/shared/ingest-pipelines/src/components/pipeline_structure_tree/tree_node_labels/more_pipelines_label.tsx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,20 @@
66
*/
77

88
import React from 'react';
9-
import { EuiLink } from '@elastic/eui';
10-
import { FormattedMessage } from '@kbn/i18n-react';
9+
import { i18n } from '@kbn/i18n';
10+
import { EuiText } from '@elastic/eui';
1111

1212
interface MorePipelinesLabelProps {
1313
count: number;
1414
}
1515

1616
export const MorePipelinesLabel = ({ count }: MorePipelinesLabelProps) => {
1717
return (
18-
<EuiLink color="primary" onClick={() => {}} data-test-subj="morePipelinesNodeLabel">
19-
<FormattedMessage
20-
id="ingestPipelines.pipelineStructureTree.morePipelinesTreeNodeLabel"
21-
defaultMessage="+{count} more {count, plural,one {pipeline} other {pipelines}}"
22-
values={{ count }}
23-
/>
24-
</EuiLink>
18+
<EuiText data-test-subj="morePipelinesNodeLabel">
19+
{i18n.translate('ingestPipelines.pipelineStructureTree.morePipelinesTreeNodeLabel', {
20+
defaultMessage: '+{count} more {count, plural,one {pipeline} other {pipelines}}',
21+
values: { count },
22+
})}
23+
</EuiText>
2524
);
2625
};

0 commit comments

Comments
 (0)