Skip to content

Commit 9a06509

Browse files
authored
[Ingest Pipelines] Offer create non existing custom pipeline (#209103)
Fixes #183992
1 parent fa76d89 commit 9a06509

File tree

13 files changed

+254
-74
lines changed

13 files changed

+254
-74
lines changed

src/platform/plugins/shared/es_ui_shared/__packages_do_not_import__/authorization/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ export interface Error {
2020
error: string;
2121
cause?: string[];
2222
message?: string;
23+
statusCode?: number;
2324
}

x-pack/platform/plugins/private/translations/translations/fr-FR.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22611,7 +22611,6 @@
2261122611
"xpack.ingestPipelines.list.loadingMessage": "Chargement des pipelines...",
2261222612
"xpack.ingestPipelines.list.loadPipelineReloadButton": "Réessayer",
2261322613
"xpack.ingestPipelines.list.manageProcessorsLinkText": "Gérer les processeurs",
22614-
"xpack.ingestPipelines.list.notFoundFlyoutMessage": "Pipeline introuvable",
2261522614
"xpack.ingestPipelines.list.pipelineDetails.cloneActionLabel": "Cloner",
2261622615
"xpack.ingestPipelines.list.pipelineDetails.closeButtonLabel": "Fermer",
2261722616
"xpack.ingestPipelines.list.pipelineDetails.deleteActionLabel": "Supprimer",

x-pack/platform/plugins/private/translations/translations/ja-JP.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22473,7 +22473,6 @@
2247322473
"xpack.ingestPipelines.list.loadingMessage": "パイプラインを読み込み中...",
2247422474
"xpack.ingestPipelines.list.loadPipelineReloadButton": "再試行",
2247522475
"xpack.ingestPipelines.list.manageProcessorsLinkText": "プロセッサーを管理",
22476-
"xpack.ingestPipelines.list.notFoundFlyoutMessage": "パイプラインが見つかりません",
2247722476
"xpack.ingestPipelines.list.pipelineDetails.cloneActionLabel": "クローンを作成",
2247822477
"xpack.ingestPipelines.list.pipelineDetails.closeButtonLabel": "閉じる",
2247922478
"xpack.ingestPipelines.list.pipelineDetails.deleteActionLabel": "削除",

x-pack/platform/plugins/private/translations/translations/zh-CN.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22110,7 +22110,6 @@
2211022110
"xpack.ingestPipelines.list.loadingMessage": "正在加载管道……",
2211122111
"xpack.ingestPipelines.list.loadPipelineReloadButton": "重试",
2211222112
"xpack.ingestPipelines.list.manageProcessorsLinkText": "管理处理器",
22113-
"xpack.ingestPipelines.list.notFoundFlyoutMessage": "未找到管道",
2211422113
"xpack.ingestPipelines.list.pipelineDetails.cloneActionLabel": "克隆",
2211522114
"xpack.ingestPipelines.list.pipelineDetails.closeButtonLabel": "关闭",
2211622115
"xpack.ingestPipelines.list.pipelineDetails.deleteActionLabel": "删除",

x-pack/platform/plugins/shared/ingest_pipelines/__jest__/client_integration/helpers/http_requests.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface ResponseError {
1313
statusCode: number;
1414
message: string | Error;
1515
attributes?: Record<string, any>;
16+
error?: string | Error;
1617
}
1718

1819
// Register helpers to mock HTTP Requests
@@ -59,7 +60,7 @@ const registerHttpRequestMockHelpers = (
5960
pipelineName: string,
6061
response?: object,
6162
error?: ResponseError
62-
) => mockResponse('GET', `${API_BASE_PATH}/${pipelineName}`, response, error);
63+
) => mockResponse('GET', `${API_BASE_PATH}/${encodeURIComponent(pipelineName)}`, response, error);
6364

6465
const setDeletePipelineResponse = (
6566
pipelineName: string,

x-pack/platform/plugins/shared/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,6 @@ import { PipelinesList } from '../../../public/application/sections/pipelines_li
1818
import { WithAppDependencies } from './setup_environment';
1919
import { getListPath, ROUTES } from '../../../public/application/services/navigation';
2020

21-
const testBedConfig: AsyncTestBedConfig = {
22-
memoryRouter: {
23-
initialEntries: [getListPath()],
24-
componentRoutePath: ROUTES.list,
25-
},
26-
doMountAsync: true,
27-
};
28-
2921
export type PipelineListTestBed = TestBed<PipelineListTestSubjects> & {
3022
actions: ReturnType<typeof createActions>;
3123
};
@@ -88,7 +80,18 @@ const createActions = (testBed: TestBed) => {
8880
};
8981
};
9082

91-
export const setup = async (httpSetup: HttpSetup): Promise<PipelineListTestBed> => {
83+
export const setup = async (
84+
httpSetup: HttpSetup,
85+
queryParams: string = ''
86+
): Promise<PipelineListTestBed> => {
87+
const testBedConfig: AsyncTestBedConfig = {
88+
memoryRouter: {
89+
initialEntries: [`${getListPath()}${queryParams}`],
90+
componentRoutePath: ROUTES.list,
91+
},
92+
doMountAsync: true,
93+
};
94+
9295
const initTestBed = registerTestBed(WithAppDependencies(PipelinesList, httpSetup), testBedConfig);
9396
const testBed = await initTestBed();
9497

@@ -111,4 +114,11 @@ export type PipelineListTestSubjects =
111114
| 'sectionLoading'
112115
| 'pipelineLoadError'
113116
| 'jsonCodeBlock'
114-
| 'reloadButton';
117+
| 'reloadButton'
118+
| 'pipelineErrorFlyout'
119+
| 'pipelineErrorFlyout.title'
120+
| 'pipelineError'
121+
| 'pipelineError.cause'
122+
| 'missingCustomPipeline'
123+
| 'missingCustomPipeline.cause'
124+
| 'createCustomPipeline';

x-pack/platform/plugins/shared/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,16 +126,37 @@ describe('<PipelinesList />', () => {
126126

127127
test('should show the details of a pipeline', async () => {
128128
const { find, exists, actions } = testBed;
129+
const { name: pipelineName } = pipeline1;
130+
httpRequestsMockHelpers.setLoadPipelineResponse(pipelineName, pipeline1);
129131

130132
await actions.clickPipelineAt(0);
131133

132134
expect(exists('pipelinesTable')).toBe(true);
133135
expect(exists('pipelineDetails')).toBe(true);
134-
expect(find('pipelineDetails.title').text()).toBe(pipeline1.name);
136+
expect(find('pipelineDetails.title').text()).toBe(pipelineName);
137+
});
138+
139+
test('should show load details of a pipeline if added to the url', async () => {
140+
const { name: pipelineName } = pipeline1;
141+
142+
httpRequestsMockHelpers.setLoadPipelineResponse(pipelineName, pipeline1);
143+
144+
await act(async () => {
145+
testBed = await setup(httpSetup, `?pipeline=${pipelineName}`);
146+
});
147+
148+
testBed.component.update();
149+
150+
const { find, exists } = testBed;
151+
152+
expect(exists('pipelinesTable')).toBe(true);
153+
expect(exists('pipelineDetails')).toBe(true);
154+
expect(find('pipelineDetails.title').text()).toBe(pipelineName);
135155
});
136156

137157
test('Replaces newline characters for spaces in flyout for json blocks', async () => {
138158
const { find, actions } = testBed;
159+
httpRequestsMockHelpers.setLoadPipelineResponse(pipeline2.name, pipeline2);
139160

140161
await actions.clickPipelineAt(1);
141162

@@ -174,6 +195,54 @@ describe('<PipelinesList />', () => {
174195
expect.anything()
175196
);
176197
});
198+
199+
describe('Pipeline error flyout', () => {
200+
const error = {
201+
statusCode: 404,
202+
message: 'Not Found',
203+
error: 'Not Found',
204+
};
205+
test('should render an error message flyout if error fetching pipeline', async () => {
206+
const nonExistingPipeline = 'nonExistingPipeline';
207+
208+
httpRequestsMockHelpers.setLoadPipelineResponse(nonExistingPipeline, {}, error);
209+
await act(async () => {
210+
testBed = await setup(httpSetup, `?pipeline=${nonExistingPipeline}`);
211+
});
212+
213+
testBed.component.update();
214+
215+
const { find, exists } = testBed;
216+
217+
expect(exists('pipelinesTable')).toBe(true);
218+
expect(exists('pipelineErrorFlyout')).toBe(true);
219+
expect(find('pipelineErrorFlyout.title').text()).toBe(nonExistingPipeline);
220+
expect(exists('pipelineError')).toBe(true);
221+
expect(find('pipelineError.cause').text()).toBe('Not Found');
222+
});
223+
224+
test('should render a create pipeline warning if @custom pipeline does not exist', async () => {
225+
const customPipeline = 'pipeline@custom';
226+
227+
httpRequestsMockHelpers.setLoadPipelineResponse(customPipeline, {}, error);
228+
await act(async () => {
229+
testBed = await setup(httpSetup, `?pipeline=${customPipeline}`);
230+
});
231+
232+
testBed.component.update();
233+
234+
const { find, exists } = testBed;
235+
236+
expect(exists('pipelinesTable')).toBe(true);
237+
expect(exists('pipelineErrorFlyout')).toBe(true);
238+
expect(find('pipelineErrorFlyout.title').text()).toBe(customPipeline);
239+
expect(exists('missingCustomPipeline')).toBe(true);
240+
expect(find('missingCustomPipeline.cause').text()).toBe(
241+
`The pipeline ${customPipeline} does not exist.`
242+
);
243+
expect(exists('createCustomPipeline')).toBe(true);
244+
});
245+
});
177246
});
178247

179248
describe('No pipelines', () => {

x-pack/platform/plugins/shared/ingest_pipelines/public/application/sections/pipelines_list/main.tsx

Lines changed: 13 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,9 @@ import { useCheckManageProcessorsPrivileges } from '../manage_processors';
3737

3838
import { EmptyList } from './empty_list';
3939
import { PipelineTable } from './table';
40-
import { PipelineDetailsFlyout } from './details_flyout';
41-
import { PipelineNotFoundFlyout } from './not_found_flyout';
4240
import { PipelineDeleteModal } from './delete_modal';
4341
import { getErrorText } from '../utils';
42+
import { PipelineFlyout } from './pipeline_flyout';
4443

4544
const getPipelineNameFromLocation = (location: Location) => {
4645
const { pipeline } = parse(location.search.substring(1));
@@ -54,7 +53,6 @@ export const PipelinesList: React.FunctionComponent<RouteComponentProps> = ({
5453
const { services } = useKibana();
5554
const pipelineNameFromLocation = getPipelineNameFromLocation(location);
5655

57-
const [selectedPipeline, setSelectedPipeline] = useState<Pipeline | undefined>(undefined);
5856
const [showFlyout, setShowFlyout] = useState<boolean>(false);
5957
const [showPopover, setShowPopover] = useState<boolean>(false);
6058

@@ -71,8 +69,6 @@ export const PipelinesList: React.FunctionComponent<RouteComponentProps> = ({
7169

7270
useEffect(() => {
7371
if (pipelineNameFromLocation && data?.length) {
74-
const pipeline = data.find((p) => p.name === pipelineNameFromLocation);
75-
setSelectedPipeline(pipeline);
7672
setShowFlyout(true);
7773
}
7874
}, [pipelineNameFromLocation, data]);
@@ -227,30 +223,6 @@ export const PipelinesList: React.FunctionComponent<RouteComponentProps> = ({
227223
</EuiButtonEmpty>
228224
);
229225

230-
const renderFlyout = (): React.ReactNode => {
231-
if (!showFlyout) {
232-
return;
233-
}
234-
if (selectedPipeline) {
235-
return (
236-
<PipelineDetailsFlyout
237-
pipeline={selectedPipeline}
238-
onClose={() => {
239-
setSelectedPipeline(undefined);
240-
goHome();
241-
}}
242-
onEditClick={goToEditPipeline}
243-
onCloneClick={goToClonePipeline}
244-
onDeleteClick={setPipelinesToDelete}
245-
/>
246-
);
247-
} else {
248-
// Somehow we triggered show pipeline details, but do not have a pipeline.
249-
// We assume not found.
250-
return <PipelineNotFoundFlyout onClose={goHome} pipelineName={pipelineNameFromLocation} />;
251-
}
252-
};
253-
254226
return (
255227
<>
256228
<EuiPageHeader
@@ -283,14 +255,24 @@ export const PipelinesList: React.FunctionComponent<RouteComponentProps> = ({
283255
pipelines={data as Pipeline[]}
284256
/>
285257

286-
{renderFlyout()}
258+
{showFlyout && (
259+
<PipelineFlyout
260+
pipeline={pipelineNameFromLocation}
261+
onClose={() => {
262+
goHome();
263+
}}
264+
onEditClick={goToEditPipeline}
265+
onCloneClick={goToClonePipeline}
266+
onDeleteClick={setPipelinesToDelete}
267+
/>
268+
)}
269+
287270
{pipelinesToDelete?.length > 0 ? (
288271
<PipelineDeleteModal
289272
callback={(deleteResponse) => {
290273
if (deleteResponse?.hasDeletedPipelines) {
291274
// reload pipelines list
292275
resendRequest();
293-
setSelectedPipeline(undefined);
294276
goHome();
295277
}
296278
setPipelinesToDelete([]);

x-pack/platform/plugins/shared/ingest_pipelines/public/application/sections/pipelines_list/not_found_flyout.tsx

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,37 +7,94 @@
77

88
import React, { FunctionComponent } from 'react';
99
import { FormattedMessage } from '@kbn/i18n-react';
10-
import { EuiFlyout, EuiFlyoutBody, EuiCallOut } from '@elastic/eui';
10+
import { EuiFlyout, EuiFlyoutBody, EuiCallOut, EuiCode, EuiButton } from '@elastic/eui';
1111
import { EuiFlyoutHeader, EuiTitle } from '@elastic/eui';
12+
import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public';
13+
import { Error, useKibana } from '../../../shared_imports';
14+
import { getCreatePath } from '../../services/navigation';
15+
import { getErrorText, isIntegrationsPipeline } from '../utils';
1216

1317
interface Props {
1418
onClose: () => void;
15-
pipelineName: string | string[] | null | undefined;
19+
pipelineName: string;
20+
error: Error;
1621
}
1722

18-
export const PipelineNotFoundFlyout: FunctionComponent<Props> = ({ onClose, pipelineName }) => {
23+
export const PipelineNotFoundFlyout: FunctionComponent<Props> = ({
24+
onClose,
25+
pipelineName,
26+
error,
27+
}) => {
28+
const { history } = useKibana().services;
29+
const renderErrorCallOut = () => {
30+
if (error.statusCode === 404 && isIntegrationsPipeline(pipelineName)) {
31+
return (
32+
<EuiCallOut
33+
title={
34+
<FormattedMessage
35+
id="xpack.ingestPipelines.list.missingCustomPipeline.title"
36+
defaultMessage="Custom pipeline does not exist"
37+
/>
38+
}
39+
color="warning"
40+
iconType="warning"
41+
data-test-subj="missingCustomPipeline"
42+
>
43+
<p data-test-subj="cause">
44+
<FormattedMessage
45+
id="xpack.ingestPipelines.list.missingCustomPipeline.text"
46+
defaultMessage="The pipeline {pipelineName} does not exist."
47+
values={{
48+
pipelineName: <EuiCode>{pipelineName}</EuiCode>,
49+
}}
50+
/>
51+
</p>
52+
<EuiButton
53+
color="warning"
54+
{...reactRouterNavigate(
55+
history,
56+
getCreatePath({
57+
pipelineName,
58+
})
59+
)}
60+
data-test-subj="createCustomPipeline"
61+
>
62+
<FormattedMessage
63+
id="xpack.ingestPipelines.list.missingCustomPipeline.button"
64+
defaultMessage="Create pipeline"
65+
/>
66+
</EuiButton>
67+
</EuiCallOut>
68+
);
69+
}
70+
return (
71+
<EuiCallOut
72+
title={
73+
<FormattedMessage
74+
id="xpack.ingestPipelines.list.loadingError"
75+
defaultMessage="Error loading pipeline"
76+
/>
77+
}
78+
color="danger"
79+
iconType="warning"
80+
data-test-subj="pipelineError"
81+
>
82+
<p data-test-subj="cause">{getErrorText(error)}</p>
83+
</EuiCallOut>
84+
);
85+
};
86+
1987
return (
20-
<EuiFlyout onClose={onClose} size="m" maxWidth={550}>
88+
<EuiFlyout onClose={onClose} size="m" maxWidth={550} data-test-subj="pipelineErrorFlyout">
2189
<EuiFlyoutHeader>
2290
{pipelineName && (
23-
<EuiTitle id="notFoundFlyoutTitle">
91+
<EuiTitle id="notFoundFlyoutTitle" data-test-subj="title">
2492
<h2>{pipelineName}</h2>
2593
</EuiTitle>
2694
)}
2795
</EuiFlyoutHeader>
2896

29-
<EuiFlyoutBody>
30-
<EuiCallOut
31-
title={
32-
<FormattedMessage
33-
id="xpack.ingestPipelines.list.notFoundFlyoutMessage"
34-
defaultMessage="Pipeline not found"
35-
/>
36-
}
37-
color="danger"
38-
iconType="warning"
39-
/>
40-
</EuiFlyoutBody>
97+
<EuiFlyoutBody>{renderErrorCallOut()} </EuiFlyoutBody>
4198
</EuiFlyout>
4299
);
43100
};

0 commit comments

Comments
 (0)